From 720e54cd8aedf9551bb773b94af116d111bb47e0 Mon Sep 17 00:00:00 2001 From: buildingjoshbetter <236675935+buildingjoshbetter@users.noreply.github.com> Date: Mon, 12 Oct 2026 17:46:53 -0500 Subject: [PATCH 01/16] chore: Compose reviewed code-only reliability candidate --- .github/workflows/ci.yml | 63 +- .github/workflows/publish.yml | 35 +- install.ps1 | 84 +- install.sh | 88 +- pyproject.toml | 11 +- tests/conftest.py | 28 +- .../ingest/test_issue_558_async_stale_scan.py | 2 +- tests/ingest/test_stop_hook_safety.py | 18 +- tests/mcp-wheel-smoke.py | 226 ++ tests/test-claude-stop-fallback-722.py | 132 + tests/test-cluster-outer-publication-753.py | 395 +++ tests/test-cluster-publication-749.py | 656 ++++ ...st-clustered-search-materialization-754.py | 669 ++++ tests/test-clustering-health-720.py | 482 +++ tests/test-clustering-parameters-721.py | 121 + tests/test-codex-projection-762.py | 473 +++ tests/test-codex-rollout-762.py | 693 +++++ tests/test-dunbar-publication-753.py | 596 ++++ tests/test-episode-index-writes-751.py | 577 ++++ tests/test-hermes-native-bridge-765.py | 689 +++++ tests/test-inference-ownership-738.py | 331 ++ tests/test-ingestion-completion-768.py | 572 ++++ tests/test-ingestion-outcomes-768.py | 550 ++++ .../test-ingestion-recall-invalidation-769.py | 342 +++ tests/test-installer-model-readiness-767.py | 459 +++ tests/test-kimi-toml-serialization-833.py | 366 +++ tests/test-l0-style-accumulator-747.py | 755 +++++ tests/test-l0-style-incremental-747.py | 960 ++++++ tests/test-l0-style-maintenance-747.py | 965 ++++++ tests/test-l0-style-routing-747.py | 1437 +++++++++ tests/test-landmark-resolution-755.py | 595 ++++ tests/test-local-inference-ownership-740.py | 361 +++ tests/test-maintenance-adapters-753.py | 957 ++++++ tests/test-maintenance-engine-routing-753.py | 846 +++++ .../test-maintenance-fixture-isolation-753.py | 245 ++ ...st-maintenance-foreground-admission-753.py | 501 +++ tests/test-maintenance-ownership-753.py | 362 +++ tests/test-maintenance-publication-753.py | 452 +++ tests/test-maintenance-runner-753.py | 589 ++++ tests/test-maintenance-source-revision-753.py | 342 +++ tests/test-model-admission-737.py | 503 +++ tests/test-model-allocation-preflight-297.py | 486 +++ tests/test-model-daemon-admission-297.py | 208 ++ tests/test-model-microbatches-735.py | 1408 +++++++++ tests/test-model-process-admission-297.py | 744 +++++ tests/test-model-request-deadlines-736.py | 419 +++ tests/test-model-residency-297.py | 504 +++ tests/test-model-server-sharing-741.py | 510 ++++ tests/test-mps-budget-743.py | 630 ++++ tests/test-native-hook-output-763.py | 498 +++ tests/test-openclaw-native-bridge-764.py | 373 +++ tests/test-preferences-status-746.py | 308 ++ tests/test-prepared-target-795.py | 655 ++++ tests/test-profile-freshness-748.py | 179 ++ ...test-rebuild-foreground-publication-756.py | 376 +++ tests/test-rebuild-source-streaming-756.py | 1210 ++++++++ tests/test-rebuild-value-exactness-756.py | 348 +++ tests/test-source-fts-exactness-753.py | 504 +++ tests/test-source-invalidation-748.py | 704 +++++ tests/test-source-provenance-745.py | 395 +++ tests/test-source-update-fence-748.py | 267 ++ tests/test-style-getter-freshness-748.py | 148 + tests/test-surprise-index-transactions-750.py | 424 +++ tests/test-telemetry-disclosure-760.py | 203 ++ tests/test-throttle-sampling-742.py | 392 +++ tests/test-tier-activation-journal-795.py | 645 ++++ tests/test-tier-activation-policy-795.py | 458 +++ tests/test-tier-active-initialization-795.py | 488 +++ tests/test-tier-device-policy-732.py | 111 + tests/test-tier-foreground-native-795.py | 664 ++++ tests/test-tier-job-binding-795.py | 462 +++ tests/test-tier-job-identity-795.py | 528 ++++ tests/test-tier-mcp-serving-795.py | 306 ++ tests/test-tier-metadata-publication-795.py | 176 ++ tests/test-tier-public-adoption-795.py | 1687 ++++++++++ tests/test-tier-public-boundaries-795.py | 2452 +++++++++++++++ tests/test-tier-rebuild-atomicity-752.py | 454 +++ tests/test-tier-rebuild-oom-recovery-297.py | 418 +++ tests/test-tier-rebuild-source-756.py | 524 ++++ tests/test-tier-reconnect-admission-684.py | 746 +++++ tests/test-tier-runtime-bridge-795.py | 1204 ++++++++ tests/test-tier-serving-gate-795.py | 572 ++++ tests/test-tier-serving-receipt-795.py | 480 +++ tests/test-tier-streamed-worker-756.py | 646 ++++ tests/test-tier-target-model-795.py | 526 ++++ tests/test-tier-warm-admission-795.py | 494 +++ tests/test-tier-writer-fence-795.py | 643 ++++ tests/test-transcript-allowlists-761.py | 605 ++++ tests/test-transcript-inline-306.py | 157 + tests/test_codex_adapter.py | 16 +- tests/test_configure_reembed.py | 21 + tests/test_configure_tier_persist.py | 14 +- tests/test_consolidation_gaps.py | 193 +- tests/test_cursor_adapter.py | 20 +- tests/test_gemini_adapter.py | 4 +- tests/test_hardening.py | 14 +- tests/test_hermes_adapter.py | 8 +- tests/test_issue_396_memory_intensity.py | 13 +- tests/test_issue_458_socket_bounds.py | 56 +- tests/test_issue_460_warm_search.py | 2 + tests/test_issue_489_mps_safety.py | 10 +- tests/test_issue_497_edge_persist.py | 13 +- tests/test_issue_498_auto_consolidate.py | 86 +- tests/test_issue_502_config.py | 45 +- tests/test_issue_511_write_path_opt.py | 20 +- tests/test_issue_514_model_server.py | 47 +- tests/test_issue_577_model_server_oom.py | 204 +- tests/test_issue_589_directives_completion.py | 36 +- tests/test_issue_590_build_vectors_batch.py | 3 + tests/test_issue_598_windows_socket.py | 4 +- ...t_issue_600_antigravity_groq_deepsearch.py | 9 + .../test_issue_630_search_supplement_merge.py | 27 +- tests/test_issue_644_ingest_races.py | 1 + tests/test_issue_646_model_server.py | 10 +- tests/test_issue_647_vector_rebuild.py | 3 + tests/test_issue_648_vec_compat.py | 13 +- tests/test_l0_style_vector.py | 9 +- tests/test_l4_entity_sheets_disabled.py | 10 +- tests/test_l5_surprise_rerank.py | 26 +- tests/test_mcp_safety.py | 22 +- tests/test_openclaw_adapter.py | 14 +- tests/test_platform_compat.py | 17 +- tests/test_reranker_tier.py | 83 +- tests/test_sensors.py | 28 +- tests/test_state_machine.py | 8 +- truememory/__init__.py | 1 + truememory/_platform.py | 75 + truememory/client.py | 19 + truememory/clustering.py | 475 ++- truememory/consolidation.py | 23 +- truememory/embedding_target.py | 124 + truememory/engine.py | 2357 +++++++++----- truememory/hooks/adapters/base.py | 43 + truememory/hooks/adapters/claude.py | 233 +- truememory/hooks/adapters/codex.py | 67 +- truememory/hooks/adapters/cursor.py | 23 +- truememory/hooks/adapters/gemini.py | 36 +- truememory/hooks/adapters/hermes.py | 118 +- truememory/hooks/adapters/kimi.py | 15 +- truememory/hooks/adapters/openclaw.py | 51 +- truememory/hooks/templates/openclaw/index.js | 258 +- .../templates/openclaw/openclaw.plugin.json | 8 + truememory/hybrid.py | 11 +- truememory/ingest/__init__.py | 4 + truememory/ingest/cli.py | 420 +-- truememory/ingest/completion.py | 770 +++++ truememory/ingest/dedup.py | 2 + truememory/ingest/encoding_gate.py | 47 +- truememory/ingest/extractor.py | 208 +- truememory/ingest/hooks/_shared.py | 118 +- truememory/ingest/hooks/compact.py | 31 +- truememory/ingest/hooks/hermes.py | 719 +++++ truememory/ingest/hooks/openclaw.py | 69 + truememory/ingest/hooks/output.py | 58 + truememory/ingest/hooks/session_start.py | 82 +- truememory/ingest/hooks/stop.py | 153 +- truememory/ingest/hooks/user_prompt_submit.py | 33 +- truememory/ingest/pipeline.py | 556 +++- truememory/ingest/provenance.py | 263 ++ truememory/ingest/transcript.py | 997 +++++- truememory/maintenance.py | 2712 +++++++++++++++++ truememory/mcp_server.py | 419 +-- truememory/model_client.py | 464 ++- truememory/model_server.py | 1460 +++++++-- truememory/mps_utils.py | 248 +- truememory/personality.py | 745 +++-- truememory/personality_style_vec.py | 439 ++- truememory/predictive.py | 103 +- truememory/rebuild_source.py | 390 +++ truememory/reranker.py | 166 +- truememory/storage.py | 505 ++- truememory/telemetry.py | 35 +- truememory/temporal.py | 482 ++- truememory/tier_switch/activation.py | 638 ++++ truememory/tier_switch/cache.py | 29 +- truememory/tier_switch/job.py | 505 +++ truememory/tier_switch/manager.py | 879 ++++-- truememory/tier_switch/projection.py | 215 ++ truememory/tier_switch/runtime.py | 827 +++++ truememory/tier_switch/sensors.py | 73 +- truememory/tier_switch/serving.py | 438 +++ truememory/tier_switch/source.py | 561 ++++ truememory/tier_switch/state_machine.py | 34 +- truememory/tier_switch/throttler.py | 276 +- truememory/tier_switch/worker.py | 432 ++- truememory/tier_switch/writer.py | 196 ++ truememory/vector_search.py | 1646 ++++++---- 187 files changed, 66036 insertions(+), 4324 deletions(-) create mode 100644 tests/mcp-wheel-smoke.py create mode 100644 tests/test-claude-stop-fallback-722.py create mode 100644 tests/test-cluster-outer-publication-753.py create mode 100644 tests/test-cluster-publication-749.py create mode 100644 tests/test-clustered-search-materialization-754.py create mode 100644 tests/test-clustering-health-720.py create mode 100644 tests/test-clustering-parameters-721.py create mode 100644 tests/test-codex-projection-762.py create mode 100644 tests/test-codex-rollout-762.py create mode 100644 tests/test-dunbar-publication-753.py create mode 100644 tests/test-episode-index-writes-751.py create mode 100644 tests/test-hermes-native-bridge-765.py create mode 100644 tests/test-inference-ownership-738.py create mode 100644 tests/test-ingestion-completion-768.py create mode 100644 tests/test-ingestion-outcomes-768.py create mode 100644 tests/test-ingestion-recall-invalidation-769.py create mode 100644 tests/test-installer-model-readiness-767.py create mode 100644 tests/test-kimi-toml-serialization-833.py create mode 100644 tests/test-l0-style-accumulator-747.py create mode 100644 tests/test-l0-style-incremental-747.py create mode 100644 tests/test-l0-style-maintenance-747.py create mode 100644 tests/test-l0-style-routing-747.py create mode 100644 tests/test-landmark-resolution-755.py create mode 100644 tests/test-local-inference-ownership-740.py create mode 100644 tests/test-maintenance-adapters-753.py create mode 100644 tests/test-maintenance-engine-routing-753.py create mode 100644 tests/test-maintenance-fixture-isolation-753.py create mode 100644 tests/test-maintenance-foreground-admission-753.py create mode 100644 tests/test-maintenance-ownership-753.py create mode 100644 tests/test-maintenance-publication-753.py create mode 100644 tests/test-maintenance-runner-753.py create mode 100644 tests/test-maintenance-source-revision-753.py create mode 100644 tests/test-model-admission-737.py create mode 100644 tests/test-model-allocation-preflight-297.py create mode 100644 tests/test-model-daemon-admission-297.py create mode 100644 tests/test-model-microbatches-735.py create mode 100644 tests/test-model-process-admission-297.py create mode 100644 tests/test-model-request-deadlines-736.py create mode 100644 tests/test-model-residency-297.py create mode 100644 tests/test-model-server-sharing-741.py create mode 100644 tests/test-mps-budget-743.py create mode 100644 tests/test-native-hook-output-763.py create mode 100644 tests/test-openclaw-native-bridge-764.py create mode 100644 tests/test-preferences-status-746.py create mode 100644 tests/test-prepared-target-795.py create mode 100644 tests/test-profile-freshness-748.py create mode 100644 tests/test-rebuild-foreground-publication-756.py create mode 100644 tests/test-rebuild-source-streaming-756.py create mode 100644 tests/test-rebuild-value-exactness-756.py create mode 100644 tests/test-source-fts-exactness-753.py create mode 100644 tests/test-source-invalidation-748.py create mode 100644 tests/test-source-provenance-745.py create mode 100644 tests/test-source-update-fence-748.py create mode 100644 tests/test-style-getter-freshness-748.py create mode 100644 tests/test-surprise-index-transactions-750.py create mode 100644 tests/test-telemetry-disclosure-760.py create mode 100644 tests/test-throttle-sampling-742.py create mode 100644 tests/test-tier-activation-journal-795.py create mode 100644 tests/test-tier-activation-policy-795.py create mode 100644 tests/test-tier-active-initialization-795.py create mode 100644 tests/test-tier-device-policy-732.py create mode 100644 tests/test-tier-foreground-native-795.py create mode 100644 tests/test-tier-job-binding-795.py create mode 100644 tests/test-tier-job-identity-795.py create mode 100644 tests/test-tier-mcp-serving-795.py create mode 100644 tests/test-tier-metadata-publication-795.py create mode 100644 tests/test-tier-public-adoption-795.py create mode 100644 tests/test-tier-public-boundaries-795.py create mode 100644 tests/test-tier-rebuild-atomicity-752.py create mode 100644 tests/test-tier-rebuild-oom-recovery-297.py create mode 100644 tests/test-tier-rebuild-source-756.py create mode 100644 tests/test-tier-reconnect-admission-684.py create mode 100644 tests/test-tier-runtime-bridge-795.py create mode 100644 tests/test-tier-serving-gate-795.py create mode 100644 tests/test-tier-serving-receipt-795.py create mode 100644 tests/test-tier-streamed-worker-756.py create mode 100644 tests/test-tier-target-model-795.py create mode 100644 tests/test-tier-warm-admission-795.py create mode 100644 tests/test-tier-writer-fence-795.py create mode 100644 tests/test-transcript-allowlists-761.py create mode 100644 tests/test-transcript-inline-306.py create mode 100644 truememory/embedding_target.py create mode 100644 truememory/ingest/completion.py create mode 100644 truememory/ingest/hooks/hermes.py create mode 100644 truememory/ingest/hooks/openclaw.py create mode 100644 truememory/ingest/hooks/output.py create mode 100644 truememory/ingest/provenance.py create mode 100644 truememory/maintenance.py create mode 100644 truememory/rebuild_source.py create mode 100644 truememory/tier_switch/activation.py create mode 100644 truememory/tier_switch/job.py create mode 100644 truememory/tier_switch/projection.py create mode 100644 truememory/tier_switch/runtime.py create mode 100644 truememory/tier_switch/serving.py create mode 100644 truememory/tier_switch/source.py create mode 100644 truememory/tier_switch/writer.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1996c090..762659bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,17 +115,38 @@ jobs: # Hunter F26: catch packaging regressions (missing py.typed, metadata # errors, broken README rendering, sdist contents) in CI instead of - # at release time. Runs once on 3.12 only — duplicating across the - # matrix wastes CI minutes without additional signal. + # at release time. Exercise both MCP dependency boundaries on each OS. build-check: - runs-on: ubuntu-latest - timeout-minutes: 10 + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: ["3.12"] + mcp-version: [minimum, latest] + pydantic-version: [latest] + include: + - os: ubuntu-latest + python-version: "3.10" + mcp-version: minimum + pydantic-version: latest + # Exercise the MCP/Pydantic floor pair and the release that removed + # eval_type_backport. Other dependencies still use normal resolution. + - os: ubuntu-latest + python-version: "3.10" + mcp-version: minimum + pydantic-version: "2.11.0" + - os: ubuntu-latest + python-version: "3.12" + mcp-version: minimum + pydantic-version: "2.14.0" steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Set up Python 3.12 + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: - python-version: "3.12" + python-version: ${{ matrix.python-version }} cache: 'pip' cache-dependency-path: pyproject.toml - name: Install build tooling @@ -134,10 +155,28 @@ jobs: run: python -m build - name: Verify package metadata run: twine check --strict dist/* - # Issue #654 (M-83): actually install the built wheel and exercise the - # console entry point, so a broken/missing entry-point or import-time - # failure is caught here (not just at release). - - name: Install wheel and smoke-test console script + # Issue #757: an editable install or --help alone does not exercise + # the installed wheel's MCP handshake and serialized tool metadata. + - name: Install wheel and verify MCP compatibility + shell: python + env: + MCP_VERSION: ${{ matrix.mcp-version }} + PYDANTIC_VERSION: ${{ matrix.pydantic-version }} run: | - pip install dist/*.whl - truememory-mcp --help + import os + from pathlib import Path + import subprocess + import venv + + wheel, = Path("dist").glob("*.whl") + env_dir = Path(os.environ["RUNNER_TEMP"]) / "mcp-wheel-env" + venv.EnvBuilder(with_pip=True).create(env_dir) + python = env_dir / ("Scripts/python.exe" if os.name == "nt" else "bin/python") + requirements = [str(wheel.resolve())] + if os.environ["MCP_VERSION"] == "minimum": + requirements.append("mcp[cli]==1.21.1") + if os.environ["PYDANTIC_VERSION"] != "latest": + requirements.append("pydantic==" + os.environ["PYDANTIC_VERSION"]) + subprocess.run([str(python), "-m", "pip", "install", *requirements], check=True) + subprocess.run([str(python), "-m", "pip", "check"], check=True) + subprocess.run([str(python), "-I", "tests/mcp-wheel-smoke.py"], check=True) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a3324ba6..90bdc63c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -32,7 +32,17 @@ jobs: build-check: runs-on: ubuntu-latest - timeout-minutes: 10 + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + mcp-version: [minimum, latest] + pydantic-version: [latest] + include: + - mcp-version: minimum + pydantic-version: "2.11.0" + - mcp-version: minimum + pydantic-version: "2.14.0" steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 @@ -46,6 +56,29 @@ jobs: run: python -m build - name: Verify package metadata run: twine check --strict dist/* + - name: Install wheel and verify MCP compatibility + shell: python + env: + MCP_VERSION: ${{ matrix.mcp-version }} + PYDANTIC_VERSION: ${{ matrix.pydantic-version }} + run: | + import os + from pathlib import Path + import subprocess + import venv + + wheel, = Path("dist").glob("*.whl") + env_dir = Path(os.environ["RUNNER_TEMP"]) / "mcp-wheel-env" + venv.EnvBuilder(with_pip=True).create(env_dir) + python = env_dir / ("Scripts/python.exe" if os.name == "nt" else "bin/python") + requirements = [str(wheel.resolve())] + if os.environ["MCP_VERSION"] == "minimum": + requirements.append("mcp[cli]==1.21.1") + if os.environ["PYDANTIC_VERSION"] != "latest": + requirements.append("pydantic==" + os.environ["PYDANTIC_VERSION"]) + subprocess.run([str(python), "-m", "pip", "install", *requirements], check=True) + subprocess.run([str(python), "-m", "pip", "check"], check=True) + subprocess.run([str(python), "-I", "tests/mcp-wheel-smoke.py"], check=True) version-check: runs-on: ubuntu-latest diff --git a/install.ps1 b/install.ps1 index 4fa9d745..94c3432e 100644 --- a/install.ps1 +++ b/install.ps1 @@ -86,7 +86,7 @@ if ($LASTEXITCODE -ne 0) { } # ---------- step 3: install truememory as a uv tool ---------- -Say "installing $PKG_SPEC (~3-5 min on first run, downloads all tier models)..." +Say "installing $PKG_SPEC (~3-5 min on first run)..." & uv tool uninstall truememory *> $null if ($LASTEXITCODE -gt 1) { Warn "uv tool uninstall returned $LASTEXITCODE — proceeding, but result may be partial" @@ -118,6 +118,8 @@ if ($uvToolDir) { } # ---------- step 4: auto-configure Claude ---------- +$setupStatus = "skipped" +$hookStatus = "skipped" if ($env:TRUEMEMORY_SKIP_SETUP -eq "1") { Say "skipping Claude setup (TRUEMEMORY_SKIP_SETUP=1)" } elseif (-not $toolPython) { @@ -126,41 +128,52 @@ if ($env:TRUEMEMORY_SKIP_SETUP -eq "1") { } else { Say "configuring Claude Code / Claude Desktop..." & $toolPython -m truememory.mcp_server --setup - if ($LASTEXITCODE -ne 0) { + if ($LASTEXITCODE -eq 0) { $setupStatus = "command succeeded" } + else { + $setupStatus = "failed" Warn "auto-setup returned non-zero (re-run: python -m truememory.mcp_server --setup)" } Say "installing hooks and CLAUDE.md instructions..." & $toolPython -m truememory.ingest.cli install - if ($LASTEXITCODE -ne 0) { + if ($LASTEXITCODE -eq 0) { $hookStatus = "command succeeded" } + else { + $hookStatus = "failed" Warn "hook install returned non-zero (re-run: python -m truememory.ingest.cli install)" } } # ---------- step 5: pre-download models for all tiers ---------- -Say "pre-downloading models for all tiers (Edge + Base + Pro)..." -Say " this takes 2-5 min but means tier switching just works afterward." -if (Test-Path $toolPython) { - Say " [1/3] Edge reranker (MiniLM-L-6-v2, ~22MB)..." - & $toolPython -c "from sentence_transformers import CrossEncoder; CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')" - if ($LASTEXITCODE -eq 0) { Ok " [1/3] Edge reranker ready" } - else { Warn " [1/3] Edge reranker download failed (search still works without it)" } - - Say " [2/3] Base/Pro embedder (Qwen3-Embedding-0.6B, ~1.2GB)..." - & $toolPython -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('Qwen/Qwen3-Embedding-0.6B', truncate_dim=256)" - if ($LASTEXITCODE -eq 0) { Ok " [2/3] Base/Pro embedder ready" } - else { Warn " [2/3] Base/Pro embedder download failed (you can retry later or use Edge tier)" } - - Say " [3/3] Base/Pro reranker (gte-modernbert, ~600MB)..." - & $toolPython -c "from sentence_transformers import CrossEncoder; CrossEncoder('Alibaba-NLP/gte-reranker-modernbert-base')" - if ($LASTEXITCODE -eq 0) { Ok " [3/3] Base/Pro reranker ready" } - else { Warn " [3/3] Base/Pro reranker download failed (you can retry later or use Edge tier)" } - - Ok "all models pre-downloaded — tier switching is instant." +Say "pre-downloading the Edge reranker and Base/Pro models..." +Say " this can take 2-5 min; uncached models require network access." +$modelChecks = @( + @{ Label = "Edge reranker"; Status = "not checked"; Code = "from sentence_transformers import CrossEncoder; CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')" }, + @{ Label = "Base/Pro embedder"; Status = "not checked"; Code = "from sentence_transformers import SentenceTransformer; SentenceTransformer('Qwen/Qwen3-Embedding-0.6B', truncate_dim=256)" }, + @{ Label = "Base/Pro reranker"; Status = "not checked"; Code = "from sentence_transformers import CrossEncoder; CrossEncoder('Alibaba-NLP/gte-reranker-modernbert-base')" } +) +if ($toolPython -and (Test-Path -LiteralPath $toolPython)) { + foreach ($modelCheck in $modelChecks) { + Say " Pre-downloading $($modelCheck.Label)..." + $modelCheck.Status = "failed" + try { + & $toolPython -c $modelCheck.Code + if ($LASTEXITCODE -eq 0) { $modelCheck.Status = "ready" } + } catch { + Warn " $($modelCheck.Label) could not be loaded: $_" + } + if ($modelCheck.Status -eq "ready") { + Ok " $($modelCheck.Label) ready" + } else { + Warn " $($modelCheck.Label) pre-download failed" + Warn "Retry this model only: & (Join-Path (uv tool dir) 'truememory\Scripts\python.exe') -c `"$($modelCheck.Code)`"" + } + } } else { Warn "could not locate tool Python at $toolPython — skipping model pre-download" - Warn "models will download on first use instead" + Warn "Model readiness is unverified. Locate the tool environment with: uv tool dir" + Warn "After locating its Python, run: python -m truememory.ingest.cli setup" } +$modelReadyCount = @($modelChecks | Where-Object { $_.Status -eq "ready" }).Count # ---------- done ---------- Write-Host "" @@ -174,10 +187,31 @@ Write-Host @" a sauron company "@ -ForegroundColor Green -$installedVer = & $toolPython -c "from importlib.metadata import version; print(version('truememory'))" 2>$null +$installedVer = $null +if ($toolPython) { + try { + $installedVer = & $toolPython -c "from importlib.metadata import version; print(version('truememory'))" 2>$null + } catch { + $installedVer = $null + } +} if (-not $installedVer) { $installedVer = "unknown" } Write-Host "" -Ok "TrueMemory v$installedVer installed successfully." +Ok "TrueMemory v$installedVer package installed." +Say "Claude registration: $setupStatus" +Say "Hooks: $hookStatus" +Say "Model pre-download checks: $modelReadyCount/3 succeeded." +foreach ($modelCheck in $modelChecks) { + Say " $($modelCheck.Label): $($modelCheck.Status)" +} +if ($modelReadyCount -eq 3) { + Ok "All three requested model pre-download checks passed." +} else { + Warn "Model readiness incomplete; package installation is retained. Retry the failed or skipped checks before relying on those models." +} +Say "The Edge embedder was not checked by this pre-download step." +Say "Registration and hook command success do not verify runtime operation." +Say "Changing embedding models can still require re-embedding stored memories." Write-Host "" Write-Host " First time? Start a new Claude session and type:" -ForegroundColor Green Write-Host "" diff --git a/install.sh b/install.sh index 752cd3b9..5d7b94fb 100755 --- a/install.sh +++ b/install.sh @@ -94,7 +94,7 @@ main() { die "failed to install managed Python $TRUEMEMORY_PY (see error above)" # ---------- step 3: install truememory as a uv tool ---------- - say "installing $PKG_SPEC (~3-5 min on first run, downloads all tier models)..." + say "installing $PKG_SPEC (~3-5 min on first run)..." # Remove any existing install first to guarantee a clean slate. # Without this, uv may serve a cached older version even with --refresh. uv tool uninstall truememory >/dev/null 2>&1 || true @@ -111,6 +111,8 @@ main() { TOOL_PYTHON="$(uv tool dir)/truememory/bin/python" # ---------- step 4: auto-configure Claude ---------- + SETUP_STATUS="skipped" + HOOK_STATUS="skipped" if [ "${TRUEMEMORY_SKIP_SETUP:-}" = "1" ]; then say "skipping Claude setup (TRUEMEMORY_SKIP_SETUP=1)" elif [ ! -x "$TOOL_PYTHON" ]; then @@ -118,45 +120,73 @@ main() { warn "Re-run manually: python -m truememory.mcp_server --setup" else say "configuring Claude Code / Claude Desktop..." - "$TOOL_PYTHON" -m truememory.mcp_server --setup || \ + if "$TOOL_PYTHON" -m truememory.mcp_server --setup; then + SETUP_STATUS="command succeeded" + else + SETUP_STATUS="failed" warn "auto-setup returned non-zero (re-run: python -m truememory.mcp_server --setup)" + fi say "installing hooks and CLAUDE.md instructions..." - "$TOOL_PYTHON" -m truememory.ingest.cli install || \ + if "$TOOL_PYTHON" -m truememory.ingest.cli install; then + HOOK_STATUS="command succeeded" + else + HOOK_STATUS="failed" warn "hook install returned non-zero (re-run: python -m truememory.ingest.cli install)" + fi fi # ---------- step 5: pre-download models for all tiers ---------- - say "pre-downloading models for all tiers (Edge + Base + Pro)..." - say " this takes 2-5 min but means tier switching just works afterward." + say "pre-downloading the Edge reranker and Base/Pro models..." + say " this can take 2-5 min; uncached models require network access." say " you'll see download progress bars below." + MODEL_READY_COUNT=0 + EDGE_RERANKER_STATUS="not checked" + BASE_EMBEDDER_STATUS="not checked" + BASE_RERANKER_STATUS="not checked" + EDGE_RERANKER_CODE="from sentence_transformers import CrossEncoder; CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')" + BASE_EMBEDDER_CODE="from sentence_transformers import SentenceTransformer; SentenceTransformer('Qwen/Qwen3-Embedding-0.6B', truncate_dim=256)" + BASE_RERANKER_CODE="from sentence_transformers import CrossEncoder; CrossEncoder('Alibaba-NLP/gte-reranker-modernbert-base')" # Use the tool's Python (resolved in step 4) to run downloads inside the uv venv. if [ -x "$TOOL_PYTHON" ]; then - # Edge: Model2Vec embedder (usually bundled) + MiniLM reranker say " [1/3] Edge reranker (MiniLM-L-6-v2, ~22MB)..." - "$TOOL_PYTHON" -c " -from sentence_transformers import CrossEncoder -CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2') -" && ok " [1/3] Edge reranker ready" || warn " [1/3] Edge reranker download failed (search still works without it)" + if "$TOOL_PYTHON" -c "$EDGE_RERANKER_CODE"; then + EDGE_RERANKER_STATUS="ready" + MODEL_READY_COUNT=$((MODEL_READY_COUNT + 1)) + ok " [1/3] Edge reranker ready" + else + EDGE_RERANKER_STATUS="failed" + warn " [1/3] Edge reranker pre-download failed" + warn "Retry this model only: \"\$(uv tool dir)/truememory/bin/python\" -c \"$EDGE_RERANKER_CODE\"" + fi # Base/Pro: Qwen3 embedder say " [2/3] Base/Pro embedder (Qwen3-Embedding-0.6B, ~1.2GB)..." - "$TOOL_PYTHON" -c " -from sentence_transformers import SentenceTransformer -SentenceTransformer('Qwen/Qwen3-Embedding-0.6B', truncate_dim=256) -" && ok " [2/3] Base/Pro embedder ready" || warn " [2/3] Base/Pro embedder download failed (you can retry later or use Edge tier)" + if "$TOOL_PYTHON" -c "$BASE_EMBEDDER_CODE"; then + BASE_EMBEDDER_STATUS="ready" + MODEL_READY_COUNT=$((MODEL_READY_COUNT + 1)) + ok " [2/3] Base/Pro embedder ready" + else + BASE_EMBEDDER_STATUS="failed" + warn " [2/3] Base/Pro embedder pre-download failed" + warn "Retry this model only: \"\$(uv tool dir)/truememory/bin/python\" -c \"$BASE_EMBEDDER_CODE\"" + fi # Base/Pro: gte-reranker say " [3/3] Base/Pro reranker (gte-modernbert, ~600MB)..." - "$TOOL_PYTHON" -c " -from sentence_transformers import CrossEncoder -CrossEncoder('Alibaba-NLP/gte-reranker-modernbert-base') -" && ok " [3/3] Base/Pro reranker ready" || warn " [3/3] Base/Pro reranker download failed (you can retry later or use Edge tier)" - - ok "all models pre-downloaded — tier switching is instant." + if "$TOOL_PYTHON" -c "$BASE_RERANKER_CODE"; then + BASE_RERANKER_STATUS="ready" + MODEL_READY_COUNT=$((MODEL_READY_COUNT + 1)) + ok " [3/3] Base/Pro reranker ready" + else + BASE_RERANKER_STATUS="failed" + warn " [3/3] Base/Pro reranker pre-download failed" + warn "Retry this model only: \"\$(uv tool dir)/truememory/bin/python\" -c \"$BASE_RERANKER_CODE\"" + fi else warn "could not locate tool Python at $TOOL_PYTHON — skipping model pre-download" - warn "models will download on first use instead" + warn "Model readiness is unverified. Locate the tool environment with: uv tool dir" + warn "After locating its Python, run: python -m truememory.ingest.cli setup" fi # ---------- done ---------- @@ -175,7 +205,21 @@ BANNER printf '\n' # Show installed version INSTALLED_VER=$("$TOOL_PYTHON" -c "from importlib.metadata import version; print(version('truememory'))" 2>/dev/null || echo "unknown") - ok "TrueMemory v${INSTALLED_VER} installed successfully." + ok "TrueMemory v${INSTALLED_VER} package installed." + say "Claude registration: $SETUP_STATUS" + say "Hooks: $HOOK_STATUS" + say "Model pre-download checks: $MODEL_READY_COUNT/3 succeeded." + say " Edge reranker: $EDGE_RERANKER_STATUS" + say " Base/Pro embedder: $BASE_EMBEDDER_STATUS" + say " Base/Pro reranker: $BASE_RERANKER_STATUS" + if [ "$MODEL_READY_COUNT" -eq 3 ]; then + ok "All three requested model pre-download checks passed." + else + warn "Model readiness incomplete; package installation is retained. Retry the failed or skipped checks before relying on those models." + fi + say "The Edge embedder was not checked by this pre-download step." + say "Registration and hook command success do not verify runtime operation." + say "Changing embedding models can still require re-embedding stored memories." printf '\n' printf ' %bFirst time?%b Start a new Claude session and type:\n' "$GREEN" "$RESET" printf '\n' diff --git a/pyproject.toml b/pyproject.toml index b859c018..f9be3037 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,10 +29,14 @@ dependencies = [ "model2vec>=0.6.0,<1.0", "sqlite-vec>=0.1.0,<1.0", "numpy>=1.24.0,<3.0", - "mcp[cli]>=1.0.0,<3.0", + # 1.21.1 removes a private Pydantic import broken by Pydantic 2.14. + # SDK 2 removes the FastMCP import path. + "mcp[cli]>=1.21.1,<2.0", "httpx>=0.27.0,<1.0", "sentence-transformers>=3.0.0,<7.0", "torch>=2.0.0,<3.0", + # Apple Silicon MPS budgeting needs recommended_max_memory(), added in 2.5. + "torch>=2.5.0,<3.0; sys_platform == 'darwin' and platform_machine == 'arm64'", "anthropic>=0.80.0,<2.0", "psutil>=5.9,<8.0", "setproctitle>=1.3,<2.0", @@ -70,6 +74,8 @@ Changelog = "https://github.com/buildingjoshbetter/TrueMemory/blob/main/CHANGELO [tool.pytest.ini_options] testpaths = ["tests"] +# Retain pytest's defaults while discovering kebab-case regression scripts. +python_files = ["test_*.py", "*_test.py", "test-*.py"] # Issue #426: register custom markers and fail on typos so the gate can # reliably exclude network/model-download tests via `-m "not network"`. addopts = "--strict-markers --timeout=120" @@ -78,7 +84,8 @@ markers = [ ] [tool.ruff.lint] -# Default ruleset is fine for the core codebase. Per-file ignores below. +# Preserve the project's established checks across Ruff's default-rule changes. +select = ["E4", "E7", "E9", "F"] [tool.ruff.lint.per-file-ignores] # __init__.py files re-export submodule symbols as the public API. Ruff diff --git a/tests/conftest.py b/tests/conftest.py index 069fb5ad..cc83c806 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -100,9 +100,14 @@ def _isolate_truememory_home(tmp_path_factory): ("PORT_PATH", "model_server.port"), ("TOKEN_PATH", "model_server.token"), ("LOCK_PATH", "model_server.lock"), + ("START_LOCK_PATH", "model_server.start.lock"), + ("START_STATE_PATH", "model_server.start.json"), ): if hasattr(mod, attr): setattr(mod, attr, tm_dir / name) + if mod_name == "truememory.model_client": + mod._APP_BUNDLE_PATH = tm_dir / "TrueMemory.app" + mod._APP_EXECUTABLE = mod._APP_BUNDLE_PATH / "Contents" / "MacOS" / "TrueMemory" yield @@ -124,30 +129,43 @@ def _isolate_environ(): @pytest.fixture(autouse=True) def _isolate_vector_search_globals(): - """Snapshot/restore ``truememory.vector_search`` embedder globals. + """Snapshot/restore the complete process-local serving identity. These are process-global (module-level) and several tests mutate them via ``set_embedding_model`` or ``monkeypatch.setattr``. monkeypatch reverts the - ones it owns, but a defensive snapshot here keeps the embedder identity - deterministic regardless of test order or ambient config.json. + ones it owns, but policy projection also changes embedding/reranker identity + and runtime acknowledgement. Restore those fields together without resetting + serving leases, runtime locals or background workers. """ try: from truememory import vector_search as _vs + from truememory import reranker as _rr + from truememory.tier_switch import runtime as _runtime except Exception: # vector_search may be unimportable in minimal-dep environments; nothing # to isolate in that case. yield return - saved = ( + saved_vector = ( getattr(_vs, "EMBEDDING_MODEL", None), getattr(_vs, "_embedding_dim", None), getattr(_vs, "_model", None), + getattr(_vs, "_model_generation", 0), + getattr(_vs, "_frozen_embedding_target", None), + getattr(_vs, "_runtime_policy_tier", None), ) + saved_reranker = (_rr._model, _rr._model_name, _rr._model_certified, + _rr._active_tier, _rr._frozen_reranker_id) + saved_acknowledgement = _runtime._acknowledged try: yield finally: - _vs.EMBEDDING_MODEL, _vs._embedding_dim, _vs._model = saved + (_vs.EMBEDDING_MODEL, _vs._embedding_dim, _vs._model, _vs._model_generation, + _vs._frozen_embedding_target, _vs._runtime_policy_tier) = saved_vector + (_rr._model, _rr._model_name, _rr._model_certified, + _rr._active_tier, _rr._frozen_reranker_id) = saved_reranker + _runtime._acknowledged = saved_acknowledgement @pytest.fixture(autouse=True) diff --git a/tests/ingest/test_issue_558_async_stale_scan.py b/tests/ingest/test_issue_558_async_stale_scan.py index 019dd859..dbcda497 100644 --- a/tests/ingest/test_issue_558_async_stale_scan.py +++ b/tests/ingest/test_issue_558_async_stale_scan.py @@ -70,7 +70,7 @@ def test_main_calls_maintenance_not_scan(self, monkeypatch): _scan_stale_sessions (synchronous).""" calls = {"maintenance": 0, "scan": 0} - monkeypatch.setattr(ss, "_run_maintenance_background", lambda: calls.__setitem__("maintenance", calls["maintenance"] + 1)) + monkeypatch.setattr(ss, "_run_maintenance_background", lambda host="claude": calls.__setitem__("maintenance", calls["maintenance"] + 1)) monkeypatch.setattr(ss, "_scan_stale_sessions", lambda: calls.__setitem__("scan", calls["scan"] + 1)) monkeypatch.setattr(ss, "_drain_backlog", lambda: None) monkeypatch.setattr(ss, "_is_first_run", lambda: True) diff --git a/tests/ingest/test_stop_hook_safety.py b/tests/ingest/test_stop_hook_safety.py index de2ed8e5..62e148ce 100644 --- a/tests/ingest/test_stop_hook_safety.py +++ b/tests/ingest/test_stop_hook_safety.py @@ -14,6 +14,12 @@ import json import subprocess from pathlib import Path +import pytest + + +@pytest.fixture(autouse=True) +def _allow_synthetic_transcripts(monkeypatch, tmp_path): + monkeypatch.setenv("TRUEMEMORY_TRANSCRIPT_DIR", str(tmp_path)) # --------------------------------------------------------------------------- @@ -46,7 +52,7 @@ def _record_popen(*args, **kwargs): monkeypatch.setattr(subprocess, "Popen", _record_popen) stop_mod._run_background_ingestion( - transcript_path="/fake/transcript.json", + transcript_path=str(tmp_path / "transcript.json"), session_id="sess-at-cap", user_id="alice", db_path="/tmp/fake.db", @@ -93,7 +99,7 @@ def _record_popen(*args, **kwargs): monkeypatch.setattr(subprocess, "Popen", _record_popen) stop_mod._run_background_ingestion( - transcript_path="/fake/transcript.json", + transcript_path=str(tmp_path / "transcript.json"), session_id="sess-ok", user_id="alice", db_path="/tmp/fake.db", @@ -174,7 +180,7 @@ def _spy_inline(*args, **kwargs): # Must not raise, must not call inline ingest, must queue to backlog. stop_mod._run_background_ingestion( - transcript_path="/fake/transcript.json", + transcript_path=str(tmp_path / "transcript.json"), session_id="sess-popen-fail", user_id="alice", db_path="/tmp/fake.db", @@ -207,7 +213,7 @@ def test_backlog_write_failure_is_swallowed(monkeypatch, tmp_path): # Should not raise stop_mod._queue_to_backlog( - transcript_path="/fake/transcript.json", + transcript_path=str(tmp_path / "transcript.json"), session_id="sess-backlog-fail", user_id="alice", db_path="/tmp/fake.db", @@ -222,7 +228,7 @@ def test_backlog_marker_schema(monkeypatch, tmp_path): monkeypatch.setattr(stop_mod, "BACKLOG_DIR", tmp_path / "backlog") stop_mod._queue_to_backlog( - transcript_path="/conv/transcript.json", + transcript_path=str(tmp_path / "transcript.json"), session_id="sess-abc", user_id="alice", db_path="/tmp/alice.db", @@ -250,7 +256,7 @@ def test_empty_session_id_still_queues(monkeypatch, tmp_path): monkeypatch.setattr(stop_mod, "BACKLOG_DIR", tmp_path / "backlog") stop_mod._queue_to_backlog( - transcript_path="/conv/transcript.json", + transcript_path=str(tmp_path / "transcript.json"), session_id="", user_id="", db_path="", diff --git a/tests/mcp-wheel-smoke.py b/tests/mcp-wheel-smoke.py new file mode 100644 index 00000000..6a0f2522 --- /dev/null +++ b/tests/mcp-wheel-smoke.py @@ -0,0 +1,226 @@ +"""Check an installed wheel's CLI and MCP wire contract without starting models. + +Run with the wheel environment's Python: python -I tests/mcp-wheel-smoke.py +This standalone probe deliberately avoids the source checkout and pytest fixtures. +""" +from __future__ import annotations + +import asyncio +import importlib.metadata +import importlib.util +import json +import os +import subprocess +import sys +import sysconfig +import tempfile +from contextlib import suppress +from pathlib import Path + +_TOOLS = { + "truememory_store", "truememory_directives", "truememory_search", + "truememory_search_deep", "truememory_get", "truememory_forget", + "truememory_stats", "truememory_configure", "truememory_status", + "truememory_entity_profile", "truememory_consolidate", +} +_ALWAYS_LOADED = { + "truememory_store", "truememory_directives", "truememory_search", + "truememory_search_deep", "truememory_forget", "truememory_stats", +} +_TIMEOUT = 30 +_PROTOCOL_VERSION = "2025-06-18" + +# main() starts model services and maintenance. The registered server object +# exposes the real MCP transport and schemas without those inference services. +_SERVER = """ +import importlib.util +import sys +from pathlib import Path + +origin = importlib.util.find_spec("truememory").origin +if Path(origin).resolve() != Path(sys.argv[1]).resolve(): + raise RuntimeError("Installed wheel import provenance mismatch") +from truememory.mcp_server import mcp +mcp.run(transport="stdio") +""" + + +class SmokeFailure(Exception): + """A safe diagnostic that contains no subprocess payload or private path.""" + + +def _require(condition: bool, message: str) -> None: + if not condition: + raise SmokeFailure(message) + + +def _object(value: object, label: str) -> dict: + _require(isinstance(value, dict), f"Expected a JSON object for {label}") + return value + + +def _environment(root: Path) -> dict[str, str]: + home = root / "home" + cache = root / "cache" + temp = root / "tmp" + for directory in (home, cache, temp): + directory.mkdir() + # Build an allowlist instead of copying credentials or personal settings. + env = { + "PATH": os.pathsep.join((str(Path(sys.executable).parent), os.defpath)), + "HOME": str(home), "USERPROFILE": str(home), + "APPDATA": str(home), "LOCALAPPDATA": str(home), + "XDG_CONFIG_HOME": str(home), "XDG_CACHE_HOME": str(cache), + "HF_HOME": str(cache), "TORCH_HOME": str(cache), + "TMPDIR": str(temp), "TMP": str(temp), "TEMP": str(temp), + "USER": "synthetic", "LOGNAME": "synthetic", "USERNAME": "synthetic", + "TRUEMEMORY_TELEMETRY": "off", + "HF_HUB_OFFLINE": "1", "TRANSFORMERS_OFFLINE": "1", + "HF_HUB_DISABLE_TELEMETRY": "1", + "OMP_NUM_THREADS": "1", "MKL_NUM_THREADS": "1", + "OPENBLAS_NUM_THREADS": "1", "NUMEXPR_MAX_THREADS": "1", + } + if os.name == "nt": + for key in ("SYSTEMROOT", "WINDIR", "COMSPEC"): + if key in os.environ: + env[key] = os.environ[key] + return env + + +def _check_cli(root: Path, env: dict[str, str], version: str) -> None: + suffix = ".exe" if os.name == "nt" else "" + executable = Path(sysconfig.get_path("scripts")) / f"truememory-mcp{suffix}" + _require(executable.is_file(), "Installed console script is missing") + for flag, expected in (("--help", "Usage: truememory-mcp"), ("--version", version)): + # The executable belongs to the venv and flags are fixed, never shell input. + result = subprocess.run( + [str(executable), flag], cwd=root, env=env, + capture_output=True, text=True, timeout=_TIMEOUT, check=False, + ) + _require(result.returncode == 0, f"Console {flag} failed (exit {result.returncode})") + _require(expected in result.stdout, f"Console {flag} output is incorrect") + + +async def _send(process: asyncio.subprocess.Process, message: dict) -> None: + _require(process.stdin is not None, "MCP stdin is unavailable") + process.stdin.write((json.dumps(message) + "\n").encode()) + await process.stdin.drain() + + +async def _response(process: asyncio.subprocess.Process, request_id: int) -> dict: + _require(process.stdout is not None, "MCP stdout is unavailable") + while True: + line = await process.stdout.readline() + _require(bool(line), "MCP server exited before its response") + message = _object(json.loads(line), "MCP response") + _require(message.get("jsonrpc") == "2.0", "Invalid JSON-RPC version") + if "id" not in message: + _require("method" in message, "Invalid MCP notification") + continue + _require(message["id"] == request_id, "Unexpected MCP response ID") + _require("error" not in message, "MCP request returned an error") + return _object(message.get("result"), "MCP result") + + +async def _exchange(process: asyncio.subprocess.Process) -> None: + await _send(process, { + "jsonrpc": "2.0", "id": 1, "method": "initialize", + "params": { + "protocolVersion": _PROTOCOL_VERSION, "capabilities": {}, + "clientInfo": {"name": "synthetic-wheel-smoke", "version": "1"}, + }, + }) + initialized = await _response(process, 1) + _require(initialized.get("protocolVersion") == _PROTOCOL_VERSION, + "MCP protocol negotiation failed") + server = _object(initialized.get("serverInfo"), "server identity") + _require(server.get("name") == "truememory", "Unexpected MCP server identity") + capabilities = _object(initialized.get("capabilities"), "server capabilities") + _require("tools" in capabilities, "MCP tools capability is missing") + await _send(process, {"jsonrpc": "2.0", "method": "notifications/initialized"}) + await _send(process, {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}) + listed = await _response(process, 2) + entries = listed.get("tools") + _require(isinstance(entries, list) and len(entries) == len(_TOOLS), + "Expected all 11 registered tools") + tools = {} + for entry in entries: + tool = _object(entry, "tool") + _require(isinstance(tool.get("name"), str), "Tool name is missing") + tools[tool["name"]] = tool + _require(set(tools) == _TOOLS, "Registered tool names differ from the contract") + for name in _ALWAYS_LOADED: + metadata = _object(tools[name].get("_meta"), "tool wire metadata") + _require(metadata.get("anthropic/alwaysLoad") is True, + "alwaysLoad tool metadata was lost on the wire") + schema = _object(tools["truememory_store"].get("inputSchema"), "store schema") + properties = _object(schema.get("properties"), "store properties") + directive = _object(properties.get("directive"), "directive schema") + _require(directive.get("type") == "boolean" and directive.get("default") is False, + "Directive input schema changed") + _require("content" in schema.get("required", []), "Store content must be required") + + +async def _check_protocol(root: Path, env: dict[str, str], origin: Path) -> None: + process = await asyncio.create_subprocess_exec( + sys.executable, "-I", "-c", _SERVER, str(origin), cwd=root, env=env, + stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.DEVNULL, limit=1024 * 1024, + ) + try: + await asyncio.wait_for(_exchange(process), timeout=_TIMEOUT) + process.stdin.close() + await asyncio.wait_for(process.wait(), timeout=5) + _require(process.returncode == 0, "MCP server did not exit cleanly") + finally: + if process.returncode is None: + with suppress(ProcessLookupError): + process.kill() + await asyncio.wait_for(process.wait(), timeout=5) + + +def main() -> None: + _require(bool(sys.flags.isolated), "Run this probe with python -I") + distribution = importlib.metadata.distribution("truememory") + # Report public package versions before subprocess checks, whose output is + # intentionally redacted. Dependency import failures then remain traceable. + print(json.dumps({ + "stage": "dependency_versions", + "python_version": ".".join(str(part) for part in sys.version_info[:3]), + "truememory_version": distribution.version, + "mcp_version": importlib.metadata.version("mcp"), + "pydantic_version": importlib.metadata.version("pydantic"), + "pydantic_core_version": importlib.metadata.version("pydantic-core"), + }), flush=True) + origin = Path(distribution.locate_file("truememory/__init__.py")).resolve() + spec = importlib.util.find_spec("truememory") + _require(origin.is_file() and spec is not None and spec.origin is not None, + "Installed wheel package is missing") + _require(Path(spec.origin).resolve() == origin, + "Probe imported a source checkout instead of the installed wheel") + direct_url = json.loads(distribution.read_text("direct_url.json") or "{}") + _require(not direct_url.get("dir_info", {}).get("editable", False), + "An editable installation is not a wheel smoke test") + with tempfile.TemporaryDirectory(prefix="synthetic-mcp-wheel-") as directory: + root = Path(directory) + env = _environment(root) + _check_cli(root, env, distribution.version) + asyncio.run(_check_protocol(root, env, origin)) + print(json.dumps({ + "status": "passed", "truememory_version": distribution.version, + "mcp_version": importlib.metadata.version("mcp"), + "tools": len(_TOOLS), "always_load_tools": len(_ALWAYS_LOADED), + })) + + +if __name__ == "__main__": + try: + main() + except SmokeFailure as error: + print(f"MCP wheel smoke failed: {error}", file=sys.stderr) + sys.exit(1) + except Exception as error: + # This final boundary must redact unexpected errors as well as known ones. + # Do not echo child output, credentials, or machine-specific paths. + print(f"MCP wheel smoke failed: {type(error).__name__}", file=sys.stderr) + sys.exit(1) diff --git a/tests/test-claude-stop-fallback-722.py b/tests/test-claude-stop-fallback-722.py new file mode 100644 index 00000000..575d65a7 --- /dev/null +++ b/tests/test-claude-stop-fallback-722.py @@ -0,0 +1,132 @@ +"""Synthetic controls for supported Claude Stop fallback; no native host claim.""" +from __future__ import annotations + +import ast +import io +import json +import os +import shlex +import sys +import tempfile +import types +import unittest +from pathlib import Path +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] + + +def adapter_source() -> dict: + source = ROOT / "truememory/hooks/adapters/claude.py" + tree = ast.parse(source.read_text()) + nodes = [node for node in tree.body if isinstance(node, (ast.FunctionDef, ast.ClassDef)) + or isinstance(node, ast.Assign) and any(isinstance(target, ast.Name) and target.id == "_HOOK_MODULES" for target in node.targets)] + namespace = {"__file__": str(source), "json": json, "os": os, "sys": sys, "Path": Path, "CLIAdapter": object} + exec(compile(ast.fix_missing_locations(ast.Module(body=[ast.ImportFrom(module="__future__", names=[ast.alias(name="annotations")], level=0), *nodes], type_ignores=[])), str(source), "exec"), namespace) + return namespace + + +ADAPTER = adapter_source() + + +class InertStopFallbackControls(unittest.TestCase): + def test_default_sessionend_and_explicit_opt_in(self) -> None: + build = ADAPTER["claude_hook_settings"] + with patch.dict(os.environ, {}, clear=True): + default = build("/synthetic/python3", "synthetic-account", "/synthetic/db") + self.assertIn("SessionEnd", default["hooks"]) + self.assertNotIn("Stop", default["hooks"]) + for value in ("0", "true", "yes", "garbage"): + with patch.dict(os.environ, {"TRUEMEMORY_CLAUDE_STOP_FALLBACK": value}, clear=True): + self.assertNotIn("Stop", build("python3")["hooks"]) + with patch.dict(os.environ, {"TRUEMEMORY_CLAUDE_STOP_FALLBACK": "1"}, clear=True): + fallback = build("python3", "synthetic-account", "/synthetic/db") + self.assertIn("SessionEnd", fallback["hooks"]) + self.assertIn("Stop", fallback["hooks"]) + for event, groups in fallback["hooks"].items(): + argv = shlex.split(groups[0]["hooks"][0]["command"]) + self.assertEqual(argv[argv.index("--host") + 1], "claude") + self.assertEqual(argv[argv.index("--event") + 1], event) + self.assertEqual(argv[argv.index("--user") + 1], "synthetic-account") + self.assertEqual(argv[argv.index("--db") + 1], "/synthetic/db") + + def test_exact_module_and_script_ownership(self) -> None: + owned = ADAPTER["_owned_hook_module"] + directory = ROOT / "truememory/ingest/hooks" + module = "truememory.ingest.hooks.stop" + self.assertEqual(owned("python3 -m " + module, directory, "python3"), module) + self.assertEqual(owned("python3 " + shlex.quote(str(directory / "stop.py")), directory, "python3"), module) + for command in ("echo truememory", "echo -m " + module, + "python3 -m " + module + ".foreign", "python3 -m " + module + " && echo x", + "python3 /foreign/truememory/stop.py", "python3 -m foreign --name truememory"): + self.assertIsNone(owned(command, directory, "python3")) + + def test_recursion_guards_do_not_read_source_or_mark_completion(self) -> None: + source = ROOT / "truememory/ingest/hooks/stop.py" + node = next(node for node in ast.parse(source.read_text()).body if isinstance(node, ast.FunctionDef) and node.name == "main") + calls = [] + namespace = {"_parse_args": lambda: types.SimpleNamespace(host="claude", user="", db=""), + "os": os, "sys": sys, "json": json, "Path": lambda *args: calls.append("source")} + exec(compile(ast.fix_missing_locations(ast.Module(body=[node], type_ignores=[])), str(source), "exec"), namespace) + with patch.dict(os.environ, {"TRUEMEMORY_EXTRACTION": "1"}, clear=True), \ + patch.object(sys, "stdin", io.StringIO("not JSON")): + namespace["main"]() + self.assertEqual(calls, []) + with patch.dict(os.environ, {}, clear=True), \ + patch.object(sys, "stdin", io.StringIO(json.dumps({"stop_hook_active": True, "transcript_path": "/synthetic/source"}))): + namespace["main"]() + self.assertEqual(calls, []) + + def test_windows_builder_preserves_fixed_identity_and_quotes(self) -> None: + with patch.object(sys, "platform", "win32"), patch.dict(os.environ, {}, clear=True): + settings = ADAPTER["claude_hook_settings"]("C:/Synthetic Python/python.exe", "synthetic account", "C:/Synthetic DB/db") + command = settings["hooks"]["SessionEnd"][0]["hooks"][0]["command"] + self.assertIn('"C:/Synthetic Python/python.exe"', command) + self.assertIn("--host claude --event SessionEnd", command) + self.assertIn('"synthetic account"', command) + with patch.object(sys, "platform", "win32"): + owned = ADAPTER["_owned_hook_module"](command, ROOT / "truememory/ingest/hooks", "C:/Synthetic Python/python.exe") + self.assertEqual(owned, "truememory.ingest.hooks.stop") + + +class FilesystemStopFallbackControls(unittest.TestCase): + def test_install_preserves_foreign_and_viable_stop_and_is_idempotent(self) -> None: + with tempfile.TemporaryDirectory(prefix="tm-synthetic-stop-") as temporary: + target = Path(temporary) / "settings.json" + foreign = {"type": "command", "command": "echo truememory foreign", "timeout": 8} + viable = {"type": "command", "command": "python3 -m truememory.ingest.hooks.stop", "timeout": 17} + target.write_text(json.dumps({"other_setting": "retained", "hooks": {"Stop": [foreign, viable]}})) + adapter_type = ADAPTER["ClaudeAdapter"] + with patch.object(adapter_type, "config_path", new=property(lambda self: target)), \ + patch.dict(os.environ, {}, clear=True): + adapter = adapter_type() + adapter.install_hooks("python3", "synthetic-account", "/synthetic/db") + first = json.loads(target.read_text()) + adapter.install_hooks("python3", "synthetic-account", "/synthetic/db") + second = json.loads(target.read_text()) + self.assertEqual(first, second) + self.assertEqual(second["other_setting"], "retained") + commands = [hook for group in second["hooks"]["Stop"] for hook in group["hooks"]] + self.assertIn(foreign, commands) + owned = [hook for hook in commands if "-m truememory.ingest.hooks.stop" in hook["command"]] + self.assertEqual(len(owned), 1) + self.assertEqual(owned[0]["timeout"], 17) + self.assertIn("--host claude --event Stop", owned[0]["command"]) + self.assertIn("SessionEnd", second["hooks"]) + + def test_foreign_substring_does_not_suppress_owned_event(self) -> None: + with tempfile.TemporaryDirectory(prefix="tm-synthetic-foreign-") as temporary: + target = Path(temporary) / "settings.json" + foreign = {"matcher": "", "hooks": [{"type": "command", "command": "echo truememory"}]} + target.write_text(json.dumps({"hooks": {"SessionEnd": [foreign], "Stop": [foreign]}})) + with patch.object(ADAPTER["ClaudeAdapter"], "config_path", new=property(lambda self: target)), \ + patch.dict(os.environ, {}, clear=True): + ADAPTER["ClaudeAdapter"]().install_hooks("python3") + settings = json.loads(target.read_text()) + self.assertEqual(settings["hooks"]["Stop"], [foreign]) + self.assertIn(foreign, settings["hooks"]["SessionEnd"]) + self.assertEqual(len(settings["hooks"]["SessionEnd"]), 2) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-cluster-outer-publication-753.py b/tests/test-cluster-outer-publication-753.py new file mode 100644 index 00000000..a27ef0d0 --- /dev/null +++ b/tests/test-cluster-outer-publication-753.py @@ -0,0 +1,395 @@ +"""Actual SQLite terminal writes with explicit synthetic clustering boundaries.""" + +import contextlib +import runpy +import sqlite3 +import struct +import tempfile +import threading +import unittest +from pathlib import Path +from unittest.mock import patch + + +TESTS = Path(__file__).resolve().parent +PRIMITIVES = runpy.run_path(str(TESTS / "test-maintenance-source-revision-753.py")) +STORAGE, MAINTENANCE = PRIMITIVES["STORAGE"], PRIMITIVES["MAINTENANCE"] +CLUSTERS = runpy.run_path(str(TESTS / "test-cluster-publication-749.py")) + + +class OuterClusterFixture(unittest.TestCase): + def setUp(self) -> None: + self.module, self.vector, self.boundary = CLUSTERS["load_clustering"]() + directory = tempfile.TemporaryDirectory(prefix="synthetic-outer-clusters-") + self.addCleanup(directory.cleanup) + self.path = Path(directory.name) / "synthetic.sqlite" + self.conn = self.open_db() + self.conn.execute("CREATE TABLE vec_messages_edge(embedding BLOB)") + for mid, values in enumerate(((1, 0), (0, 1), (0, 0), (2, 2), (4, 0)), 1): + self.conn.execute( + "INSERT INTO messages(id,content,sender,recipient,timestamp,category,modality) " + "VALUES (?,'synthetic source','synthetic-sender','synthetic-recipient','2026-01-01','synthetic-session','text')", (mid,), + ) + self.conn.execute("INSERT INTO vec_messages_edge(rowid,embedding) VALUES (?,?)", (mid, struct.pack("2f", *values))) + self.conn.execute("INSERT INTO vector_cache_registry VALUES ('edge','vec_messages_edge','vec_messages_sep_edge',5,5,'synthetic-model-a',2,1.0,1.0)") + self.conn.executemany("INSERT INTO metadata(key,value,updated_at) VALUES (?,?,'synthetic-stamp')", [ + ("embed_model", "synthetic-model-a"), ("embed_dim", "2"), + ("vec_source_v1:vec_messages_edge", '{"generation":"synthetic-v1","cursor":5,"complete":true}'), + ]) + self.conn.execute("INSERT INTO message_clusters(message_id,cluster_id,noise) VALUES (1,7,0)") + self.conn.execute("INSERT INTO cluster_centroids(cluster_id,centroid,message_count,session_range) VALUES (7,?,1,'synthetic-prior')", (struct.pack("2f", 9, 9),)) + self.conn.commit() + self.dependency = MAINTENANCE.make_layer_dependency(1, {"algorithm": "synthetic-hdbscan"}) + + def open_db(self) -> sqlite3.Connection: + conn = STORAGE.create_db(self.path) + conn.execute("PRAGMA busy_timeout=100") + self.addCleanup(conn.close) + return conn + + def output(self, conn: sqlite3.Connection | None = None) -> tuple: + conn = conn or self.conn + return (conn.execute("SELECT * FROM message_clusters ORDER BY message_id").fetchall(), + conn.execute("SELECT * FROM cluster_centroids ORDER BY cluster_id").fetchall()) + + def success(self, conn: sqlite3.Connection | None = None) -> tuple: + return (conn or self.conn).execute( + "SELECT successful_epoch,successful_revision,successful_dependency,output_count " + "FROM maintenance_layers WHERE layer='clusters'" + ).fetchone() + + def spec(self, after_build=None, count_action=None, resolver=None, factory=None): + def build(conn): + self.module.cluster_messages(conn) + if after_build is not None: + after_build() + + def count(conn): + if count_action is not None: + count_action() + return conn.execute("SELECT count(*) FROM cluster_centroids").fetchone()[0] + + return MAINTENANCE.LayerSpec( + "clusters", "cluster_messages", resolver or (lambda: self.dependency), build, count, + publication_guard=factory or self.module.cluster_publication_guard, + ) + + def run_layer(self, spec=None, **kwargs): + return MAINTENANCE.run_layers(self.conn, (spec or self.spec(),), force=True, **kwargs)[0] + + +class TestClusterOuterOwnership(OuterClusterFixture): + def test_actual_commit_holds_model_lock_and_publishes_checkpoint_with_output(self) -> None: + reader = self.open_db() + previous = self.output(reader), self.success(reader) + observed = [] + + def trace(sql): + if sql == "COMMIT": + observed.append((self.vector._lock.locked(), self.output(reader), self.success(reader))) + + result = self.run_layer(self.spec(count_action=lambda: self.conn.set_trace_callback(trace))) + self.assertEqual(result.outcome, "success") + self.assertEqual(result.output_count, 2) + self.assertEqual(observed, [(True, *previous)]) + self.assertFalse(self.vector._lock.locked()) + self.assertNotEqual((self.output(reader), self.success(reader)), previous) + self.assertEqual(self.success(reader)[3], 2) + + def test_guard_factory_captures_before_attempt_snapshot_and_resolvers_run_outside_lock(self) -> None: + captures = [] + resolutions = [] + original = self.module.cluster_publication_guard + + def factory(conn): + captures.append(conn.in_transaction) + self.assertEqual(conn.execute("SELECT outcome FROM maintenance_layers WHERE layer='clusters'").fetchone(), ("running",)) + return original(conn) + + def resolver(): + available = self.vector._lock.acquire(blocking=False) + resolutions.append(available) + if available: + self.vector._lock.release() + return self.dependency + + self.assertEqual(self.run_layer(self.spec(factory=factory, resolver=resolver)).outcome, "success") + self.assertEqual(captures, [False]) + self.assertGreaterEqual(len(resolutions), 5) + self.assertTrue(all(resolutions)) + + def test_model_changes_after_inner_release_roll_back_output_and_checkpoint(self) -> None: + old = self.output(), self.success() + rollbacks = [] + + def change_model(): + self.assertTrue(self.conn.in_transaction) + self.assertFalse(self.vector._lock.locked()) + with self.vector._lock: + self.vector.EMBEDDING_MODEL = "synthetic-model-a-replacement" + self.conn.set_trace_callback(lambda sql: rollbacks.append(self.vector._lock.locked()) if sql == "ROLLBACK" else None) + + result = self.run_layer(self.spec(after_build=change_model)) + self.assertEqual(result.outcome, "failed") + self.assertEqual((self.output(), self.success()), old) + self.assertEqual(rollbacks, [True]) + self.assertFalse(self.vector._lock.locked()) + + def test_dimension_tier_table_registry_and_manifest_changes_are_fenced(self) -> None: + changes = { + "dimension": lambda: setattr(self.vector, "_embedding_dim", 3), + "tier": lambda: setattr(self.vector, "_active_tier_group", lambda: "synthetic-new-tier"), + "table": lambda: self.conn.execute("UPDATE vector_cache_registry SET vec_table='vec_messages_replaced'"), + "registry": lambda: self.conn.execute("UPDATE vector_cache_registry SET last_updated=2.0"), + "generation": lambda: self.conn.execute("UPDATE metadata SET value=? WHERE key='vec_source_v1:vec_messages_edge'", ('{"generation":"synthetic-v2","cursor":5,"complete":true}',)), + "progress": lambda: self.conn.execute("UPDATE metadata SET value=? WHERE key='vec_source_v1:vec_messages_edge'", ('{"generation":"synthetic-v1","cursor":4,"complete":false}',)), + "schema": lambda: self.conn.execute("ALTER TABLE vec_messages_edge ADD COLUMN synthetic INTEGER"), + } + original_group = self.vector._active_tier_group + for kind, mutate in changes.items(): + with self.subTest(kind=kind): + old = self.output(), self.success() + result = self.run_layer(self.spec(after_build=mutate)) + self.assertEqual(result.outcome, "failed") + self.assertEqual((self.output(), self.success()), old) + self.assertFalse(self.vector._lock.locked()) + self.vector._embedding_dim = 2 + self.vector._active_tier_group = original_group + + def test_source_mutation_after_builder_is_rejected_before_checkpoint(self) -> None: + old = self.output(), self.success() + result = self.run_layer(self.spec(after_build=lambda: self.conn.execute( + "UPDATE messages SET content='synthetic replacement' WHERE id=1" + ))) + self.assertEqual(result.outcome, "failed") + self.assertEqual((self.output(), self.success()), old) + + def test_busy_final_model_lock_does_not_wait_or_release_other_owner(self) -> None: + old = self.output(), self.success() + try: + result = self.run_layer(self.spec(after_build=self.vector._lock.acquire)) + self.assertEqual(result.outcome, "failed") + self.assertEqual((self.output(), self.success()), old) + self.assertTrue(self.vector._lock.locked()) + finally: + if self.vector._lock.locked(): + self.vector._lock.release() + + def test_model_changed_after_capture_before_builder_cannot_relabel_output(self) -> None: + old = self.output(), self.success() + original = self.module.cluster_publication_guard + + def factory(conn): + guard = original(conn) + self.vector.EMBEDDING_MODEL = "synthetic-model-a-replacement" + return guard + + result = self.run_layer(self.spec(factory=factory)) + self.assertEqual(result.outcome, "failed") + self.assertEqual((self.output(), self.success()), old) + + def test_guard_does_not_add_another_full_raw_source_scan(self) -> None: + original = self.module._cluster_source_state + calls = [] + + def source(*args, **kwargs): + calls.append(True) + return original(*args, **kwargs) + + with patch.object(self.module, "_cluster_source_state", source): + self.assertEqual(self.run_layer().outcome, "success") + self.assertEqual(len(calls), 2) + + def test_concurrent_writer_can_commit_while_initial_capture_waits_for_model(self) -> None: + writer = self.open_db() + started = threading.Event() + results = [] + self.vector._lock.acquire() + + def capture(): + started.set() + try: + results.append(self.module.cluster_publication_guard(self.conn)) + except Exception as error: + results.append(error) + + worker = threading.Thread(target=capture) + worker.start() + try: + self.assertTrue(started.wait(1)) + self.assertFalse(self.conn.in_transaction) + writer.execute("INSERT INTO metadata(key,value) VALUES ('synthetic-writer','committed')") + writer.commit() + finally: + self.vector._lock.release() + worker.join(2) + self.assertFalse(worker.is_alive()) + self.assertEqual(len(results), 1) + self.assertNotIsInstance(results[0], Exception) + self.assertFalse(self.vector._lock.locked()) + + +class TestClusterOuterFailure(OuterClusterFixture): + def test_actual_commit_failure_rolls_back_while_lock_held_then_releases(self) -> None: + old = self.output(), self.success() + terminal = [] + denied = [] + + def authorizer(action, operation, *unused): + if action == sqlite3.SQLITE_TRANSACTION and operation == "COMMIT" and not denied: + denied.append(self.vector._lock.locked()) + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + + def observe(sql): + if sql == "ROLLBACK": + terminal.append(self.vector._lock.locked()) + + def inject(): + self.conn.set_authorizer(authorizer) + self.conn.set_trace_callback(observe) + + try: + result = self.run_layer(self.spec(count_action=inject)) + finally: + self.conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.assertEqual(result.outcome, "failed") + self.assertEqual(denied, [True]) + self.assertEqual(terminal, [True]) + self.assertEqual((self.output(), self.success()), old) + self.assertFalse(self.vector._lock.locked()) + + def test_cancel_after_guard_entry_rolls_back_before_releasing(self) -> None: + old = self.output(), self.success() + cancel = threading.Event() + original = self.module.cluster_publication_guard + rollbacks = [] + + def factory(conn): + guard = original(conn) + + @contextlib.contextmanager + def held(): + with guard as validate: + cancel.set() + yield validate + + return held() + + self.conn.set_trace_callback(lambda sql: rollbacks.append(self.vector._lock.locked()) if sql == "ROLLBACK" else None) + result = self.run_layer(self.spec(factory=factory), cancel=cancel) + self.assertEqual(result.outcome, "abandoned") + self.assertEqual(rollbacks, [True]) + self.assertEqual((self.output(), self.success()), old) + self.assertFalse(self.vector._lock.locked()) + + def test_guard_validation_exception_releases_model_and_preserves_old_output(self) -> None: + old = self.output(), self.success() + original = self.module._cluster_dependency_identity + calls = [] + + def identity(*args): + calls.append(True) + if len(calls) == 4: + raise ValueError("synthetic guard failure") + return original(*args) + + with patch.object(self.module, "_cluster_dependency_identity", identity): + result = self.run_layer() + self.assertEqual(result.outcome, "failed") + self.assertEqual(len(calls), 4) + self.assertEqual((self.output(), self.success()), old) + self.assertFalse(self.vector._lock.locked()) + + def test_interruption_during_validation_rolls_back_under_model_ownership(self) -> None: + old = self.output(), self.success() + original = self.module._cluster_dependency_identity + calls = [] + rollbacks = [] + + def identity(*args): + calls.append(True) + if len(calls) == 4: + raise KeyboardInterrupt() + return original(*args) + + self.conn.set_trace_callback(lambda sql: rollbacks.append(self.vector._lock.locked()) if sql == "ROLLBACK" else None) + with patch.object(self.module, "_cluster_dependency_identity", identity): + with self.assertRaises(KeyboardInterrupt): + self.run_layer() + self.assertEqual(rollbacks, [True]) + self.assertEqual((self.output(), self.success()), old) + self.assertFalse(self.vector._lock.locked()) + + def test_failed_rollback_propagates_and_does_not_leave_model_lock_held(self) -> None: + old = self.output(), self.success() + denied = [] + + def authorizer(action, operation, *unused): + if action == sqlite3.SQLITE_TRANSACTION and operation in {"COMMIT", "ROLLBACK"}: + denied.append((operation, self.vector._lock.locked())) + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + + try: + with self.assertRaises(RuntimeError): + self.run_layer(self.spec(count_action=lambda: self.conn.set_authorizer(authorizer))) + self.assertTrue(self.conn.in_transaction) + self.assertFalse(self.vector._lock.locked()) + finally: + self.conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.conn.rollback() + self.assertEqual(denied, [("COMMIT", True), ("ROLLBACK", True)]) + self.assertEqual((self.output(), self.success()), old) + + def test_dependency_change_in_final_resolver_rejects_before_guard_entry(self) -> None: + old = self.output(), self.success() + original = MAINTENANCE.record_layer_success_in_transaction + + def checkpoint(*args, **kwargs): + original(*args, **kwargs) + self.dependency = MAINTENANCE.make_layer_dependency(2) + + with patch.object(MAINTENANCE, "record_layer_success_in_transaction", checkpoint): + result = self.run_layer() + self.assertEqual(result.outcome, "failed") + self.assertEqual((self.output(), self.success()), old) + self.assertFalse(self.vector._lock.locked()) + + def test_incomplete_index_and_missing_hdbscan_never_publish_empty_success(self) -> None: + old = self.output(), self.success() + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('vec_build_state:vec_messages_edge','in_progress')") + self.conn.commit() + self.assertEqual(self.run_layer().outcome, "failed") + self.assertEqual(self.boundary.inputs, []) + self.conn.execute("DELETE FROM metadata WHERE key='vec_build_state:vec_messages_edge'") + self.conn.commit() + self.boundary.missing = True + self.assertEqual(self.run_layer().outcome, "failed") + self.assertEqual((self.output(), self.success()), old) + self.dependency = MAINTENANCE.make_layer_dependency(1, available=False, error_category="ImportError") + self.assertEqual(self.run_layer().outcome, "unavailable") + self.assertEqual((self.output(), self.success()), old) + + def test_all_noise_publishes_valid_empty_cluster_count_and_complete_assignments(self) -> None: + self.boundary.labels = [-1] * 5 + result = self.run_layer() + self.assertEqual((result.outcome, result.output_count), ("success_empty", 0)) + self.assertEqual(self.output()[0], [(i, -1, 1) for i in range(1, 6)]) + self.assertEqual(self.output()[1], []) + self.assertEqual(self.success()[3], 0) + + def test_capture_rejects_borrowed_transaction_without_committing_it(self) -> None: + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('synthetic-pending','retained')") + with self.assertRaises(RuntimeError): + self.module.cluster_publication_guard(self.conn) + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + self.assertIsNone(self.conn.execute("SELECT value FROM metadata WHERE key='synthetic-pending'").fetchone()) + + def test_existing_six_layer_specs_do_not_capture_or_enter_cluster_guards(self) -> None: + self.assertTrue(all(spec.publication_guard is None for spec in MAINTENANCE.nonvector_layer_specs())) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-cluster-publication-749.py b/tests/test-cluster-publication-749.py new file mode 100644 index 00000000..941587ba --- /dev/null +++ b/tests/test-cluster-publication-749.py @@ -0,0 +1,656 @@ +"""SQLite publication tests with explicit synthetic native-library boundaries.""" + +import ast +import builtins +import math +import sqlite3 +import struct +import tempfile +import threading +import types +import unittest +from collections.abc import Callable, Iterator +from pathlib import Path +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] + + +def f32(value: float) -> float: + return struct.unpack("f", struct.pack("f", value))[0] + + +class SyntheticArray: + """Only the numeric operations exercised by the production cluster builder.""" + + def __init__(self, values: list) -> None: + self.values = values + + def __iter__(self) -> Iterator[object]: + return iter(SyntheticArray(value) if isinstance(value, list) else value for value in self.values) + + def __len__(self) -> int: + return len(self.values) + + def __eq__(self, scalar: float) -> list[bool]: + return [value == scalar for row in self.values for value in row] + + def __setitem__(self, mask: list[bool], scalar: float) -> None: + for row, selected in zip(self.values, mask): + if selected: + row[0] = scalar + + def __truediv__(self, other: "SyntheticArray") -> "SyntheticArray": + return SyntheticArray([[f32(value / scale[0]) for value in row] + for row, scale in zip(self.values, other.values)]) + + def astype(self, dtype: str) -> "SyntheticArray": + return SyntheticArray([f32(value) for value in self.values]) + + def tolist(self) -> list: + return self.values + + +class SyntheticNumpy: + float32 = "float32" + ndarray = SyntheticArray + + @staticmethod + def array(values: list, dtype: str | None = None) -> SyntheticArray: + return SyntheticArray([f32(value) for value in values]) + + @staticmethod + def stack(values: list[SyntheticArray]) -> SyntheticArray: + return SyntheticArray([value.tolist() for value in values]) + + @staticmethod + def mean(values: list[SyntheticArray], axis: int) -> SyntheticArray: + return SyntheticArray([sum(column) / len(values) for column in zip(*(value.tolist() for value in values))]) + + class linalg: + @staticmethod + def norm(values: SyntheticArray, axis: int, keepdims: bool) -> SyntheticArray: + return SyntheticArray([[f32(math.sqrt(sum(value * value for value in row)))] for row in values.tolist()]) + + +# The parent verification can inject actual NumPy here on the test host. Local +# execution never imports native libraries or the application package. +NUMPY = SyntheticNumpy() + + +def load_clustering() -> tuple[types.ModuleType, types.ModuleType, types.SimpleNamespace]: + vector = types.ModuleType("synthetic_vector_search") + vector._lock = threading.Lock() + vector.EMBEDDING_MODEL = "synthetic-model-a" + vector._embedding_dim = 2 + vector._cfg_get_model_group = lambda model: "basepro" if model == "synthetic-model-b" else "edge" + tree = ast.parse((ROOT / "truememory/vector_search.py").read_text()) + selected = [node for node in tree.body if isinstance(node, ast.FunctionDef) + and node.name in {"_active_tier_group", "_active_vec_table"}] + vector.sqlite3 = sqlite3 + exec(compile(ast.Module(body=selected, type_ignores=[]), "synthetic-vector-resolver", "exec"), vector.__dict__) + + boundary = types.SimpleNamespace(labels=[0, 0, -1, 1, 1], fit=None, parameters=[], inputs=[], missing=False) + + class SyntheticHdbscan: + def __init__(self, **parameters: object) -> None: + boundary.parameters.append(parameters) + + def fit_predict(self, values: SyntheticArray) -> list[int]: + boundary.inputs.append(values.tolist()) + if boundary.fit is not None: + boundary.fit() + return boundary.labels + + def synthetic_import( + name: str, globals: dict | None = None, locals: dict | None = None, + fromlist: tuple[str, ...] = (), level: int = 0, + ) -> object: + if name == "numpy": + return NUMPY + if name == "hdbscan": + if boundary.missing: + raise ModuleNotFoundError("synthetic missing hdbscan", name="hdbscan") + return types.SimpleNamespace(HDBSCAN=SyntheticHdbscan) + if name == "truememory": + return types.SimpleNamespace(vector_search=vector) + if name == "truememory.vector_search": + return vector + return builtins.__import__(name, globals, locals, fromlist, level) + + module = types.ModuleType("synthetic_clustering") + module.__dict__["__builtins__"] = dict(vars(builtins), __import__=synthetic_import) + path = ROOT / "truememory/clustering.py" + exec(compile(path.read_text(), str(path), "exec"), module.__dict__) + return module, vector, boundary + + +SCHEMA = """ +CREATE TABLE messages ( + id INTEGER PRIMARY KEY, content TEXT, sender TEXT, recipient TEXT, + timestamp TEXT, category TEXT, modality TEXT +); +CREATE TABLE vec_messages_edge (embedding BLOB); +CREATE TABLE metadata (key TEXT PRIMARY KEY, value TEXT, updated_at TEXT); +CREATE TABLE vector_cache_registry ( + tier_group TEXT PRIMARY KEY, vec_table TEXT, sep_table TEXT, + last_embedded_id INTEGER, vector_count INTEGER, model_name TEXT, + embedding_dim INTEGER, last_updated REAL, created REAL +); +""" + + +class ClusterFixture(unittest.TestCase): + def setUp(self) -> None: + self.module, self.vector, self.boundary = load_clustering() + self.temp = tempfile.TemporaryDirectory(prefix="synthetic-clusters-") + self.addCleanup(self.temp.cleanup) + self.path = Path(self.temp.name) / "synthetic.sqlite" + self.conn = self.open_db() + self.conn.executescript(SCHEMA + self.module._CLUSTER_SCHEMA) + self.seed() + + def open_db(self) -> sqlite3.Connection: + conn = sqlite3.connect(self.path, check_same_thread=False) + self.addCleanup(conn.close) + conn.execute("PRAGMA journal_mode = WAL") + conn.execute("PRAGMA foreign_keys = ON") + conn.execute("PRAGMA busy_timeout = 1000") + return conn + + def seed(self) -> None: + for table in ("message_clusters", "cluster_centroids", "vec_messages_edge", "messages", "metadata", "vector_cache_registry"): + self.conn.execute(f"DELETE FROM {table}") + vectors = [(1, 0), (0, 1), (0, 0), (2, 2), (4, 0)] + for mid, (vector, category) in enumerate(zip(vectors, ["session-z", "session-a", "noise", None, "session-z"]), 1): + self.conn.execute( + "INSERT INTO messages VALUES (?, 'synthetic source', 'synthetic-sender', 'synthetic-recipient', '2026-01-01', ?, 'text')", + (mid, category), + ) + self.conn.execute("INSERT INTO vec_messages_edge(rowid, embedding) VALUES (?, ?)", (mid, struct.pack("2f", *vector))) + self.conn.execute("INSERT INTO metadata VALUES ('embed_model', 'synthetic-model-a', 'synthetic-stamp')") + self.conn.execute("INSERT INTO metadata VALUES ('embed_dim', '2', 'synthetic-stamp')") + self.conn.execute("INSERT INTO vector_cache_registry VALUES ('edge', 'vec_messages_edge', 'vec_messages_sep_edge', 5, 5, 'synthetic-model-a', 2, 1.0, 1.0)") + self.conn.execute("INSERT INTO message_clusters VALUES (1, 7, 0)") + self.conn.execute("INSERT INTO cluster_centroids VALUES (7, ?, 1, 'previous-session', 'synthetic previous summary')", (struct.pack("2f", 9, 9),)) + self.conn.commit() + + def output(self, conn: sqlite3.Connection | None = None) -> tuple[list[tuple], list[tuple]]: + db = conn or self.conn + return ( + db.execute("SELECT * FROM message_clusters ORDER BY message_id").fetchall(), + db.execute("SELECT * FROM cluster_centroids ORDER BY cluster_id").fetchall(), + ) + + def start_worker(self, action: Callable[[], object]) -> tuple[threading.Thread, list[object]]: + outcomes = [] + + def run() -> None: + try: + outcomes.append(action()) + except Exception as error: # Forward thread failures to the test. + outcomes.append(error) + + worker = threading.Thread(target=run) + worker.start() + self.addCleanup(worker.join, 3) + return worker, outcomes + + +class TestClusterComputation(ClusterFixture): + def test_reference_parameters_normalization_assignments_and_centroids(self) -> None: + self.assertEqual(self.module.cluster_messages(self.conn, min_cluster_size=3, min_samples=2), 2) + self.assertEqual(self.boundary.parameters, [{ + "min_cluster_size": 3, "min_samples": 2, "metric": "euclidean", "cluster_selection_method": "eom", + }]) + assignments, centroids = self.output() + self.assertEqual(assignments, [(1, 0, 0), (2, 0, 0), (3, -1, 1), (4, 1, 0), (5, 1, 0)]) + self.assertEqual(centroids, [ + (0, struct.pack("2f", 0.5, 0.5), 2, "session-a, session-z", ""), + (1, struct.pack("2f", 3, 1), 2, "session-z", ""), + ]) + self.assertEqual(self.boundary.inputs[0][:3], [[1, 0], [0, 1], [0, 0]]) + for actual in self.boundary.inputs[0][3]: + self.assertAlmostEqual(actual, 1 / math.sqrt(2), places=6) + self.assertFalse(self.conn.in_transaction) + + def test_compute_failure_leaves_previous_cache_after_caller_commit(self) -> None: + before = self.output() + + def fail() -> None: + self.assertFalse(self.conn.in_transaction) + raise RuntimeError("synthetic compute failure") + + self.boundary.fit = fail + with self.assertRaisesRegex(RuntimeError, "synthetic compute failure"): + self.module.cluster_messages(self.conn) + self.conn.commit() + self.assertEqual(self.output(), before) + self.assertFalse(self.conn.in_transaction) + + def test_embedding_read_failure_preserves_previous_cache(self) -> None: + before = self.output() + with patch.object(self.module, "_get_all_embeddings", side_effect=ValueError("synthetic embedding failure")): + with self.assertRaisesRegex(ValueError, "synthetic embedding failure"): + self.module.cluster_messages(self.conn) + self.conn.commit() + self.assertEqual(self.output(), before) + self.assertFalse(self.conn.in_transaction) + + def test_centroid_serialization_failure_never_opens_publication(self) -> None: + before = self.output() + serialize = self.module._serialize_f32 + calls = [] + + def fail_second(vector: SyntheticArray) -> bytes: + self.assertFalse(self.conn.in_transaction) + calls.append(1) + if len(calls) == 2: + raise ValueError("synthetic centroid failure") + return serialize(vector) + + with patch.object(self.module, "_serialize_f32", fail_second): + with self.assertRaisesRegex(ValueError, "synthetic centroid failure"): + self.module.cluster_messages(self.conn) + self.conn.commit() + self.assertEqual(self.output(), before) + + def test_incomplete_hdbscan_labels_cannot_publish_partial_cache(self) -> None: + before = self.output() + self.boundary.labels = [0] + with self.assertRaisesRegex(ValueError, "incomplete assignment"): + self.module.cluster_messages(self.conn) + self.assertEqual(self.output(), before) + + def test_missing_dependency_remains_an_error_even_with_empty_vectors(self) -> None: + self.conn.execute("DELETE FROM vec_messages_edge") + self.conn.commit() + before = self.output() + self.boundary.missing = True + with self.assertRaises(ModuleNotFoundError): + self.module.cluster_messages(self.conn) + self.assertEqual(self.output(), before) + self.assertFalse(self.conn.in_transaction) + + def test_empty_completed_vector_input_atomically_clears_old_cache(self) -> None: + self.conn.execute("DELETE FROM vec_messages_edge") + self.conn.commit() + self.assertEqual(self.module.cluster_messages(self.conn), 0) + self.assertEqual(self.output(), ([], [])) + self.assertEqual(self.boundary.parameters, []) + + def test_empty_snapshot_cannot_clear_cache_after_concurrent_vector_addition(self) -> None: + self.conn.execute("DELETE FROM vec_messages_edge") + self.conn.commit() + writer = self.open_db() + before = self.output() + extract = self.module._get_all_embeddings + + def insert_after_read(conn: sqlite3.Connection) -> tuple[list[int], SyntheticArray]: + result = extract(conn) + writer.execute("INSERT INTO vec_messages_edge(rowid, embedding) VALUES (1, ?)", (struct.pack("2f", 1, 0),)) + writer.commit() + return result + + with patch.object(self.module, "_get_all_embeddings", insert_after_read): + with self.assertRaisesRegex(RuntimeError, "changed during computation"): + self.module.cluster_messages(self.conn) + self.assertEqual(self.output(), before) + + def test_missing_vector_table_is_not_successful_empty_input(self) -> None: + self.conn.execute("DROP TABLE vec_messages_edge") + self.conn.commit() + before = self.output() + with self.assertRaises(sqlite3.OperationalError): + self.module.cluster_messages(self.conn) + self.assertEqual(self.output(), before) + + def test_in_progress_vector_index_does_not_clear_old_cache(self) -> None: + self.conn.execute("DELETE FROM vec_messages_edge") + self.conn.execute("INSERT INTO metadata VALUES ('vec_build_state:vec_messages_edge', 'in_progress', 'synthetic-stamp')") + self.conn.commit() + before = self.output() + with self.assertRaisesRegex(RuntimeError, "rebuild is in progress"): + self.module.cluster_messages(self.conn) + self.assertEqual(self.output(), before) + + def test_orphan_vector_is_rejected_without_resurrecting_assignment(self) -> None: + self.conn.execute("DELETE FROM messages WHERE id = 2") + self.conn.commit() + before = self.output() + with self.assertRaisesRegex(RuntimeError, "without source messages"): + self.module.cluster_messages(self.conn) + self.assertEqual(self.output(), before) + + def test_legacy_vector_table_without_identity_metadata_remains_supported(self) -> None: + self.conn.execute("ALTER TABLE vec_messages_edge RENAME TO vec_messages") + self.conn.execute("DROP TABLE vector_cache_registry") + self.conn.execute("DROP TABLE metadata") + self.conn.commit() + self.assertEqual(self.module.cluster_messages(self.conn), 2) + + +class TestClusterSourceFence(ClusterFixture): + def test_source_and_vector_space_changes_reject_publication(self) -> None: + writer = self.open_db() + changes = { + "new_source_without_vector": "INSERT INTO messages(id, content, category) VALUES (6, 'synthetic addition', 'new-session')", + "source_edit": "UPDATE messages SET content = 'synthetic replacement' WHERE id = 2", + "source_delete": "DELETE FROM messages WHERE id = 2", + "category_edit": "UPDATE messages SET category = 'changed-session' WHERE id = 2", + "timestamp_edit": "UPDATE messages SET timestamp = '2026-02-01' WHERE id = 2", + "vector_edit": "UPDATE vec_messages_edge SET embedding = X'000080400000803F' WHERE rowid = 2", + "raw_signed_zero": "UPDATE vec_messages_edge SET embedding = X'000000800000803F' WHERE rowid = 2", + "vector_delete": "DELETE FROM vec_messages_edge WHERE rowid = 2", + "vector_insert": "INSERT INTO vec_messages_edge(rowid, embedding) VALUES (6, X'0000803F00000000')", + "stored_model": "UPDATE metadata SET value = 'synthetic-model-new' WHERE key = 'embed_model'", + "stored_dimension": "UPDATE metadata SET value = '4' WHERE key = 'embed_dim'", + "stored_identity_stamp": "UPDATE metadata SET updated_at = 'new-stamp' WHERE key = 'embed_model'", + "cache_model": "UPDATE vector_cache_registry SET model_name = 'synthetic-model-new'", + "cache_dimension": "UPDATE vector_cache_registry SET embedding_dim = 4", + "cache_identity_stamp": "UPDATE vector_cache_registry SET created = 2.0", + "rebuild_in_progress": "INSERT INTO metadata VALUES ('vec_build_state:vec_messages_edge', 'in_progress', 'synthetic-stamp')", + } + for name, statement in changes.items(): + with self.subTest(change=name): + self.seed() + before = self.output() + + def change_source() -> None: + writer.execute(statement) + writer.commit() + + self.boundary.fit = change_source + with self.assertRaisesRegex(RuntimeError, "changed during computation|rebuild is in progress"): + self.module.cluster_messages(self.conn) + self.conn.commit() + self.assertEqual(self.output(), before) + + def test_active_table_change_rejects_identical_vector_bytes(self) -> None: + writer = self.open_db() + writer.execute("CREATE TABLE vec_alternative (embedding BLOB)") + writer.execute("INSERT INTO vec_alternative(rowid, embedding) SELECT rowid, embedding FROM vec_messages_edge") + writer.commit() + before = self.output() + + def switch_table() -> None: + writer.execute("UPDATE vector_cache_registry SET vec_table = 'vec_alternative'") + writer.commit() + + self.boundary.fit = switch_table + with self.assertRaisesRegex(RuntimeError, "changed during computation"): + self.module.cluster_messages(self.conn) + self.assertEqual(self.output(), before) + + def test_runtime_model_and_dimension_changes_reject_publication(self) -> None: + for field, new_value in (("EMBEDDING_MODEL", "synthetic-model-new"), ("_embedding_dim", 4)): + with self.subTest(field=field): + before = self.output() + + def change_model() -> None: + with self.vector._lock: + setattr(self.vector, field, new_value) + + self.boundary.fit = change_model + with self.assertRaisesRegex(RuntimeError, "changed during computation"): + self.module.cluster_messages(self.conn) + self.assertEqual(self.output(), before) + + def test_table_definition_change_rejects_identical_vector_bytes(self) -> None: + writer = self.open_db() + before = self.output() + + def change_schema() -> None: + writer.execute("ALTER TABLE vec_messages_edge ADD COLUMN synthetic_marker TEXT") + writer.commit() + + self.boundary.fit = change_schema + with self.assertRaisesRegex(RuntimeError, "changed during computation"): + self.module.cluster_messages(self.conn) + self.assertEqual(self.output(), before) + + def test_unrelated_metadata_write_does_not_reject_current_source(self) -> None: + writer = self.open_db() + + def unrelated_write() -> None: + writer.execute("INSERT INTO metadata VALUES ('synthetic-unrelated', 'new', 'synthetic-stamp')") + writer.commit() + + self.boundary.fit = unrelated_write + self.assertEqual(self.module.cluster_messages(self.conn), 2) + + def test_fingerprint_and_embedding_reads_share_one_snapshot(self) -> None: + writer = self.open_db() + before = self.output() + extract = self.module._get_all_embeddings + captured = [] + + def concurrent_read(conn: sqlite3.Connection) -> tuple[list[int], SyntheticArray]: + writer.execute("UPDATE vec_messages_edge SET embedding = ? WHERE rowid = 1", (struct.pack("2f", 9, 9),)) + writer.execute("UPDATE messages SET category = 'changed-session' WHERE id = 1") + writer.commit() + result = extract(conn) + captured.append(result[1].tolist()) + return result + + with patch.object(self.module, "_get_all_embeddings", concurrent_read): + with self.assertRaisesRegex(RuntimeError, "changed during computation"): + self.module.cluster_messages(self.conn) + self.assertEqual(captured[0][0], [1, 0]) + self.assertEqual(self.output(), before) + + def test_second_writer_can_commit_while_compute_is_paused(self) -> None: + writer = self.open_db() + entered = threading.Event() + release = threading.Event() + + def pause() -> None: + entered.set() + release.wait(2) + + self.boundary.fit = pause + worker, outcomes = self.start_worker(lambda: self.module.cluster_messages(self.conn)) + self.addCleanup(release.set) + self.assertTrue(entered.wait(2)) + self.assertFalse(self.conn.in_transaction) + writer.execute("INSERT INTO metadata VALUES ('synthetic-unrelated', 'new', 'synthetic-stamp')") + writer.commit() + release.set() + worker.join(2) + self.assertEqual(outcomes, [2]) + + +class TestClusterPublication(ClusterFixture): + def test_mid_centroid_failure_rolls_back_both_tables_even_if_caller_commits(self) -> None: + before = self.output() + self.conn.executescript(""" + CREATE TRIGGER synthetic_centroid_failure BEFORE INSERT ON cluster_centroids + WHEN new.cluster_id = 1 BEGIN SELECT RAISE(ABORT, 'synthetic centroid insert failure'); END; + """) + with self.assertRaisesRegex(sqlite3.IntegrityError, "synthetic centroid insert failure"): + self.module.cluster_messages(self.conn) + self.conn.commit() + self.assertEqual(self.output(), before) + self.assertFalse(self.conn.in_transaction) + self.assertTrue(self.vector._lock.acquire(blocking=False)) + self.vector._lock.release() + + def test_caller_transaction_retains_unrelated_work_and_publication_until_rollback(self) -> None: + reader = self.open_db() + before = self.output() + self.conn.execute("INSERT INTO metadata VALUES ('synthetic-caller', 'pending', 'synthetic-stamp')") + self.assertEqual(self.module.cluster_messages(self.conn), 2) + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.output(reader), before) + self.assertIsNone(reader.execute("SELECT value FROM metadata WHERE key = 'synthetic-caller'").fetchone()) + self.conn.rollback() + self.assertEqual(self.output(), before) + self.assertIsNone(self.conn.execute("SELECT value FROM metadata WHERE key = 'synthetic-caller'").fetchone()) + + def test_publication_failure_rolls_back_savepoint_but_preserves_caller_write(self) -> None: + before = self.output() + self.conn.executescript(""" + CREATE TRIGGER synthetic_assignment_failure BEFORE INSERT ON message_clusters + WHEN new.message_id = 2 BEGIN SELECT RAISE(ABORT, 'synthetic assignment failure'); END; + """) + self.conn.execute("INSERT INTO metadata VALUES ('synthetic-caller', 'pending', 'synthetic-stamp')") + with self.assertRaisesRegex(sqlite3.IntegrityError, "synthetic assignment failure"): + self.module.cluster_messages(self.conn) + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.output(), before) + self.assertEqual(self.conn.execute("SELECT value FROM metadata WHERE key = 'synthetic-caller'").fetchone(), ("pending",)) + self.conn.rollback() + + def test_caller_read_snapshot_conflict_is_not_retried_or_committed(self) -> None: + writer = self.open_db() + before = self.output() + self.conn.execute("BEGIN") + self.conn.execute("SELECT count(*) FROM messages").fetchone() + + def write() -> None: + writer.execute("UPDATE messages SET category = 'changed-session' WHERE id = 2") + writer.commit() + + self.boundary.fit = write + with self.assertRaises(sqlite3.OperationalError): + self.module.cluster_messages(self.conn) + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.output(), before) + self.assertEqual(len(self.boundary.inputs), 1) + self.conn.rollback() + + def test_schema_creation_does_not_commit_caller_transaction(self) -> None: + self.conn.execute("DROP TABLE message_clusters") + self.conn.execute("DROP TABLE cluster_centroids") + self.conn.commit() + self.conn.execute("INSERT INTO metadata VALUES ('synthetic-caller', 'pending', 'synthetic-stamp')") + self.module.cluster_messages(self.conn) + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + self.assertIsNone(self.conn.execute("SELECT name FROM sqlite_master WHERE name = 'message_clusters'").fetchone()) + self.assertIsNone(self.conn.execute("SELECT value FROM metadata WHERE key = 'synthetic-caller'").fetchone()) + + def test_busy_model_lock_fails_without_waiting_and_releases_writer(self) -> None: + writer = self.open_db() + before = self.output() + self.boundary.fit = lambda: self.vector._lock.acquire() + try: + with self.assertRaisesRegex(RuntimeError, "Embedding model is busy"): + self.module.cluster_messages(self.conn) + self.assertFalse(self.conn.in_transaction) + self.assertEqual(self.output(), before) + writer.execute("INSERT INTO metadata VALUES ('synthetic-unrelated', 'new', 'synthetic-stamp')") + writer.commit() + finally: + self.vector._lock.release() + + def test_model_lock_is_held_through_publication_commit(self) -> None: + commits = [] + real_conn = self.conn + vector = self.vector + + class CheckCommit: + def __getattr__(self, name: str) -> object: + return getattr(real_conn, name) + + def execute(self, sql: str, parameters: tuple = ()) -> sqlite3.Cursor: + if sql == "COMMIT": + commits.append(vector._lock.locked()) + return real_conn.execute(sql, parameters) + + self.module.cluster_messages(CheckCommit()) + self.assertEqual(commits, [True, True]) + self.assertFalse(self.vector._lock.locked()) + + def test_commit_and_release_failure_roll_back_before_later_caller_commit(self) -> None: + before = self.output() + for caller_owned in (False, True): + with self.subTest(caller_owned=caller_owned): + denied = [] + actions = [] + if caller_owned: + self.conn.execute("INSERT INTO metadata VALUES ('synthetic-caller', 'pending', 'synthetic-stamp')") + + def deny_terminal_once(action: int, operation: str, *unused: str | None) -> int: + if action in (sqlite3.SQLITE_TRANSACTION, sqlite3.SQLITE_SAVEPOINT): + actions.append(operation) + target = "RELEASE" if caller_owned else "COMMIT" + if operation == target and action in (sqlite3.SQLITE_TRANSACTION, sqlite3.SQLITE_SAVEPOINT) and not denied: + denied.append(target) + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + + # Install after read transaction completion. This also expires + # SQLite's cached prepared statements and authorization checks. + self.boundary.fit = lambda: self.conn.set_authorizer(deny_terminal_once) + try: + with self.assertRaises(sqlite3.DatabaseError): + self.module.cluster_messages(self.conn) + finally: + # Disabling with None is supported only on Python 3.11+. + self.conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.assertEqual(denied, ["RELEASE" if caller_owned else "COMMIT"]) + self.assertIn("ROLLBACK", actions) + if caller_owned: + self.assertEqual(actions[-2:], ["ROLLBACK", "RELEASE"]) + self.assertEqual(self.conn.in_transaction, caller_owned) + self.assertFalse(self.vector._lock.locked()) + self.conn.commit() + self.assertEqual(self.output(), before) + if caller_owned: + self.assertEqual(self.conn.execute("SELECT value FROM metadata WHERE key = 'synthetic-caller'").fetchone(), ("pending",)) + + def test_cancellation_during_publication_rolls_back_and_releases_model_lock(self) -> None: + before = self.output() + real_conn = self.conn + + class CancelAfterAssignment: + def __getattr__(self, name: str) -> object: + return getattr(real_conn, name) + + def executemany(self, sql: str, parameters: list[tuple]) -> sqlite3.Cursor: + cursor = real_conn.executemany(sql, parameters) + if sql.startswith("INSERT INTO message_clusters"): + raise KeyboardInterrupt("synthetic cancellation") + return cursor + + with self.assertRaises(KeyboardInterrupt): + self.module.cluster_messages(CancelAfterAssignment()) + self.conn.commit() + self.assertEqual(self.output(), before) + self.assertFalse(self.conn.in_transaction) + self.assertFalse(self.vector._lock.locked()) + + def test_reader_sees_old_assignments_and_centroids_until_atomic_commit(self) -> None: + reader = self.open_db() + before = self.output() + entered = threading.Event() + release = threading.Event() + + def pause() -> int: + entered.set() + release.wait(2) + return 0 + + self.conn.create_function("synthetic_pause", 0, pause) + self.conn.executescript(""" + CREATE TRIGGER synthetic_pause_insert BEFORE INSERT ON cluster_centroids + WHEN new.cluster_id = 1 BEGIN SELECT synthetic_pause(); END; + """) + worker, outcomes = self.start_worker(lambda: self.module.cluster_messages(self.conn)) + self.addCleanup(release.set) + self.assertTrue(entered.wait(2)) + reader.execute("BEGIN") + self.assertEqual(self.output(reader), before) + release.set() + worker.join(2) + self.assertEqual(outcomes, [2]) + self.assertEqual(self.output(reader), before) + reader.commit() + self.assertEqual(self.output(reader), self.output()) + self.assertNotEqual(self.output(reader), before) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-clustered-search-materialization-754.py b/tests/test-clustered-search-materialization-754.py new file mode 100644 index 00000000..6702ee13 --- /dev/null +++ b/tests/test-clustered-search-materialization-754.py @@ -0,0 +1,669 @@ +"""Exact clustered retrieval with synthetic SQLite data and fake native boundaries.""" +from __future__ import annotations + +import ast +import builtins +import heapq +import math +import random +import sqlite3 +import struct +import tempfile +import types +import unittest +import weakref +from pathlib import Path +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] + + +class SyntheticArray(list): + def astype(self, dtype: object) -> "SyntheticArray": + return SyntheticArray(struct.unpack("f", struct.pack("f", value))[0] for value in self) + + +class SyntheticNumpy: + float32 = "float32" + ndarray = SyntheticArray + + @staticmethod + def array(values: object, dtype: object = None) -> SyntheticArray: + return SyntheticArray(values).astype(dtype) + + @staticmethod + def dot(left: list, right: list) -> float: + if len(left) != len(right): + raise ValueError("synthetic shape mismatch") + return sum(a * b for a, b in zip(left, right)) + + class linalg: + @staticmethod + def norm(values: list) -> float: + return math.sqrt(sum(value * value for value in values)) + + +# Remote verification may inject real NumPy. The default imports no framework +# or application package and never constructs a real embedding model. +NUMPY = SyntheticNumpy() + +# Frozen implementation at 9b2b6ff, including its actual SQL and stable sort. +REFERENCE_SOURCE = r''' +def reference_search_clustered( + conn: sqlite3.Connection, + query: str, + limit: int = 10, + top_clusters: int = 3, + include_directives: bool = False, +) -> list[dict]: + """ + Two-stage clustered search: find top clusters, then search within them. + + 1. Embed the query. + 2. Find the *top_clusters* most similar cluster centroids. + 3. Retrieve messages from those clusters. + 4. Rank by vector similarity within the selected clusters. + + This can surface results that flat search misses by focusing on + contextually coherent message groups. + + Args: + conn: Open database connection. + query: The search query. + limit: Maximum results to return. + top_clusters: Number of top clusters to search within. + + Returns: + List of result dicts sorted by similarity. + """ + from truememory.vector_search import get_model + from truememory.mps_utils import encode_with_model_ownership + + model = get_model() + query_vec = encode_with_model_ownership(model, [query])[0].astype(np.float32) + + # Check if clusters exist + try: + cluster_count = conn.execute( + "SELECT COUNT(*) FROM cluster_centroids" + ).fetchone()[0] + except Exception: + return [] + + if cluster_count == 0: + return [] + + # Find top clusters by centroid similarity + centroids = conn.execute( + "SELECT cluster_id, centroid, message_count FROM cluster_centroids" + ).fetchall() + + cluster_scores = [] + for cid, centroid_blob, msg_count in centroids: + dim = len(centroid_blob) // 4 + centroid = np.array(struct.unpack(f"{dim}f", centroid_blob), dtype=np.float32) + # Cosine similarity + sim = np.dot(query_vec, centroid) / ( + np.linalg.norm(query_vec) * np.linalg.norm(centroid) + 1e-9 + ) + cluster_scores.append((cid, float(sim), msg_count)) + + cluster_scores.sort(key=lambda x: x[1], reverse=True) + selected_clusters = [c[0] for c in cluster_scores[:top_clusters]] + + if not selected_clusters: + return [] + + # Get message IDs from selected clusters + placeholders = ",".join("?" * len(selected_clusters)) + cluster_msg_rows = conn.execute( + f"SELECT message_id FROM message_clusters WHERE cluster_id IN ({placeholders})", + selected_clusters, + ).fetchall() + + cluster_msg_ids = {r[0] for r in cluster_msg_rows} + if not cluster_msg_ids: + return [] + + # Get full messages with their embeddings + id_placeholders = ",".join("?" * len(cluster_msg_ids)) + msg_ids_list = list(cluster_msg_ids) + + directive_filter = ( + "" if include_directives + else " AND (m.directive = 0 OR m.directive IS NULL)" + ) + messages = conn.execute( + f"""SELECT m.id, m.content, m.sender, m.recipient, m.timestamp, + m.category, m.modality, m.directive + FROM messages m + WHERE m.id IN ({id_placeholders}){directive_filter}""", + msg_ids_list, + ).fetchall() + + if not messages: + return [] + + # Resolve vec table once outside the loop (not per-message) + from truememory.vector_search import _active_vec_table + _vec_tbl = _active_vec_table(conn) + + # Batch-fetch all embeddings in one query (issue #584) instead of + # one SELECT per message. Chunk into groups of 500 to stay within + # SQLite's parameter limits. + _CHUNK = 500 + emb_map: dict[int, np.ndarray] = {} + all_msg_ids = [msg[0] for msg in messages] + for chunk_start in range(0, len(all_msg_ids), _CHUNK): + chunk = all_msg_ids[chunk_start : chunk_start + _CHUNK] + ph = ",".join("?" * len(chunk)) + try: + rows = conn.execute( + f"SELECT rowid, embedding FROM {_vec_tbl} WHERE rowid IN ({ph})", + chunk, + ).fetchall() + for rid, blob in rows: + dim = len(blob) // 4 + emb_map[rid] = np.array( + struct.unpack(f"{dim}f", blob), dtype=np.float32 + ) + except Exception: + logger.debug("Batch embedding fetch failed for chunk", exc_info=True) + + # Pre-compute query norm once + _query_norm = np.linalg.norm(query_vec) + 1e-9 + + # Score each message by vector similarity to query + results = [] + for msg in messages: + msg_id = msg[0] + msg_vec = emb_map.get(msg_id) + if msg_vec is not None: + sim = float(np.dot(query_vec, msg_vec) / ( + _query_norm * (np.linalg.norm(msg_vec) + 1e-9) + )) + else: + sim = 0.0 + + results.append({ + "id": msg_id, + "content": msg[1], + "sender": msg[2], + "recipient": msg[3], + "timestamp": msg[4], + "category": msg[5], + "modality": msg[6], + "directive": bool(msg[7]), + "score": sim, + "source": "clustered", + }) + + results.sort(key=lambda r: r["score"], reverse=True) + return results[:limit] +''' + + +def load_clustering() -> tuple[types.ModuleType, types.SimpleNamespace]: + boundary = types.SimpleNamespace(query=[1.0, 0.0, 0.0, 0.0], before_encode=None, encodes=0) + vector = types.ModuleType("synthetic_vector_search") + vector._active_tier_group = lambda: "basepro" + vector.sqlite3 = sqlite3 + source = (ROOT / "truememory/vector_search.py").read_text() + nodes = [node for node in ast.parse(source).body if isinstance(node, ast.FunctionDef) + and node.name == "_active_vec_table"] + exec(compile(ast.Module(body=nodes, type_ignores=[]), "synthetic-vector-table", "exec"), vector.__dict__) + model = object() + vector.get_model = lambda: model + + def encode(actual_model: object, texts: list[str]) -> list: + assert actual_model is model and texts == ["synthetic query"] + boundary.encodes += 1 + if boundary.before_encode: + boundary.before_encode() + return [NUMPY.array(boundary.query, dtype=NUMPY.float32)] + + def synthetic_import(name: str, globals: dict | None = None, locals: dict | None = None, + fromlist: tuple = (), level: int = 0) -> object: + if name == "numpy": + return NUMPY + if name == "truememory.vector_search": + return vector + if name == "truememory.mps_utils": + return types.SimpleNamespace(encode_with_model_ownership=encode) + if name == "truememory" or name.startswith("truememory."): + raise AssertionError("unexpected application import") + return builtins.__import__(name, globals, locals, fromlist, level) + + module = types.ModuleType("synthetic_clustering") + module.__dict__["__builtins__"] = dict(vars(builtins), __import__=synthetic_import) + path = ROOT / "truememory/clustering.py" + exec(compile(path.read_text(), str(path), "exec"), module.__dict__) + exec(compile(REFERENCE_SOURCE, "frozen-clustered-reference-9b2b6ff", "exec"), module.__dict__) + return module, boundary + + +SCHEMA = """ +CREATE TABLE messages ( + id INTEGER PRIMARY KEY, content TEXT, sender TEXT, recipient TEXT, + timestamp TEXT, category TEXT, modality TEXT, directive INTEGER +); +CREATE TABLE cluster_centroids (cluster_id INTEGER PRIMARY KEY, centroid BLOB, message_count INTEGER); +CREATE TABLE message_clusters (message_id INTEGER PRIMARY KEY, cluster_id INTEGER); +CREATE INDEX idx_cluster_id ON message_clusters(cluster_id); +CREATE TABLE vec_messages (embedding BLOB); +CREATE TABLE vector_cache_registry (tier_group TEXT PRIMARY KEY, vec_table TEXT); +INSERT INTO vector_cache_registry VALUES ('basepro', 'vec_messages'); +""" + + +def blob(values: list[float]) -> bytes: + return struct.pack(f"{len(values)}f", *values) + + +class ObservedCursor: + def __init__(self, cursor: sqlite3.Cursor, owner: "ObservedConnection", kind: str) -> None: + self.cursor, self.owner, self.kind = cursor, owner, kind + + def _record(self, row: tuple | None) -> tuple | None: + if row is not None: + self.owner.rows[self.kind] = self.owner.rows.get(self.kind, 0) + 1 + return row + + def __iter__(self) -> "ObservedCursor": + return self + + def __next__(self) -> tuple: + return self._record(next(self.cursor)) + + def fetchone(self) -> tuple | None: + return self._record(self.cursor.fetchone()) + + def fetchall(self) -> list[tuple]: + rows = self.cursor.fetchall() + self.owner.max_fetchall[self.kind] = max(self.owner.max_fetchall.get(self.kind, 0), len(rows)) + return [self._record(row) for row in rows] + + def close(self) -> None: + self.cursor.close() + self.owner.closed += 1 + + +class ObservedConnection: + def __init__(self, conn: sqlite3.Connection, maximum_parameters: int = 500) -> None: + self.conn = conn + self.maximum_parameters = maximum_parameters + self.parameter_counts = [] + self.rows = {} + self.max_fetchall = {} + self.before_vectors = None + self.before_content = None + self.closed = 0 + + def __getattr__(self, name: str) -> object: + return getattr(self.conn, name) + + def execute(self, sql: str, parameters: tuple | list = ()) -> ObservedCursor: + self.parameter_counts.append(len(parameters)) + if len(parameters) > self.maximum_parameters: + raise sqlite3.OperationalError("synthetic SQL parameter limit") + normalized = " ".join(sql.lower().split()) + kind = "other" + if normalized.startswith("select") and "content" in normalized: + kind = "content" + if self.before_content: + self.before_content() + elif normalized.startswith("select rowid, embedding"): + kind = "vectors" + if self.before_vectors: + callback, self.before_vectors = self.before_vectors, None + callback() + elif normalized.startswith("select distinct m.id"): + kind = "candidate_ids" + return ObservedCursor(self.conn.execute(sql, parameters), self, kind) + + +class ClusteredSearchFixture(unittest.TestCase): + def setUp(self) -> None: + self.module, self.boundary = load_clustering() + self.temp = tempfile.TemporaryDirectory(prefix="synthetic-cluster-search-") + self.addCleanup(self.temp.cleanup) + self.path = Path(self.temp.name) / "synthetic.sqlite" + self.conn = self.open_db() + self.conn.executescript(SCHEMA) + + def open_db(self) -> sqlite3.Connection: + conn = sqlite3.connect(self.path) + conn.execute("PRAGMA journal_mode=WAL") + conn.execute("PRAGMA busy_timeout=1000") + self.addCleanup(conn.close) + return conn + + def seed(self, count: int = 40, clusters: int = 5, *, random_seed: int = 0, + all_ties: bool = False, directives: bool = True, content_size: int = 32) -> None: + rng = random.Random(random_seed) + for table in ("messages", "message_clusters", "cluster_centroids", "vec_messages"): + self.conn.execute(f"DELETE FROM {table}") + centroids = [[1.0, 0.0, 0.0, 0.0], [1.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 0.0, 0.0], [-1.0, 0.0, 0.0, 0.0], [0.0] * 4] + for cid in range(clusters): + self.conn.execute("INSERT INTO cluster_centroids VALUES (?,?,?)", (cid, blob(centroids[cid % 5]), count)) + ids = [index * 7 + 3 for index in range(count)] + rng.shuffle(ids) + for mid in ids: + vector = [1.0, 0.0, 0.0, 0.0] if all_ties else [float(rng.randrange(-3, 4)) for _ in range(4)] + directive = (None, 0, 0, 0, 1)[mid % 5] if directives else 0 + self.conn.execute("INSERT INTO messages VALUES (?,?,?,?,?,?,?,?)", ( + mid, "synthetic-" + "x" * content_size, "synthetic-sender", "synthetic-recipient", + "2026-01-01", None if mid % 3 else "synthetic-category", "text", directive, + )) + self.conn.execute("INSERT INTO message_clusters VALUES (?,?)", (mid, mid % clusters)) + if all_ties or mid % 11: + self.conn.execute("INSERT INTO vec_messages(rowid,embedding) VALUES (?,?)", (mid, blob(vector))) + self.conn.commit() + + def compare(self, **kwargs: object) -> list[dict]: + expected = self.module.reference_search_clustered(self.conn, "synthetic query", **kwargs) + result = self.module.search_clustered(self.conn, "synthetic query", **kwargs) + self.assertEqual(result, expected) + return result + + +class TestExactClusteredSearch(ClusteredSearchFixture): + def test_randomized_results_match_frozen_sql_reference(self) -> None: + for seed in range(15): + self.seed(61, random_seed=seed) + self.boundary.query = [float(random.Random(seed + axis).randrange(-3, 4)) for axis in range(4)] + for top_clusters in (1, 3, 0, -1, 8): + for limit in (0, 1, 17, 100, -7, -100): + with self.subTest(seed=seed, top_clusters=top_clusters, limit=limit): + self.compare(limit=limit, top_clusters=top_clusters, include_directives=bool(seed % 2)) + + def test_equal_scores_keep_original_primary_key_order(self) -> None: + self.seed(63, all_ties=True, directives=False) + for table, key in (("messages", "id"), ("message_clusters", "message_id"), ("vec_messages", "rowid")): + self.conn.execute(f"UPDATE {table} SET {key}=-{key} WHERE {key}<40") + self.conn.commit() + results = self.compare(limit=31) + self.assertEqual([row["id"] for row in results], sorted(row["id"] for row in results)) + self.assertEqual(len({row["score"] for row in results}), 1) + + def test_missing_vectors_zero_vectors_and_negative_scores(self) -> None: + self.seed(4, clusters=1, all_ties=True, directives=False) + self.conn.execute("DELETE FROM vec_messages WHERE rowid=3") + self.conn.execute("UPDATE vec_messages SET embedding=? WHERE rowid=10", (blob([0.0] * 4),)) + self.conn.execute("UPDATE vec_messages SET embedding=? WHERE rowid=17", (blob([-1.0, 0.0, 0.0, 0.0]),)) + self.conn.commit() + rows = self.compare(limit=100) + self.assertEqual([row["id"] for row in rows], [24, 3, 10, 17]) + self.assertEqual([row["score"] for row in rows[1:3]], [0.0, 0.0]) + self.assertLess(rows[-1]["score"], 0) + + def test_directive_filter_and_opt_in_match(self) -> None: + self.seed(50, clusters=1, all_ties=True) + self.assertTrue(all(not row["directive"] for row in self.compare(limit=100))) + self.assertTrue(any(row["directive"] for row in self.compare(limit=100, include_directives=True))) + + def test_missing_vector_table_keeps_zero_score_fallback(self) -> None: + self.seed(20, clusters=1, directives=False) + self.conn.execute("UPDATE vector_cache_registry SET vec_table='missing_synthetic_vectors'") + self.conn.commit() + rows = self.compare(limit=7) + self.assertEqual([row["score"] for row in rows], [0.0] * 7) + + def test_partial_corrupt_vector_chunk_keeps_previous_fallback_behavior(self) -> None: + self.seed(25, clusters=1, directives=False) + self.conn.execute("UPDATE vec_messages SET embedding=X'00' WHERE rowid=24") + self.conn.commit() + self.compare(limit=20) + + def test_no_clusters_returns_empty_after_encoding(self) -> None: + self.assertEqual(self.compare(), []) + self.assertEqual(self.boundary.encodes, 2) + self.assertFalse(self.conn.in_transaction) + + def test_zero_query_preserves_all_score_ties(self) -> None: + self.seed(20, clusters=1) + self.boundary.query = [0.0] * 4 + self.compare(limit=10) + + +class TestClusteredSearchBounds(ClusteredSearchFixture): + def test_large_cluster_bounds_vectors_heap_and_full_content(self) -> None: + self.seed(2107, clusters=1, directives=False, content_size=4096) + observed = ObservedConnection(self.conn) + heap_peak = 0 + arrays_live = 0 + arrays_peak = 0 + original_array = NUMPY.array + + def release_array() -> None: + nonlocal arrays_live + arrays_live -= 1 + + def array(*args: object, **kwargs: object) -> object: + nonlocal arrays_live, arrays_peak + result = original_array(*args, **kwargs) + arrays_live += 1 + arrays_peak = max(arrays_peak, arrays_live) + weakref.finalize(result, release_array) + return result + + def push(heap: list, item: tuple) -> None: + nonlocal heap_peak + heapq.heappush(heap, item) + heap_peak = max(heap_peak, len(heap)) + + def replace(heap: list, item: tuple) -> None: + nonlocal heap_peak + heapq.heapreplace(heap, item) + heap_peak = max(heap_peak, len(heap)) + + self.module.heapq = types.SimpleNamespace(merge=heapq.merge, heappush=push, heapreplace=replace) + with patch.object(NUMPY, "array", array): + result = self.module.search_clustered(observed, "synthetic query", limit=100) + self.assertEqual(len(result), 100) + self.assertEqual(observed.rows["content"], 100) + self.assertEqual(observed.rows["candidate_ids"], 2107) + self.assertLessEqual(max(observed.parameter_counts), 500) + self.assertLessEqual(observed.max_fetchall["vectors"], 500) + self.assertEqual(heap_peak, 100) + self.assertLessEqual(arrays_peak, 505) + self.assertGreater(arrays_peak, 100) + self.assertFalse(self.conn.in_transaction) + + def test_low_sqlite_variable_limit_and_many_selected_clusters(self) -> None: + self.seed(619, clusters=619, all_ties=True, directives=False) + expected = self.module.reference_search_clustered(self.conn, "synthetic query", limit=580, top_clusters=619) + cap = 17 if hasattr(self.conn, "setlimit") else 500 + if hasattr(self.conn, "setlimit"): + old_limit = self.conn.setlimit(sqlite3.SQLITE_LIMIT_VARIABLE_NUMBER, cap) + self.addCleanup(self.conn.setlimit, sqlite3.SQLITE_LIMIT_VARIABLE_NUMBER, old_limit) + observed = ObservedConnection(self.conn, maximum_parameters=cap) + result = self.module.search_clustered(observed, "synthetic query", limit=580, top_clusters=619) + self.assertEqual(result, expected) + self.assertLessEqual(max(observed.parameter_counts), cap) + self.assertEqual(observed.rows["content"], 580) + self.assertGreater(observed.closed, 1) + + def test_negative_limit_materializes_actual_requested_output_only(self) -> None: + self.seed(31, clusters=1, all_ties=True, directives=False) + observed = ObservedConnection(self.conn) + result = self.module.search_clustered(observed, "synthetic query", limit=-7) + self.assertEqual(result, self.module.reference_search_clustered(self.conn, "synthetic query", limit=-7)) + self.assertEqual(observed.rows["content"], 31 - 7) + + def test_legacy_duplicate_memberships_do_not_duplicate_candidates(self) -> None: + self.seed(31, clusters=3, all_ties=True, directives=False) + self.conn.execute("ALTER TABLE message_clusters RENAME TO old_memberships") + self.conn.execute("CREATE TABLE message_clusters(message_id INTEGER,cluster_id INTEGER)") + self.conn.execute("INSERT INTO message_clusters SELECT * FROM old_memberships") + self.conn.execute("INSERT INTO message_clusters SELECT * FROM old_memberships") + self.conn.commit() + self.compare(limit=100) + + +class TestClusteredSearchSnapshots(ClusteredSearchFixture): + def test_concurrent_changes_do_not_mix_scores_and_winner_content(self) -> None: + self.seed(30, clusters=1, all_ties=True, directives=False) + expected = self.module.reference_search_clustered(self.conn, "synthetic query", limit=5) + writer = self.open_db() + observed = ObservedConnection(self.conn) + + def change() -> None: + self.assertTrue(self.conn.in_transaction) + writer.execute("UPDATE messages SET content='synthetic replacement',directive=1 WHERE id=3") + writer.execute("DELETE FROM messages WHERE id=10") + writer.execute("UPDATE vec_messages SET embedding=? WHERE rowid=17", (blob([-1.0, 0.0, 0.0, 0.0]),)) + writer.execute("UPDATE message_clusters SET cluster_id=99 WHERE message_id=24") + writer.execute("UPDATE cluster_centroids SET centroid=?", (blob([-1.0, 0.0, 0.0, 0.0]),)) + writer.commit() + + observed.before_vectors = change + result = self.module.search_clustered(observed, "synthetic query", limit=5) + self.assertEqual(result, expected) + self.assertFalse(self.conn.in_transaction) + self.assertNotEqual(self.module.search_clustered(self.conn, "synthetic query", limit=5), expected) + + def test_caller_transaction_is_not_committed(self) -> None: + self.seed(15, clusters=1, all_ties=True, directives=False) + reader = self.open_db() + before = reader.execute("SELECT content FROM messages WHERE id=3").fetchone() + self.conn.execute("UPDATE messages SET content='synthetic pending' WHERE id=3") + rows = self.module.search_clustered(self.conn, "synthetic query", limit=1) + self.assertEqual(rows[0]["content"], "synthetic pending") + self.assertTrue(self.conn.in_transaction) + self.assertEqual(reader.execute("SELECT content FROM messages WHERE id=3").fetchone(), before) + self.conn.rollback() + self.assertEqual(self.conn.execute("SELECT content FROM messages WHERE id=3").fetchone(), before) + + def test_encoding_finishes_before_owned_snapshot_begins(self) -> None: + self.seed(10, clusters=1) + self.boundary.before_encode = lambda: self.assertFalse(self.conn.in_transaction) + self.module.search_clustered(self.conn, "synthetic query") + + def test_materialization_failure_closes_owned_transaction(self) -> None: + self.seed(10, clusters=1) + observed = ObservedConnection(self.conn) + + def fail() -> None: + raise RuntimeError("synthetic read failure") + + observed.before_content = fail + with self.assertRaisesRegex(RuntimeError, "synthetic read failure"): + self.module.search_clustered(observed, "synthetic query") + self.assertFalse(self.conn.in_transaction) + self.assertGreater(observed.closed, 0) + + def test_cancellation_keeps_caller_work_and_closes_candidate_cursors(self) -> None: + self.seed(10, clusters=1, all_ties=True, directives=False) + self.conn.execute("UPDATE messages SET content='synthetic pending' WHERE id=3") + observed = ObservedConnection(self.conn) + + def cancel() -> None: + raise KeyboardInterrupt("synthetic cancellation") + + observed.before_vectors = cancel + with self.assertRaises(KeyboardInterrupt): + self.module.search_clustered(observed, "synthetic query") + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.conn.execute("SELECT content FROM messages WHERE id=3").fetchone(), ("synthetic pending",)) + self.assertGreater(observed.closed, 0) + self.conn.rollback() + + def test_caller_materialization_failure_preserves_pending_work(self) -> None: + self.seed(10, clusters=1, all_ties=True, directives=False) + self.conn.execute("UPDATE messages SET content='synthetic pending' WHERE id=3") + observed = ObservedConnection(self.conn) + + def fail() -> None: + raise RuntimeError("synthetic read failure") + + observed.before_content = fail + with self.assertRaisesRegex(RuntimeError, "synthetic read failure"): + self.module.search_clustered(observed, "synthetic query") + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.conn.execute("SELECT content FROM messages WHERE id=3").fetchone(), ("synthetic pending",)) + self.conn.rollback() + + def test_nonfinite_scores_fail_without_identifying_source_rows(self) -> None: + self.seed(10, clusters=1, all_ties=True, directives=False) + for target in ("centroid", "embedding", "query"): + with self.subTest(target=target): + self.seed(10, clusters=1, all_ties=True, directives=False) + self.boundary.query = [1.0, 0.0, 0.0, 0.0] + invalid = [float("nan"), 0.0, 0.0, 0.0] + if target == "query": + self.boundary.query = invalid + else: + table = "cluster_centroids" if target == "centroid" else "vec_messages" + self.conn.execute(f"UPDATE {table} SET {target}=?", (blob(invalid),)) + self.conn.commit() + with self.assertRaises(ValueError) as raised: + self.module.search_clustered(self.conn, "synthetic query") + self.assertEqual(str(raised.exception), "Clustered search similarity is nonfinite") + self.assertFalse(self.conn.in_transaction) + + +def benchmark_clustered_search(variant: str, clusters: int) -> dict: + """Prepared Linux/real-NumPy benchmark; invoke one case per fresh process.""" + import gc + import hashlib + import json + import resource + import statistics + import sys + import time + import tracemalloc + + if isinstance(NUMPY, SyntheticNumpy) or sys.platform != "linux": + raise RuntimeError("Benchmark requires explicit real NumPy on the isolated Linux test host") + if variant not in ("reference", "current") or clusters not in (3, 30): + raise ValueError("Benchmark requires a frozen variant and cluster count") + fixture = ClusteredSearchFixture() + fixture.setUp() + try: + fixture.seed(10000, clusters=clusters, random_seed=41, directives=False, content_size=4096) + for table, key, column in (("vec_messages", "rowid", "embedding"), + ("cluster_centroids", "cluster_id", "centroid")): + for identifier, vector in fixture.conn.execute(f"SELECT {key},{column} FROM {table}").fetchall(): + fixture.conn.execute(f"UPDATE {table} SET {column}=? WHERE {key}=?", (vector + bytes(252 * 4), identifier)) + fixture.conn.commit() + fixture.boundary.query = [1.0] + [0.0] * 255 + search = (fixture.module.reference_search_clustered if variant == "reference" + else fixture.module.search_clustered) + samples = [] + fingerprints = set() + for _ in range(7): + gc.collect() + observed = ObservedConnection(fixture.conn, maximum_parameters=10000) + tracemalloc.start() + try: + started = time.perf_counter() + result = search(observed, "synthetic query", limit=100, top_clusters=3) + elapsed = time.perf_counter() - started + _, peak = tracemalloc.get_traced_memory() + finally: + tracemalloc.stop() + assert len(result) == 100 + fingerprints.add(hashlib.sha256(json.dumps(result, sort_keys=True).encode()).hexdigest()) + samples.append({"seconds": elapsed, "python_peak_bytes": peak, + "full_message_rows": observed.rows["content"], + "maximum_parameters": max(observed.parameter_counts)}) + del result + assert len(fingerprints) == 1 + return {"variant": variant, "clusters": clusters, "rows": 10000, "dimensions": 256, + "limit": 100, "top_clusters": 3, "repetitions": 7, + "source_sha256": hashlib.sha256((ROOT / "truememory/clustering.py").read_bytes()).hexdigest(), + "reference_commit": "9b2b6ff", "numpy_version": NUMPY.__version__, + "result_sha256": fingerprints.pop(), "samples": samples, + "median_seconds": statistics.median(sample["seconds"] for sample in samples), + "p95_seconds": sorted(sample["seconds"] for sample in samples)[-1], + "process_high_water_kib": resource.getrusage(resource.RUSAGE_SELF).ru_maxrss, + "memory_note": "Python allocation peak is not all native memory; process HWM includes fixture setup", + "model_execution": False, "synthetic_only": True} + finally: + fixture.doCleanups() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-clustering-health-720.py b/tests/test-clustering-health-720.py new file mode 100644 index 00000000..59190b91 --- /dev/null +++ b/tests/test-clustering-health-720.py @@ -0,0 +1,482 @@ +"""Clustering health with SQLite in memory and synthetic native boundaries.""" + +import ast +import builtins +import json +import logging +import os +import re +import runpy +import sqlite3 +import sys +import threading +import time +import types +import unittest +from pathlib import Path +from unittest.mock import Mock, patch + + +ROOT = Path(__file__).resolve().parents[1] + + +def load_functions(path: str, names: set[str], namespace: dict, *, method: bool = False) -> dict: + tree = ast.parse((ROOT / path).read_text(encoding="utf-8")) + body = next(node.body for node in tree.body if isinstance(node, ast.ClassDef) + and node.name == "TrueMemoryEngine") if method else tree.body + nodes = [ast.ImportFrom(module="__future__", names=[ast.alias(name="annotations")], level=0)] + for node in body: + if isinstance(node, ast.FunctionDef) and node.name in names: + node.decorator_list = [] + nodes.append(node) + exec(compile(ast.fix_missing_locations(ast.Module(body=nodes, type_ignores=[])), path, "exec"), namespace) + return namespace + + +def isolated_builtins(modules: dict) -> dict: + runtime = MAINTENANCE._fixture_modules["tier_switch.runtime"] + + def safe_import(name: str, globals: dict | None = None, locals: dict | None = None, + fromlist: tuple = (), level: int = 0) -> object: + if name in modules: + return modules[name] + if name == "truememory.tier_switch.runtime": + return runtime + if name.startswith("truememory") or name.split(".")[0] in {"torch", "numpy", "hdbscan", "sqlite_vec"}: + raise AssertionError("Unexpected production or native import: " + name) + return builtins.__import__(name, globals, locals, fromlist, level) + return dict(vars(builtins), __import__=safe_import) + + +def load_primitives() -> tuple[types.ModuleType, types.ModuleType]: + loader = runpy.run_path(str(Path(__file__).with_name("test-maintenance-source-revision-753.py"))) + return loader["STORAGE"], loader["MAINTENANCE"] + + +STORAGE, MAINTENANCE = load_primitives() +INSTALLED_DEPENDENCY = MAINTENANCE._installed_dependency + + +class HealthFixture(unittest.TestCase): + def setUp(self) -> None: + self.conn = STORAGE.create_db(":memory:") + self.addCleanup(self.conn.close) + self.conn.execute("CREATE TABLE synthetic_vectors(embedding BLOB)") + self.conn.execute( + "INSERT INTO vector_cache_registry VALUES " + "('edge','synthetic_vectors','synthetic_separation',0,0,'synthetic-model',2,0,0)" + ) + self.conn.commit() + self.vector = types.SimpleNamespace( + _lock=threading.Lock(), EMBEDDING_MODEL="synthetic-model", _embedding_dim=2, + _active_tier_group=lambda: "edge", _active_vec_table=lambda conn: "synthetic_vectors", + _active_sep_table=lambda conn: "synthetic_separation", resolve_tier=lambda: "edge", + _frozen_embedding_target=None, _runtime_policy_tier=None, + ) + self.enter_context(patch.dict(MAINTENANCE._fixture_modules, {"vector_search": self.vector})) + self.versions = {"numpy": "synthetic", "hdbscan": "synthetic", "sqlite_vec": "synthetic"} + self.absent: set[str] = set() + self.probes = [] + + def find_spec(name: str) -> object: + self.probes.append(name) + return None if name in self.absent else object() + + self.enter_context(patch.dict(sys.modules, {"truememory.vector_search": self.vector})) + self.enter_context(patch.object(MAINTENANCE, "_installed_dependency", lambda name, dist: self.versions[name])) + self.enter_context(patch.object(MAINTENANCE, "importlib", types.SimpleNamespace( + util=types.SimpleNamespace(find_spec=find_spec), + import_module=Mock(side_effect=AssertionError("Health imported a module")), + ))) + self.coordinator = MAINTENANCE.MaintenanceCoordinator(None) + self.coordinator.refresh_capabilities() + self.namespace = load_functions("truememory/engine.py", {"get_clustering_health"}, { + "__builtins__": isolated_builtins({"truememory.maintenance": MAINTENANCE}), + }, method=True) + self.engine = types.SimpleNamespace( + conn=self.conn, _write_lock=threading.Lock(), _maintenance_coordinator=self.coordinator, + _maintenance_handle=self.conn, + _ensure_connection=Mock(side_effect=AssertionError("Health opened database")), + _get_maintenance_coordinator=Mock(side_effect=AssertionError("Health initialized scheduler")), + ) + self.engine.get_clustering_health = types.MethodType(self.namespace["get_clustering_health"], self.engine) + + def enter_context(self, context: object) -> object: + result = context.__enter__() + self.addCleanup(context.__exit__, None, None, None) + return result + + def health(self) -> dict: + return self.engine.get_clustering_health() + + def publish(self, count: int = 0, coverage: str = "complete") -> None: + dependency = MAINTENANCE._cluster_schedule_dependency(self.conn, versions=self.versions) + source = MAINTENANCE.read_source_revision(self.conn) + self.conn.execute("BEGIN") + MAINTENANCE.record_layer_success_in_transaction( + self.conn, layer="clusters", dependency=dependency, source=source, + output_count=count, run_generation="synthetic", coverage=coverage, + ) + self.conn.commit() + + +class TestClusteringHealth(HealthFixture): + def test_package_inspection_is_bounded_and_import_free(self) -> None: + versions = Mock(return_value="synthetic") + with patch.object(MAINTENANCE, "_installed_dependency", INSTALLED_DEPENDENCY), \ + patch.object(MAINTENANCE.importlib, "metadata", types.SimpleNamespace(version=versions), create=True): + self.probes.clear() + health = MAINTENANCE.clustering_health() + self.assertEqual(health["state"], "unknown") + self.assertEqual(len(self.probes), 6) + self.assertEqual(versions.call_count, 3) + MAINTENANCE.importlib.import_module.assert_not_called() + + def test_missing_hdbscan_before_any_attempt_is_visible(self) -> None: + self.absent.add("hdbscan") + self.versions["hdbscan"] = None + health = self.health() + self.assertEqual((health["status"], health["availability"], health["outcome"]), + ("degraded", "unavailable", "pending")) + self.assertEqual(health["missing_dependencies"], ["hdbscan"]) + self.assertIn("truememory[clustering]", health["guidance"]) + self.assertEqual(health["dependency_error"], "DependencyMissing") + self.assertFalse(self.coordinator.snapshot()["active"]) + + def test_metadata_failure_does_not_claim_package_is_missing(self) -> None: + self.versions["hdbscan"] = None + health = self.health() + self.assertEqual(health["availability"], "unavailable") + self.assertEqual(health["missing_dependencies"], []) + self.assertIsNone(health["guidance"]) + + def test_disconnected_engine_is_unknown_without_opening_or_scheduling(self) -> None: + self.engine.conn = None + health = self.health() + self.assertEqual((health["status"], health["state"], health["availability"]), + ("degraded", "unknown", "unknown")) + self.engine._ensure_connection.assert_not_called() + self.engine._get_maintenance_coordinator.assert_not_called() + self.assertFalse(self.coordinator.snapshot()["active"]) + + def test_foreground_and_model_busy_remain_distinct_and_do_not_wait(self) -> None: + self.engine._write_lock.acquire() + try: + health = self.health() + finally: + self.engine._write_lock.release() + self.assertEqual((health["state"], health["pending_reason"]), ("deferred", "foreground_busy")) + self.vector._lock.acquire() + try: + health = self.health() + finally: + self.vector._lock.release() + self.assertEqual((health["state"], health["availability"], health["pending_reason"]), + ("deferred", "deferred", "model_busy")) + self.assertFalse(self.conn.in_transaction) + self.assertEqual(self.conn.execute("SELECT outcome FROM maintenance_layers WHERE layer='clusters'").fetchone()[0], + "pending") + + def test_pending_and_successful_empty_are_not_missing_dependency(self) -> None: + pending = self.health() + self.assertEqual((pending["state"], pending["availability"], pending["freshness"]), + ("pending", "available", "untrusted")) + self.publish() + empty = self.health() + self.assertEqual((empty["status"], empty["state"], empty["outcome"], empty["output_count"]), + ("ok", "ready", "success_empty", 0)) + self.assertEqual(empty["freshness"], "current") + + def test_unverified_vector_coverage_cannot_claim_full_health(self) -> None: + self.publish(2, "vector_generation_unverified") + health = self.health() + self.assertEqual((health["status"], health["outcome"], health["output_count"]), ("degraded", "success", 2)) + self.assertEqual((health["coverage"], health["pending_reason"]), + ("vector_generation_unverified", "coverage_unverified")) + self.assertIsNone(health["last_error"]) + + def test_append_and_correction_preserve_prior_count_but_report_pending(self) -> None: + self.publish(2) + self.conn.execute("INSERT INTO messages(id,content) VALUES(1,'synthetic')") + self.conn.commit() + health = self.health() + self.assertEqual((health["state"], health["freshness"], health["output_count"]), + ("pending", "append_pending", 2)) + self.conn.execute("UPDATE messages SET content='synthetic changed' WHERE id=1") + self.conn.commit() + self.assertEqual(self.health()["freshness"], "correction_pending") + + def test_borrowed_transaction_is_never_committed_or_rolled_back(self) -> None: + self.publish() + self.conn.execute("INSERT INTO messages(id,content) VALUES(1,'synthetic pending')") + queries = [] + self.conn.set_trace_callback(queries.append) + health = self.health() + self.conn.set_trace_callback(None) + self.assertTrue(health["pending_caller_commit"]) + self.assertEqual(health["pending_reason"], "pending_caller_commit") + self.assertTrue(self.conn.in_transaction) + self.assertFalse(any(q.strip().upper() in {"COMMIT", "ROLLBACK", "BEGIN"} for q in queries)) + self.conn.rollback() + self.assertEqual(self.conn.execute("SELECT count(*) FROM messages").fetchone()[0], 0) + + def test_runtime_import_failure_remains_degraded_with_installed_packages(self) -> None: + self.publish(3) + self.conn.execute("UPDATE maintenance_layers SET outcome='unavailable',error_category='LayerUnavailableError' " + "WHERE layer='clusters'") + self.conn.commit() + health = self.health() + self.assertEqual((health["status"], health["availability"], health["last_error"], health["output_count"]), + ("degraded", "available", "LayerUnavailableError", 3)) + self.assertIsNone(health["guidance"]) + self.publish(3) + self.assertEqual(self.health()["status"], "ok") + + def test_worker_extension_failure_and_dependency_recovery_are_observed(self) -> None: + dependency = MAINTENANCE._cluster_schedule_dependency(self.conn, versions=self.versions) + failure = MAINTENANCE.make_layer_dependency(1, MAINTENANCE._dependency_parameters(dependency), + available=False, error_category="VectorExtensionUnavailable") + epoch = self.coordinator.capability_snapshot()[0] + self.coordinator._publish_extension_evidence(epoch, failure) + self.assertEqual(self.health()["dependency_error"], "VectorExtensionUnavailable") + self.coordinator.refresh_capabilities() + self.assertEqual(self.health()["availability"], "available") + + def test_errors_are_categorical_and_inspection_is_read_only(self) -> None: + self.publish(3) + self.conn.execute("UPDATE maintenance_layers SET outcome='failed',error_category=? WHERE layer='clusters'", + ("synthetic private text\n/path",)) + self.conn.commit() + queries = [] + self.conn.set_trace_callback(queries.append) + changes = self.conn.total_changes + health = self.health() + self.conn.set_trace_callback(None) + self.assertEqual(health["last_error"], "UnknownError") + self.assertNotIn("synthetic private", json.dumps(health)) + self.assertEqual(self.conn.total_changes, changes) + self.assertFalse(any(q.lstrip().upper().startswith(("INSERT", "UPDATE", "DELETE", "CREATE", "PRAGMA")) + for q in queries)) + self.assertFalse(self.conn.in_transaction) + self.assertFalse(self.coordinator.snapshot()["active"]) + + def test_unavailable_tracking_reports_unknown_and_releases_locks(self) -> None: + with patch.object(MAINTENANCE, "read_layer_states", side_effect=sqlite3.OperationalError("synthetic private")): + health = self.health() + self.assertEqual((health["state"], health["last_error"]), ("unknown", "OperationalError")) + self.assertNotIn("synthetic private", json.dumps(health)) + self.assertFalse(self.conn.in_transaction) + self.assertTrue(self.engine._write_lock.acquire(blocking=False)) + self.engine._write_lock.release() + + def test_rollback_failure_is_categorical_and_releases_engine_lock(self) -> None: + class ReadBoundary: + def __getattr__(boundary, name: str) -> object: + return getattr(self.conn, name) + + def rollback(boundary) -> None: + raise sqlite3.OperationalError("synthetic private rollback") + + self.engine.conn = ReadBoundary() + health = self.health() + self.assertEqual((health["state"], health["last_error"], health["pending_reason"]), + ("unknown", "OperationalError", "inspection_rollback_failed")) + self.assertNotIn("synthetic private", json.dumps(health)) + self.assertTrue(self.engine._write_lock.acquire(blocking=False)) + self.engine._write_lock.release() + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + + def test_missing_package_repair_changes_live_evidence_without_starting_work(self) -> None: + self.absent.add("hdbscan") + self.versions["hdbscan"] = None + self.assertEqual(self.health()["availability"], "unavailable") + self.absent.clear() + self.versions["hdbscan"] = "synthetic-repaired" + self.assertEqual((self.health()["availability"], self.health()["state"]), ("available", "pending")) + self.assertFalse(self.coordinator.snapshot()["active"]) + + +class TestClusteringWarnings(HealthFixture): + def test_borrowed_success_and_rollback_preserve_prior_process_failure(self) -> None: + self.conn.execute("CREATE TABLE synthetic_cluster_output(id INTEGER)") + self.conn.execute("INSERT INTO synthetic_cluster_output VALUES(1)") + self.conn.commit() + self.publish(1) + personality = types.SimpleNamespace(extract_preferences=lambda conn: {}) + for count in (0, 2): + with self.subTest(count=count): + with patch.object(MAINTENANCE.logger, "warning"): + self.coordinator.observe_clustering_outcome("failed", "SyntheticBulkFailure") + dependency = MAINTENANCE._cluster_schedule_dependency(self.conn, versions=self.versions) + + def build(conn: sqlite3.Connection) -> None: + conn.execute("DELETE FROM synthetic_cluster_output") + conn.executemany("INSERT INTO synthetic_cluster_output VALUES(?)", [(i,) for i in range(count)]) + + spec = MAINTENANCE.LayerSpec( + "clusters", "cluster_messages", lambda: dependency, build, + lambda conn: conn.execute("SELECT count(*) FROM synthetic_cluster_output").fetchone()[0], + ) + self.conn.execute("BEGIN") + try: + with patch.object(MAINTENANCE, "engine_layer_specs", return_value=(spec,)), \ + patch.object(MAINTENANCE.importlib, "import_module", return_value=personality, side_effect=None): + report = MAINTENANCE.run_engine_maintenance( + self.conn, self.coordinator, force=True, prepare_extensions=False, allow_caller_transaction=True) + self.assertEqual(report.results[0].output_count, count) + self.assertTrue(report.results[0].pending_caller_commit) + self.assertEqual(self.coordinator.clustering_failure(), ("failed", "SyntheticBulkFailure")) + finally: + self.conn.rollback() + self.assertEqual(self.health()["process_error"], "SyntheticBulkFailure") + self.assertEqual(self.conn.execute("SELECT count(*) FROM synthetic_cluster_output").fetchone()[0], 1) + self.assertEqual(self.health()["output_count"], 1) + with patch.object(MAINTENANCE, "engine_layer_specs", return_value=(spec,)), \ + patch.object(MAINTENANCE.importlib, "import_module", return_value=personality, side_effect=None): + report = MAINTENANCE.run_engine_maintenance( + self.conn, self.coordinator, force=True, prepare_extensions=False) + self.assertFalse(report.results[0].pending_caller_commit) + self.assertIsNone(self.coordinator.clustering_failure()) + + def test_bulk_ingest_failure_is_visible_and_recovery_clears_process_error(self) -> None: + namespace = { + "__builtins__": isolated_builtins({}), "Path": Path, "time": time, "os": os, + "logger": logging.getLogger("synthetic_ingest_health"), + "create_db": lambda path: self.conn, "load_messages_from_file": lambda conn, path: 0, + "init_vec_table": lambda conn: None, "build_vectors": lambda conn: 0, + "build_separation_vectors": lambda conn: 0, + "cluster_messages": Mock(side_effect=ImportError("synthetic private sentinel")), + } + for flag in ("VECTOR", "HYBRID", "CLUSTERING", "PERSONALITY", "STYLE_VEC", "CONSOLIDATION", + "PREDICTIVE", "TEMPORAL"): + namespace["_HAS_" + flag] = flag in {"VECTOR", "CLUSTERING"} + load_functions("truememory/engine.py", {"ingest"}, namespace, method=True) + self.engine.db_path = Path(":memory:") + self.engine.stats = {} + for flag in ("hybrid", "temporal", "salience", "personality", "style_vec", "consolidation", + "predictive", "reranker", "hyde"): + setattr(self.engine, "_has_" + flag, False) + self.engine._get_maintenance_coordinator = lambda: self.coordinator + with self.assertLogs(MAINTENANCE.logger, level=logging.WARNING) as captured: + for _ in range(2): + result = namespace["ingest"](self.engine, "synthetic-unused-input") + self.assertEqual(len(captured.records), 1) + self.assertNotIn("synthetic private sentinel", " ".join(captured.output)) + self.assertIn("ERROR", result["build_clusters"]) + self.assertEqual(self.health()["process_error"], "ImportError") + self.assertEqual(self.health()["status"], "degraded") + namespace["cluster_messages"] = lambda conn: 0 + namespace["ingest"](self.engine, "synthetic-unused-input") + self.assertIsNone(self.health()["process_error"]) + + def test_warnings_are_sanitized_deduplicated_and_clear_after_success(self) -> None: + self.absent.add("hdbscan") + with self.assertLogs(MAINTENANCE.logger, level=logging.WARNING) as captured: + for _ in range(3): + self.coordinator.observe_clustering_outcome("unavailable", "synthetic private\n/path") + self.coordinator.observe_clustering_outcome("success_empty") + self.coordinator.observe_clustering_outcome("unavailable", "DependencyMissing") + self.assertEqual(len(captured.records), 2) + self.assertIn("UnknownError", captured.output[0]) + self.assertNotIn("synthetic private", " ".join(captured.output)) + self.assertTrue(all("truememory[clustering]" in line for line in captured.output)) + + def test_runtime_failure_has_no_false_install_guidance_and_reads_never_warn(self) -> None: + with self.assertLogs(MAINTENANCE.logger, level=logging.WARNING) as captured: + self.coordinator.observe_clustering_outcome("failed", "RuntimeError") + self.assertNotIn("pip install", captured.output[0]) + with patch.object(MAINTENANCE.logger, "warning") as warning: + for _ in range(3): + self.health() + self.coordinator.observe_clustering_outcome("deferred", "ModelBusy") + warning.assert_not_called() + + def test_runner_reports_only_actual_cluster_attempts(self) -> None: + result = MAINTENANCE.LayerResult("clusters", "cluster_messages", "unavailable", None, 0, + "LayerUnavailableError", True) + personality = types.SimpleNamespace(extract_preferences=lambda conn: {}) + with patch.object(MAINTENANCE, "engine_layer_specs", return_value=()), \ + patch.object(MAINTENANCE, "run_layers", return_value=(result,)), \ + patch.object(MAINTENANCE.importlib, "import_module", return_value=personality, side_effect=None), \ + patch.object(self.coordinator, "observe_clustering_outcome") as observed: + MAINTENANCE.run_engine_maintenance(self.conn, self.coordinator, prepare_extensions=False) + observed.assert_called_once_with("unavailable", "LayerUnavailableError") + observed.reset_mock() + with patch.object(MAINTENANCE, "run_layers", return_value=(result._replace(attempted=False),)): + MAINTENANCE.run_engine_maintenance(self.conn, self.coordinator, prepare_extensions=False) + observed.assert_not_called() + + +class TestMCPClusteringHealth(HealthFixture): + def server(self, memory: object | None) -> dict: + modules = { + "truememory.maintenance": MAINTENANCE, + "truememory.engine": types.SimpleNamespace(get_vectors_load_error=lambda: "SyntheticVectorError"), + "truememory.tier_switch.manager": types.SimpleNamespace(RebuildManager=types.SimpleNamespace( + get_instance=lambda: types.SimpleNamespace(get_status=lambda status_id: {"status_id": status_id}))), + } + return load_functions("truememory/mcp_server.py", {"_build_health_payload", "truememory_status", "truememory_stats"}, { + "__builtins__": isolated_builtins(modules), "json": json, "re": re, "_memory": memory, + "_get_memory": Mock(side_effect=AssertionError("Health constructed Memory")), + "_reranker_error_lock": threading.Lock(), "_reranker_last_error": "SyntheticRerankerError", + "_llm_error_lock": threading.Lock(), "_llm_last_error": {"synthetic": "SyntheticLLMError"}, + "_current_llm_provider_name": "synthetic", "_encoding_gate_error_lock": threading.Lock(), + "_encoding_gate_last_error": "SyntheticGateError", "_encoding_gate_degradation_count": 2, + "_build_model_server_health": lambda: {"status": "ok", "running": True}, + "_load_config": lambda: {"tier": "edge"}, "__version__": "synthetic", + "_get_rss_mb": lambda: 0, "_MAX_RSS_MB": 0, + }) + + def test_existing_health_contract_and_values_are_preserved(self) -> None: + server = self.server(types.SimpleNamespace(_engine=self.engine)) + health = server["_build_health_payload"]() + self.assertEqual(health["reranker"], {"status": "degraded", "last_error": "SyntheticRerankerError"}) + self.assertEqual(health["hyde_llm"], {"status": "degraded", "active_provider": "synthetic", + "last_error_by_provider": {"synthetic": "SyntheticLLMError"}}) + self.assertEqual(health["vectors"], {"status": "degraded", "last_error": "SyntheticVectorError"}) + self.assertEqual(health["encoding_gate"], {"status": "degraded", "last_error": "SyntheticGateError", + "degradation_count": 2}) + self.assertEqual(health["model_server"], {"status": "ok", "running": True}) + self.assertTrue(all(item["status"] in {"ok", "degraded"} for item in health.values())) + self.assertIn("clustering", health) + self.assertEqual(health["clustering"]["state"], "pending") + + def test_status_before_memory_initialization_reports_missing_dependency_without_initialization(self) -> None: + self.absent.add("hdbscan") + self.versions["hdbscan"] = None + server = self.server(None) + result = json.loads(server["truememory_status"](17)) + self.assertEqual(result["rebuild"], {"status_id": 17}) + self.assertIn("clustering", result["degradation"]) + self.assertEqual(result["degradation"]["clustering"]["availability"], "unavailable") + self.assertEqual(result["degradation"]["clustering"]["missing_dependencies"], ["hdbscan"]) + server["_get_memory"].assert_not_called() + + def test_stats_tool_includes_the_same_clustering_health(self) -> None: + self.engine._ensure_connection = Mock() + memory = types.SimpleNamespace(_engine=self.engine, stats=lambda: {"message_count": 0}) + server = self.server(memory) + server["_get_memory"] = lambda: memory + result = json.loads(server["truememory_stats"]()) + self.assertEqual(result["health"]["clustering"], self.health()) + self.assertEqual(result["message_count"], 0) + self.engine._ensure_connection.assert_called_once_with() + + def test_clustering_inspection_failure_cannot_hide_existing_subsystems(self) -> None: + server = self.server(types.SimpleNamespace(_engine=self.engine)) + expected = server["_build_health_payload"]() + expected.pop("clustering") + self.engine.get_clustering_health = Mock(side_effect=RuntimeError("synthetic private inspection")) + result = server["_build_health_payload"]() + clustering = result.pop("clustering") + self.assertEqual(result, expected) + self.assertEqual((clustering["status"], clustering["state"], clustering["last_error"]), + ("degraded", "unknown", "RuntimeError")) + self.assertNotIn("synthetic private", json.dumps(clustering)) + result = json.loads(server["truememory_status"]()) + self.assertEqual(result["degradation"]["clustering"], clustering) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-clustering-parameters-721.py b/tests/test-clustering-parameters-721.py new file mode 100644 index 00000000..f4a7f0c0 --- /dev/null +++ b/tests/test-clustering-parameters-721.py @@ -0,0 +1,121 @@ +"""Parameter controls and synthetic HDBSCAN sensitivity, without memory data.""" +from __future__ import annotations + +import ast +import math +import runpy +import sqlite3 +import struct +import sys +import unittest +from pathlib import Path +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] + + +def builder() -> object: + path = ROOT / "truememory/clustering.py" + parsed = ast.parse(path.read_text(encoding="utf-8")) + function = next(node for node in parsed.body if isinstance(node, ast.FunctionDef) and node.name == "cluster_messages") + namespace: dict[str, object] = {"sqlite3": sqlite3} + exec(compile(ast.Module(body=[function], type_ignores=[]), str(path), "exec"), namespace) + return namespace["cluster_messages"] + + +class ClusterParameterControls(unittest.TestCase): + def test_invalid_parameters_fail_before_import_lock_or_database_access(self) -> None: + call = builder() + invalid = [ + {"min_cluster_size": value} for value in [1, 0, -1, True, 2.5, "3", None, 2147483648] + ] + [ + {"min_samples": value} for value in [0, -1, False, 2.5, "2", None, 2147483648] + ] + [ + {"cluster_selection_method": value} for value in ["", "LEAF", "unknown", True, None, []] + ] + with patch.dict(sys.modules, {"hdbscan": None, "truememory": None}): + for kwargs in invalid: + with self.subTest(kwargs=kwargs), self.assertRaises(ValueError): + call(object(), **kwargs) + + def test_default_values_and_keyword_only_method(self) -> None: + import inspect + signature = inspect.signature(builder()) + self.assertEqual(signature.parameters["min_cluster_size"].default, 10) + self.assertEqual(signature.parameters["min_samples"].default, 5) + self.assertEqual(signature.parameters["cluster_selection_method"].default, "eom") + self.assertEqual(signature.parameters["cluster_selection_method"].kind, inspect.Parameter.KEYWORD_ONLY) + + def test_valid_parameters_continue_to_original_dependency_boundary(self) -> None: + call = builder() + with patch.dict(sys.modules, {"hdbscan": None}): + for method in ["eom", "leaf"]: + with self.subTest(method=method), self.assertRaises(ModuleNotFoundError): + call(object(), 2, 1, cluster_selection_method=method) + + def test_actual_constructor_preserves_metric_and_forwards_explicit_leaf(self) -> None: + harness = runpy.run_path(str(ROOT / "tests/test-cluster-publication-749.py")) + module, vector, boundary = harness["load_clustering"]() + with sqlite3.connect(":memory:") as conn: + conn.executescript(harness["SCHEMA"] + module._CLUSTER_SCHEMA) + for mid, values in enumerate([(1, 0), (0, 1), (0, 0), (2, 2), (4, 0)], 1): + conn.execute("INSERT INTO messages VALUES (?, ?, 'source', '', '2020-01-01', '', 'text')", (mid, "synthetic")) + conn.execute("INSERT INTO vec_messages_edge(rowid, embedding) VALUES (?, ?)", (mid, struct.pack("2f", *values))) + conn.execute("INSERT INTO metadata VALUES ('embed_model', 'synthetic-model-a', '')") + conn.execute("INSERT INTO metadata VALUES ('embed_dim', '2', '')") + conn.execute("INSERT INTO vector_cache_registry VALUES ('edge', 'vec_messages_edge', 'vec_messages_sep_edge', 5, 5, 'synthetic-model-a', 2, 1.0, 1.0)") + conn.commit() + self.assertEqual(module.cluster_messages(conn, 3, 2, cluster_selection_method="leaf"), 2) + self.assertEqual(boundary.parameters, [{ + "min_cluster_size": 3, "min_samples": 2, "metric": "euclidean", "cluster_selection_method": "leaf", + }]) + self.assertFalse(conn.in_transaction) + self.assertFalse(vector._lock.locked()) + + def test_scheduled_default_parameter_identity_is_preserved(self) -> None: + parsed = ast.parse((ROOT / "truememory/maintenance.py").read_text(encoding="utf-8")) + function = next(node for node in parsed.body if isinstance(node, ast.FunctionDef) and node.name == "_cluster_schedule_dependency") + assignment = next(node for node in function.body if isinstance(node, ast.Assign) and any(isinstance(target, ast.Name) and target.id == "parameters" for target in node.targets)) + self.assertEqual(ast.literal_eval(assignment.value), { + "min_cluster_size": 10, "min_samples": 5, "metric": "euclidean", "cluster_selection_method": "eom", + }) + + +class SyntheticHdbscanSensitivity(unittest.TestCase): + def test_actual_backend_is_deterministic_across_explicit_policies(self) -> None: + import hdbscan + import numpy as np + + rng = np.random.default_rng(721) + angles = [] + for parent in [0.0, 2 * math.pi / 3, 4 * math.pi / 3]: + for offset in [-0.12, 0.0, 0.12]: + angles.extend(parent + offset + rng.normal(0.0, 0.008, 15)) + values = np.column_stack((np.cos(angles), np.sin(angles))).astype(np.float32) + observations = [] + for size, samples, method in [(10, 5, "eom"), (5, 3, "leaf")]: + kwargs = { + "min_cluster_size": size, "min_samples": samples, + "metric": "euclidean", "cluster_selection_method": method, + "core_dist_n_jobs": 1, + } + first = hdbscan.HDBSCAN(**kwargs).fit_predict(values) + second = hdbscan.HDBSCAN(**kwargs).fit_predict(values) + self.assertEqual(first.tolist(), second.tolist()) + self.assertEqual(len(first), 135) + self.assertTrue(np.isfinite(first).all()) + groups = len(set(first.tolist()) - {-1}) + self.assertGreaterEqual(groups, 3) + self.assertLessEqual(groups, 27) + observations.append(groups) + self.assertGreaterEqual(observations[1], observations[0]) + + +if __name__ == "__main__": + if "--stdlib-controls" in sys.argv: + sys.argv.remove("--stdlib-controls") + suite = unittest.defaultTestLoader.loadTestsFromTestCase(ClusterParameterControls) + outcome = unittest.TextTestRunner().run(suite) + raise SystemExit(0 if outcome.wasSuccessful() else 1) + unittest.main() diff --git a/tests/test-codex-projection-762.py b/tests/test-codex-projection-762.py new file mode 100644 index 00000000..31a55938 --- /dev/null +++ b/tests/test-codex-projection-762.py @@ -0,0 +1,473 @@ +"""Inert synthetic controls for the pinned private Codex projection.""" +from __future__ import annotations + +import builtins +import copy +import json +from pathlib import Path +import sys +import types +import unittest +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] +TURN = "synthetic-projection-turn" + + +def load_parser() -> types.ModuleType: + module = types.ModuleType("synthetic_codex_projection_parser") + + def safe_import(name: str, globals: dict | None = None, locals: dict | None = None, + fromlist: tuple = (), level: int = 0) -> object: + if name.startswith(("truememory", "torch", "numpy", "sentence_transformers")): + raise AssertionError("Application imports are forbidden") + return builtins.__import__(name, globals, locals, fromlist, level) + + module.__dict__["__builtins__"] = dict(vars(builtins), __import__=safe_import) + source = (ROOT / "truememory/ingest/transcript.py").read_text(encoding="utf-8") + with patch.dict(sys.modules, {module.__name__: module}): + exec(compile(source, "synthetic-projection-parser", "exec"), module.__dict__) + return module + + +def event(kind: str, **fields: object) -> dict: + return {"type": "event_msg", "payload": {"type": kind, **fields}} + + +def response(parts: tuple[str, ...], phase: str | None = None) -> dict: + payload = {"type": "message", "role": "assistant", "id": "synthetic-item", + "content": [{"type": "output_text", "text": text} for text in parts], + "internal_chat_message_metadata_passthrough": {"turn_id": TURN}} + if phase is not None: + payload["phase"] = phase + return {"type": "response_item", "timestamp": "2020-01-01T00:00:00Z", "payload": payload} + + +def context(mode: str = "plan", turn: str = TURN, **settings: object) -> dict: + return {"type": "turn_context", "payload": {"turn_id": turn, + "collaboration_mode": {"mode": mode, "settings": {"model": "synthetic-model", **settings}}}} + + +def plan_event(text: str, **fields: object) -> dict: + return event("item_completed", turn_id=TURN, thread_id="synthetic-thread", + item={"type": "Plan", "id": TURN + "-plan", "text": text}, **fields) + + +def plan_start(timestamp: object = -1) -> dict: + return event("item_started", turn_id=TURN, thread_id="synthetic-thread", started_at_ms=timestamp, + item={"type": "Plan", "id": TURN + "-plan", "text": ""}) + + +def lifecycle(parts: tuple[str, ...], visible: str | None, *, mode: object = "plan", + paginated: bool = False, phase: str | None = None) -> list[dict]: + fields: dict = {"turn_id": TURN} + if mode is not None: + fields["collaboration_mode_kind"] = mode + values = [event("task_started", **fields)] + if visible is not None: + if paginated: + item = {"type": "AgentMessage", "id": "synthetic-item", + "content": [{"type": "Text", "text": visible}]} + if phase is not None: + item["phase"] = phase + values.append(event("item_completed", turn_id=TURN, + thread_id="synthetic-thread", item=item)) + else: + values.append(event("agent_message", message=visible, phase=phase)) + values.extend([response(parts, phase), + event("task_complete", turn_id=TURN, last_agent_message=visible)]) + return values + + +class ProjectionGrammarTests(unittest.TestCase): + def setUp(self) -> None: + self.parser = load_parser() + + def outcome(self, records: list[dict]) -> object: + text = "\n".join(json.dumps(value) for value in records) + with patch.object(self.parser, "_transcript_path", return_value=None): + result = self.parser.parse_transcript_outcome(text) + messages = self.parser.parse_transcript(text) + self.assertEqual(messages, result.messages) + return result + + def check_complete(self, parts: tuple[str, ...], visible: str | None, **fields: object) -> None: + result = self.outcome(lifecycle(parts, visible, **fields)) + self.assertEqual(result.status, "complete", result.error_categories) + self.assertEqual(result.error_categories, ()) + public = "\n".join(value for value in parts if value).strip() + self.assertEqual([value.content for value in result.messages], [public] if public else []) + if result.messages: + self.assertEqual(result.messages[0].timestamp, "2020-01-01T00:00:00Z") + + def test_citation_boundaries_cross_blocks(self) -> None: + parts = (" xhiddeny ") + for paginated in (False, True): + self.check_complete(parts, " xy ", mode="default", paginated=paginated) + + def test_citation_literal_nonnested_first_close(self) -> None: + self.check_complete(("axyz",), + "az", mode="default") + + def test_open_citation_hides_through_eof(self) -> None: + self.check_complete(("visiblehidden None: + text = "" + self.check_complete((text,), text, mode="default") + + def test_citation_only_has_no_invented_agent(self) -> None: + self.check_complete(("hidden",), None) + + def test_multiple_citations_preserve_surrounding_text(self) -> None: + self.check_complete(("a1b2c",), + "abc", mode=None) + + def test_whole_line_plan_known_mode(self) -> None: + parts = ("before\n\nstep\n\nafter") + for paginated in (False, True): + self.check_complete(parts, "before\nafter", paginated=paginated) + + def test_plan_only_and_open_at_eof(self) -> None: + self.check_complete(("\nstep\n",), None) + + def test_plan_crlf_and_surviving_endings_exact(self) -> None: + self.check_complete(("before\r\n \t\r\nstep\r\n\r\nafter\r\n",), + "before\r\nafter\r\n") + + def test_plan_rust_white_space(self) -> None: + for marker in ("\u0085", "\u00a0", "\u2007", "\u2028", "\u3000"): + with self.subTest(marker=repr(marker)): + text = f"before\n{marker}{marker}\nstep\n{marker}{marker}\nafter" + self.check_complete((text,), "before\nafter") + + def test_python_extra_whitespace_is_literal(self) -> None: + for marker in ("\x1c", "\x1d", "\x1e", "\x1f"): + text = f"{marker}{marker}" + self.check_complete((text,), text, mode="default") + + def test_inline_and_partial_plan_tags_visible(self) -> None: + for text in ("xyz", " extra"): + self.check_complete((text,), text, mode="default") + + def test_nested_plan_open_is_literal_hidden_body(self) -> None: + self.check_complete(("before\n\n\nstep\n\nafter\n",), + "before\nafter\n") + + def test_multiple_plan_blocks(self) -> None: + self.check_complete(("a\n\n1\n\nb\n\n2\n\nc",), + "a\nb\nc") + + def test_citation_then_plan_order(self) -> None: + self.check_complete(("a\nhidden\nstep\n\nb",), + "a\nb") + self.check_complete(("a\n\nstep\n\nb",), + "ab", mode=None) + + def test_default_unknown_plan_scope_remains_partial(self) -> None: + text = "before\n\nstep\n\nafter" + for mode in ("default", None, "unknown", True): + for echo in (text, "before\nafter"): + result = self.outcome(lifecycle((text,), echo, mode=mode)) + self.assertEqual(result.status, "partial") + self.assertIn("codex_unsupported_projection", result.error_categories) + self.assertEqual(result.messages[0].content, text) + + def test_mode_does_not_leak_across_lifecycles(self) -> None: + text = "before\n\nstep\n\nafter" + records = lifecycle(("plain",), "plain", mode="plan") + records.extend(lifecycle((text,), text, mode=None)) + result = self.outcome(records) + self.assertEqual(result.status, "partial") + self.assertIn("codex_unsupported_projection", result.error_categories) + + def test_wrong_echo_and_terminal_never_fit_alternatives(self) -> None: + text = "ahiddenb" + result = self.outcome(lifecycle((text,), text)) + self.assertEqual(result.status, "partial") + self.assertIn("codex_unmatched_conversation_event", result.error_categories) + self.assertIn("codex_unmatched_terminal", result.error_categories) + + def test_terminal_all_phases_use_same_exact_projection(self) -> None: + for phase in (None, "commentary", "partial_answer", "final_answer"): + self.check_complete(("ahiddenb",), "ab", phase=phase) + + def test_unknown_tags_not_stripped(self) -> None: + text = "ahiddenb" + self.check_complete((text,), text) + + def test_unexplained_structured_channels_remain_partial(self) -> None: + records = lifecycle(("ahiddenb",), "ab") + records[1]["payload"]["memory_citation"] = {"entries": [], "rolloutIds": ["synthetic"]} + self.assertEqual(self.outcome(records).status, "partial") + records = lifecycle(("\nstep\n",), None) + records.insert(1, event("item_completed", turn_id=TURN, thread_id="synthetic-thread", + item={"type": "Plan", "id": TURN + "-plan", "text": "wrong"})) + self.assertEqual(self.outcome(records).status, "partial") + + def test_exact_citation_object_legacy_and_paginated(self) -> None: + body = "\nsynthetic/path:+9-+2|note=[ exact ]\r\n\na\nb\na\n" + citation = {"entries": [{"path": "synthetic/path", "lineStart": 9, "lineEnd": 2, "note": "exact"}], + "rolloutIds": ["a", "b"]} + parts = ("a" + body + "b") + for paginated in (False, True): + records = lifecycle(parts, "ab", paginated=paginated) + target = records[1]["payload"]["item"] if paginated else records[1]["payload"] + target["memory_citation"] = citation + result = self.outcome(records) + self.assertEqual(result.status, "complete", result.error_categories) + self.assertEqual(result.messages[0].content, "\n".join(parts)) + + def test_citation_opaque_values_order_and_first_complete_blocks(self) -> None: + body1 = "p:00000000000001-4294967295|note=[n]wrong:1-2|note=[x]x\r\ny\u2028z\n" + body2 = "bad:-1-2|note=[x]\np:4294967296-1|note=[x]\np:1_0-2|note=[x]\np:١-2|note=[x]\np:1-2|note=[n]y\u2028z\nxunused" + fingerprint = self.parser._codex_derive_citation([body1, body2]) + self.assertEqual(fingerprint, ((("p", 1, 4294967295, "n"), ("p", 1, 2, "n")), ("x", "y\u2028z"))) + self.assertEqual(self.parser._codex_u32("+" + "0" * 5000 + "1"), 1) + + def test_citation_id_fallback_and_rust_trim(self) -> None: + body = "incomplete \u3000x\u3000 \ny\n" + self.assertEqual(self.parser._codex_derive_citation([body]), ((), ("x", "y"))) + self.assertEqual(self.parser._codex_derive_citation(["x"]), None) + self.assertEqual(self.parser._codex_derive_citation(["\x1cx\x1c"]), ((), ("\x1cx\x1c",))) + + def test_supplied_citation_shapes_and_mismatch_reject(self) -> None: + valid = {"entries": [{"path": "p", "lineStart": 1, "lineEnd": 2, "note": "n"}], "rolloutIds": []} + body = "p:1-2|note=[n]" + invalid = [[], {}, {"entries": []}, {"entries": [], "rolloutIds": [], "extra": True}, + {"entries": [], "rolloutIds": []}, {"entries": [], "rolloutIds": [True]}] + for key, value in (("lineStart", True), ("lineEnd", -1), ("lineEnd", 4294967296), ("note", "wrong")): + item = copy.deepcopy(valid) + item["entries"][0][key] = value + invalid.append(item) + for citation in invalid: + records = lifecycle(("a" + body + "b",), "ab") + records[1]["payload"]["memory_citation"] = citation + result = self.outcome(records) + self.assertEqual(result.status, "partial", citation) + self.assertIn("codex_unsupported_projection", result.error_categories) + + def test_citation_absent_null_is_optional_compatibility(self) -> None: + text = "asynthetic-idb" + for supplied in (False, True): + records = lifecycle((text,), "ab") + if supplied: + records[1]["payload"]["memory_citation"] = None + self.assertEqual(self.outcome(records).status, "complete") + records = lifecycle(("plain",), "plain") + records[1]["payload"]["memory_citation"] = {"entries": [], "rolloutIds": []} + self.assertEqual(self.outcome(records).status, "partial") + + def test_equal_visible_citation_credits_are_metadata_selective(self) -> None: + records = [event("task_started", turn_id=TURN, collaboration_mode_kind="plan")] + for value in ("a", "b"): + records.append(response(("same" + value + "",))) + for value in ("b", "a"): + records.append(event("agent_message", message="same", memory_citation={"entries": [], "rolloutIds": [value]})) + result = self.outcome(records) + self.assertEqual(result.status, "partial") + self.assertEqual(result.unrecognized_records, 1) + + def test_context_plan_proof_precedes_supported_candidates(self) -> None: + text = "before\n\nstep\n\nafter" + records = lifecycle((text,), "before\nafter", mode=None) + records.insert(1, context(reasoning_effort="synthetic-custom", developer_instructions=None)) + self.assertEqual(self.outcome(records).status, "complete") + + def test_context_future_previous_and_foreign_turn_not_borrowed(self) -> None: + text = "before\n\nstep\n\nafter" + for place, value in ((-1, context()), (0, context()), (1, context(turn="other"))): + records = lifecycle((text,), "before\nafter", mode=None) + records.insert(place, value) + result = self.outcome(records) + self.assertEqual(result.status, "partial") + self.assertIn("codex_unsupported_projection", result.error_categories) + + def test_context_conflict_cannot_choose_favorable_mode(self) -> None: + text = "before\n\nstep\n\nafter" + for place in (1, -1): + records = lifecycle((text,), "before\nafter", mode="plan") + records.insert(place, context("default")) + self.assertEqual(self.outcome(records).status, "partial") + records = lifecycle((text,), "before\nafter", mode="default") + records.insert(1, context()) + self.assertEqual(self.outcome(records).status, "partial") + + def test_foreign_unscoped_context_cannot_override_known_start_plan(self) -> None: + text = "before\n\nstep\n\nafter" + foreign = context("default", turn="foreign") + unknown = context("default") + unknown["payload"].pop("turn_id") + malformed = context(turn="foreign", reasoning_effort=True) + for value in (foreign, unknown, malformed): + with self.subTest(value=value): + records = lifecycle((text,), "before\nafter", mode="plan") + records.insert(1, value) + self.assertEqual(self.outcome(records).status, "complete") + records = lifecycle((text,), "before\nafter", mode=None) + records.insert(1, value) + self.assertEqual(self.outcome(records).status, "partial") + + def test_explicit_invalid_mode_cannot_be_replaced_by_context(self) -> None: + text = "\nstep\n" + for mode in (True, "unknown", [], {}): + records = lifecycle((text,), None, mode=mode) + records.insert(1, context()) + self.assertEqual(self.outcome(records).status, "partial") + + def test_context_required_and_optional_shapes(self) -> None: + text = "\nstep\n" + for settings in ({}, {"reasoning_effort": None}, {"reasoning_effort": "custom"}, + {"developer_instructions": "synthetic"}): + records = lifecycle((text,), None, mode=None) + records.insert(1, context(**settings)) + self.assertEqual(self.outcome(records).status, "complete") + for settings in ({"model": True}, {"reasoning_effort": True}, {"reasoning_effort": ""}, + {"developer_instructions": False}, {"unknown": "x"}): + records = lifecycle((text,), None, mode=None) + records.insert(1, context(**settings)) + self.assertEqual(self.outcome(records).status, "partial") + + def test_context_validation_only_material_to_plan(self) -> None: + for value in (context(turn="foreign"), context(reasoning_effort=True), context("default"), + {"type": "turn_context", "payload": {"collaboration_mode": "unknown"}}): + records = lifecycle(("plain",), "plain") + records.insert(1, value) + self.assertEqual(self.outcome(records).status, "complete") + self.assertEqual(self.outcome([value]).status, "complete") + + def test_typed_plan_exact_body_separate_from_visible_echo(self) -> None: + text = "before\n\nstep\n\nafter" + records = lifecycle((text,), "before\nafter") + records.insert(1, plan_event("step\n")) + result = self.outcome(records) + self.assertEqual(result.status, "complete", result.error_categories) + self.assertEqual([value.content for value in result.messages], [text]) + + def test_typed_plan_last_body_exact_crlf_eof_and_empty(self) -> None: + for text, body in (("\nfirst\n\n\r\nsecond\r\n", "second\r\n"), + ("\nlast", "last"), + ("\n", "")): + records = lifecycle((text,), None) + records.insert(1, plan_event(body)) + self.assertEqual(self.outcome(records).status, "complete") + records[1]["payload"]["item"]["text"] = body + "wrong" + self.assertEqual(self.outcome(records).status, "partial") + + def test_typed_plan_body_citations_after_plan_extraction(self) -> None: + text = "\n\nstep\n\n" + records = lifecycle((text,), None) + records.insert(1, plan_event("step\n")) + self.assertEqual(self.outcome(records).status, "complete") + text = "\nstephidden\n" + records = lifecycle((text,), None) + records.insert(1, plan_event("step\n")) + self.assertEqual(self.outcome(records).status, "complete") + + def test_typed_plan_output_only_not_input_text(self) -> None: + text = "\nstep\n" + records = lifecycle((text,), None) + records[-2]["payload"]["content"][0]["type"] = "input_text" + records.insert(1, plan_event("step\n")) + self.assertEqual(self.outcome(records).status, "partial") + + def test_typed_plan_cannot_supply_canonical_or_agent_credit(self) -> None: + self.assertEqual(self.outcome([event("task_started", turn_id=TURN, collaboration_mode_kind="plan"), + plan_event("step\n")]).status, "partial") + records = lifecycle(("\nstep\n",), None) + records.insert(1, plan_event("step\n")) + records.insert(2, event("agent_message", message="step\n")) + self.assertEqual(self.outcome(records).status, "partial") + + def test_typed_plan_occurrence_credit_once_and_scope_boundaries(self) -> None: + text = "\nstep\n" + records = lifecycle((text,), None) + records.insert(1, plan_event("step\n")) + records.insert(2, plan_event("step\n")) + self.assertEqual(self.outcome(records).status, "partial") + records = lifecycle((text,), None) + records.append(plan_event("step\n")) + self.assertEqual(self.outcome(records).status, "partial") + records = lifecycle((text,), None, mode=None) + records.insert(1, plan_event("step\n")) + records.insert(2, context()) + self.assertEqual(self.outcome(records).status, "partial") + + def test_typed_plan_wire_shape_and_time_types(self) -> None: + text = "\nstep\n" + for fields in ({}, {"started_at_ms": None}, {"started_at_ms": -(2**63), "completed_at_ms": 2**63 - 1}): + records = lifecycle((text,), None) + records.insert(1, plan_event("step\n", **fields)) + self.assertEqual(self.outcome(records).status, "complete") + for fields in ({"completed_at_ms": None}, {"started_at_ms": True}, + {"completed_at_ms": 2**63}, {"unknown": "x"}): + records = lifecycle((text,), None) + records.insert(1, plan_event("step\n", **fields)) + self.assertEqual(self.outcome(records).status, "partial") + for key, value in (("id", "wrong"), ("text", []), ("unknown", "x")): + records = lifecycle((text,), None) + item = plan_event("step\n") + item["payload"]["item"][key] = value + records.insert(1, item) + self.assertEqual(self.outcome(records).status, "partial") + + def test_plan_start_is_supported_context_without_credit(self) -> None: + text = "before\n\nstep\n\nafter" + records = lifecycle((text,), "before\nafter") + records.insert(1, plan_start()) + records.insert(2, plan_event("step\n")) + result = self.outcome(records) + self.assertEqual(result.status, "complete", result.error_categories) + self.assertEqual([value.content for value in result.messages], [text]) + records = lifecycle((text,), "before\nafter") + records.insert(1, plan_start(-(2**63))) + self.assertEqual(self.outcome(records).status, "complete") + + def test_plan_start_cannot_create_canonical_evidence(self) -> None: + records = [event("task_started", turn_id=TURN, collaboration_mode_kind="plan"), plan_start()] + self.assertEqual(self.outcome(records).status, "partial") + records = lifecycle(("plain",), "plain") + records.insert(1, plan_start()) + self.assertEqual(self.outcome(records).status, "partial") + + def test_plan_start_scope_mode_and_replay_are_strict(self) -> None: + text = "\nstep\n" + for mode in (None, "default"): + records = lifecycle((text,), None, mode=mode) + records.insert(1, plan_start()) + self.assertEqual(self.outcome(records).status, "partial") + records = lifecycle((text,), None) + records.insert(1, plan_start()) + records.insert(2, plan_start()) + self.assertEqual(self.outcome(records).status, "partial") + records = lifecycle((text,), None) + records.extend([plan_start()]) + self.assertEqual(self.outcome(records).status, "partial") + records = lifecycle((text,), None) + records.insert(1, plan_event("step\n")) + records.insert(2, plan_start()) + self.assertEqual(self.outcome(records).status, "partial") + + def test_plan_start_required_signed_timestamp_and_empty_shape(self) -> None: + text = "\nstep\n" + for timestamp in (None, True, "1", -(2**63)-1, 2**63): + records = lifecycle((text,), None) + records.insert(1, plan_start(timestamp)) + self.assertEqual(self.outcome(records).status, "partial") + for remove in ("started_at_ms", "thread_id", "turn_id"): + records = lifecycle((text,), None) + value = plan_start() + value["payload"].pop(remove) + records.insert(1, value) + self.assertEqual(self.outcome(records).status, "partial") + for key, value in (("text", "step"), ("id", "wrong"), ("unknown", "x")): + records = lifecycle((text,), None) + item = plan_start() + item["payload"]["item"][key] = value + records.insert(1, item) + self.assertEqual(self.outcome(records).status, "partial") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-codex-rollout-762.py b/tests/test-codex-rollout-762.py new file mode 100644 index 00000000..640d3563 --- /dev/null +++ b/tests/test-codex-rollout-762.py @@ -0,0 +1,693 @@ +"""Synthetic native rollout contracts, without importing the application package.""" +from __future__ import annotations + +import ast +import builtins +import copy +import errno +import io +import json +from pathlib import Path +import sys +import types +import unittest +from bisect import bisect_left +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] +SENTINEL = "synthetic-private-content" +SCAFFOLD = "synthetic-scaffold-content" +TURN = "synthetic-turn" + + +def load_parser(source: str | None = None) -> types.ModuleType: + module = types.ModuleType("synthetic_codex_parser") + if source is None: + source = (ROOT / "truememory/ingest/transcript.py").read_text(encoding="utf-8") + + def safe_import(name: str, globals: dict | None = None, locals: dict | None = None, + fromlist: tuple = (), level: int = 0) -> object: + if name.startswith(("truememory", "torch", "numpy", "sentence_transformers")): + raise AssertionError("Application imports are forbidden in parser tests") + return builtins.__import__(name, globals, locals, fromlist, level) + + module.__dict__["__builtins__"] = dict(vars(builtins), __import__=safe_import) + with patch.dict(sys.modules, {module.__name__: module}): + exec(compile(source, "synthetic-parser", "exec"), module.__dict__) + return module + + +def response(role: str = "user", parts: tuple[str, ...] = (SENTINEL,), *, + turn: str | None = TURN, phase: str | None = None, item: str = "synthetic-item") -> dict: + payload = {"type": "message", "role": role, "id": item, + "content": [{"type": "input_text" if role == "user" else "output_text", "text": text} + for text in parts]} + if turn is not None: + payload["internal_chat_message_metadata_passthrough"] = {"turn_id": turn} + if phase is not None: + payload["phase"] = phase + return {"type": "response_item", "timestamp": "2026-01-01T00:00:00Z", "payload": payload} + + +def event(kind: str, **fields: object) -> dict: + return {"type": "event_msg", "payload": {"type": kind, **fields}} + + +def start(turn: str = TURN) -> dict: + return event("task_started", turn_id=turn, collaboration_mode_kind="default") + + +def terminal(text: object = "done", turn: str = TURN) -> dict: + return event("task_complete", turn_id=turn, last_agent_message=text) + + +def completed(role: str, text: str, *, turn: str = TURN, + item: str = "synthetic-item", phase: str | None = None) -> dict: + value = {"type": "UserMessage" if role == "user" else "AgentMessage", "id": item, + "content": [{"type": "text" if role == "user" else "Text", "text": text}]} + if phase is not None: + value["phase"] = phase + return event("item_completed", turn_id=turn, thread_id="synthetic-thread", item=value) + + +def lifecycle(*, paginated: bool = False, parts: tuple[str, ...] = ("done",), + phase: str | None = None, turn: str = TURN) -> list[dict]: + user = response(turn=turn) + assistant = response("assistant", parts, turn=turn, phase=phase) + if paginated: + user_echo = completed("user", SENTINEL, turn=turn) + agent_echo = completed("assistant", "".join(parts), turn=turn, phase=phase) + else: + user_echo = event("user_message", message=SENTINEL, images=[], audio=None) + agent_echo = event("agent_message", message="".join(parts), phase=phase) + return [start(turn), user, user_echo, agent_echo, assistant, terminal("".join(parts), turn)] + + +def annotation(kind: str, **fields: object) -> dict: + return {"provenance": {"type": kind, **fields}} + + +def annotate(record: dict, annotations: object = None, kinds: object = None) -> dict: + metadata = record["payload"].setdefault("internal_chat_message_metadata_passthrough", {}) + metadata.update(content_item_metadata=annotations, content_item_kinds=kinds) + return record + + +def load_stop_count(parser: types.ModuleType, text: str, source: str | None = None) -> tuple[object, list[Path]]: + source = source or (ROOT / "truememory/ingest/hooks/stop.py").read_text(encoding="utf-8") + node = next(node for node in ast.parse(source).body + if isinstance(node, ast.FunctionDef) and node.name == "_has_enough_messages") + calls: list[Path] = [] + + def parse(path: Path) -> object: + if not isinstance(path, Path): + raise AssertionError("Stop must select the explicit path contract") + calls.append(path) + return parser._parse_transcript_text_outcome(text.strip()) + + fake = types.SimpleNamespace(parse_transcript_outcome=parse, strip_truememory_blocks=parser.strip_truememory_blocks) + + def safe_import(name: str, globals: dict | None = None, locals: dict | None = None, + fromlist: tuple = (), level: int = 0) -> object: + if name == "truememory.ingest.transcript": + return fake + raise AssertionError("Stop count imported an unexpected module") + + namespace = {"Path": Path, "json": json, "__builtins__": dict(vars(builtins), __import__=safe_import)} + exec(compile(ast.Module(body=[node], type_ignores=[]), "synthetic-stop", "exec"), namespace) + return namespace["_has_enough_messages"], calls + + +def counted_reconciliation(parser: types.ModuleType, records: list[dict]) -> tuple[object, int]: + """Count key operations and binary-search reads in the actual outcome path.""" + operations = [0] + + class CountedText(str): + def __hash__(self) -> int: + operations[0] += 1 + return super().__hash__() + + def __eq__(self, other: object) -> bool: + operations[0] += 1 + return super().__eq__(other) + + decode = parser._decode_codex_record + + def counted_decode(entry: dict) -> object: + result = decode(entry) + if result is not None: + for key in ("projection", "phase", "turn_id", "item_id"): + value = getattr(result, key) + if isinstance(value, str): + setattr(result, key, CountedText(value)) + return result + + def counted_bisect(values: list[int], target: int) -> int: + class Positions: + def __len__(self) -> int: + return len(values) + + def __getitem__(self, index: int) -> int: + operations[0] += 1 + return values[index] + + return bisect_left(Positions(), target) + + with patch.object(parser, "_decode_codex_record", counted_decode), \ + patch.object(parser, "bisect_left", counted_bisect, create=True): + result = parser._parse_transcript_text_outcome(json.dumps(records)) + return result, operations[0] + + +class NativeRolloutTests(unittest.TestCase): + def setUp(self) -> None: + self.parser = load_parser() + + def check(self, records: list, expected: list[tuple[str, str]], status: str = "complete", + malformed: int = 0, unknown: int = 0) -> object: + for text in (json.dumps(records), "\n".join(json.dumps(record) for record in records)): + with patch.object(Path, "stat", side_effect=FileNotFoundError(errno.ENOENT, "synthetic missing")): + messages = self.parser.parse_transcript(text) + outcome = self.parser.parse_transcript_outcome(text) + self.assertEqual([(message.role, message.content) for message in messages], expected) + self.assertEqual(outcome.messages, messages) + self.assertEqual((outcome.status, outcome.total_records, outcome.malformed_records, + outcome.unrecognized_records), (status, len(records), malformed, unknown)) + self.assertNotIn(SENTINEL, repr(outcome)) + self.assertNotIn(TURN, repr(outcome)) + self.assertNotIn(SCAFFOLD, repr(outcome)) + return outcome + + def test_full_legacy_and_paginated_lifecycles(self) -> None: + for paginated in (False, True): + with self.subTest(paginated=paginated): + result = self.check(lifecycle(paginated=paginated), [("human", SENTINEL), ("assistant", "done")]) + self.assertEqual(self.parser.format_for_extraction(result.messages), + "User: " + SENTINEL + "\n\nAssistant: done") + + def test_original_blocks_and_exact_private_projection(self) -> None: + for parts in (("alpha", "beta"), (" a", "b "), ("", "alpha", "", "beta")): + for paginated in (False, True): + with self.subTest(parts=parts, paginated=paginated): + self.check(lifecycle(parts=parts, paginated=paginated), + [("human", SENTINEL), ("assistant", "\n".join(p for p in parts if p).strip())]) + records = lifecycle(parts=(" a", "b ")) + records[-1] = terminal("ab") + self.check(records, [("human", SENTINEL), ("assistant", "a\nb")], "partial", unknown=1) + records[-1] = terminal(" a\nb ") + self.check(records, [("human", SENTINEL), ("assistant", "a\nb")], "partial", unknown=1) + + def test_each_phase_and_blank_tail_preserve_summary(self) -> None: + for phase in (None, "commentary", "partial_answer", "final_answer"): + with self.subTest(phase=phase): + records = lifecycle(phase=phase) + records[-1:-1] = [event("agent_message", message=" \n"), response("assistant", (" \n",))] + self.check(records, [("human", SENTINEL), ("assistant", "done")]) + + def test_duplicate_occurrences_and_ids_are_not_deduplicated(self) -> None: + records = lifecycle() + records[-1:-1] = [event("agent_message", message="done"), response("assistant", ("done",))] + self.check(records, [("human", SENTINEL), ("assistant", "done"), ("assistant", "done")]) + self.check([response()] * 5, [("human", SENTINEL)] * 5) + + def test_extra_echo_cannot_borrow_terminal_credit(self) -> None: + for paginated in (False, True): + records = lifecycle(paginated=paginated) + records.insert(-1, copy.deepcopy(records[3])) + self.check(records, [("human", SENTINEL), ("assistant", "done")], "partial", unknown=1) + + def test_per_original_block_echoes_are_not_ordinary_finalization(self) -> None: + records = lifecycle(parts=("alpha", "beta")) + records[3:4] = [event("agent_message", message="alpha"), event("agent_message", message="beta")] + self.check(records, [("human", SENTINEL), ("assistant", "alpha\nbeta")], "partial", unknown=2) + + def test_missing_echoes_and_monotonic_occurrence_matching(self) -> None: + records = [start(), response("assistant", ("a",)), response("assistant", ("b",)), + event("agent_message", message="b"), terminal("b")] + self.check(records, [("assistant", "a"), ("assistant", "b")]) + records.insert(-1, event("agent_message", message="a")) + self.check(records, [("assistant", "a"), ("assistant", "b")], "partial", unknown=1) + + def test_summary_is_not_last_row_or_consumed_echo(self) -> None: + records = lifecycle(parts=("a",)) + records[-1:-1] = [response("assistant", ("b",)), event("stream_error", message=SENTINEL)] + self.check(records, [("human", SENTINEL), ("assistant", "a"), ("assistant", "b")]) + records[-1] = terminal(None) + self.check(records, [("human", SENTINEL), ("assistant", "a"), ("assistant", "b")]) + + def test_blank_echo_requires_canonical_occurrence(self) -> None: + self.check([start(), event("agent_message", message=" \n"), response("assistant", (" \n",)), terminal(None)], []) + self.check([start(), event("agent_message", message=" \n"), terminal(None)], [], "partial", unknown=1) + for invalid in ("", " \n", 1, [], {}): + with self.subTest(summary_type=type(invalid).__name__): + self.check([start(), terminal(invalid)], [], "partial", unknown=1) + + def test_rust_blank_predicate_is_separate_from_public_strip(self) -> None: + for nonblank in ("\u001c", "\u001d", "\u001e", "\u001f"): + self.check([start(), response("assistant", (nonblank,)), terminal(nonblank)], []) + for blank in ("\u0085", "\u00a0", "\u2003", "\u2028", "\u3000"): + self.check([start(), response("assistant", (blank,)), terminal(blank)], [], "partial", unknown=1) + + def test_lifecycle_aliases_and_missing_legacy_response_ids(self) -> None: + records = lifecycle() + records[0]["payload"]["type"] = "turn_started" + records[-1]["payload"]["type"] = "turn_complete" + for index in (1, 4): + records[index]["payload"].pop("internal_chat_message_metadata_passthrough") + records[index]["payload"].pop("id") + self.check(records, [("human", SENTINEL), ("assistant", "done")]) + + def test_orphan_terminal_requires_explicit_matching_scope(self) -> None: + self.check([response("assistant", ("done",)), terminal()], [("assistant", "done")]) + self.check([response("assistant", ("done",), turn=None), terminal()], + [("assistant", "done")], "partial", unknown=1) + self.check([terminal(), response("assistant", ("done",))], + [("assistant", "done")], "partial", unknown=1) + self.check([response("assistant", ("done",)), response("assistant", ("other",), turn="other"), terminal()], + [("assistant", "done"), ("assistant", "other")], "partial", unknown=1) + + def test_sessions_and_reused_turn_ids_are_occurrence_scopes(self) -> None: + self.check(lifecycle() + lifecycle(), [("human", SENTINEL), ("assistant", "done")] * 2) + self.check(lifecycle() + [{"type": "session_meta", "payload": {}}] + [start(), terminal()], + [("human", SENTINEL), ("assistant", "done")], "partial", unknown=1) + self.check(lifecycle() + [start("second"), terminal("done", "second")], + [("human", SENTINEL), ("assistant", "done")], "partial", unknown=1) + self.check([response("assistant", ("done",), turn=None), {"type": "session_meta", "payload": {}}, + event("agent_message", message="done")], [("assistant", "done")], "partial", unknown=1) + + def test_conflicting_turn_phase_and_item_identity_stay_partial(self) -> None: + for field, value in (("turn_id", "other-turn"), ("id", "other-item"), ("phase", "commentary")): + records = lifecycle(paginated=True, phase="final_answer") + target = records[3]["payload"] if field == "turn_id" else records[3]["payload"]["item"] + target[field] = value + result = self.parser._parse_transcript_text_outcome(json.dumps(records)) + self.assertEqual(result.status, "partial") + self.assertEqual(len(result.messages), 2) + records = lifecycle() + records[4]["payload"]["internal_chat_message_metadata_passthrough"]["turn_id"] = "other-turn" + self.check(records, [("human", SENTINEL), ("assistant", "done")], "partial", unknown=3) + + def test_duplicate_and_overlapping_terminals_do_not_manufacture_evidence(self) -> None: + records = lifecycle() + [terminal()] + self.check(records, [("human", SENTINEL), ("assistant", "done")], "partial", unknown=1) + records = [start(), response("assistant", ("done",)), start("other"), terminal("done", "other")] + result = self.parser._parse_transcript_text_outcome(json.dumps(records)) + self.assertEqual(result.status, "partial") + self.assertIn("codex_unsupported_lifecycle", result.error_categories) + + def test_events_cannot_explain_each_other_or_rewrites(self) -> None: + self.check([start(), event("agent_message", message="done"), terminal()], [], "partial", unknown=2) + records = lifecycle() + records[4] = response("assistant", ("original",)) + self.check(records, [("human", SENTINEL), ("assistant", "original")], "partial", unknown=2) + + def test_markup_and_mode_rewriting_are_explicit_coverage_gaps(self) -> None: + for parts in (("xhiddeny"), + ("before\n\nstep\n\nafter",)): + for mode in ("plan", "default", None): + records = lifecycle(parts=parts) + records[0]["payload"]["collaboration_mode_kind"] = mode + result = self.parser._parse_transcript_text_outcome(json.dumps(records)) + self.assertEqual(result.status, "partial") + self.assertIn("codex_unsupported_projection", result.error_categories) + self.assertEqual(len(result.messages), 2) + + def test_compaction_does_not_clear_original_evidence_or_emit_copies(self) -> None: + records = lifecycle() + records.insert(-1, {"type": "compacted", "payload": {"message": SCAFFOLD, + "replacement_history": [response("assistant", (SCAFFOLD,))["payload"]]}}) + self.check(records, [("human", SENTINEL), ("assistant", "done")]) + self.check([start(), records[-2], terminal(None)], []) + + def test_excluded_context_variants_do_not_unwrap_text(self) -> None: + top = ("session_meta", "turn_context", "compacted", "token_usage_record", "world_state", "retained_context", + "security_risk_score", "inter_agent_communication", "inter_agent_communication_metadata") + variants = ("reasoning", "agent_message", "function_call", "function_call_output", "custom_tool_call", + "custom_tool_call_output", "compaction", "compaction_summary", "configuration_update") + records = [{"type": kind, "payload": {"role": "user", "content": SENTINEL}} for kind in top] + records += [{"type": "response_item", "payload": {"type": kind, "role": "user", "content": SENTINEL}} + for kind in variants] + self.check(records, []) + + def test_outer_flags_are_strict_booleans(self) -> None: + for key in ("compaction_output", "inherited_user_message"): + for value in (0, 1, "false", "true", [], {}, None): + record = response() + record["metadata"] = {key: value} + with self.subTest(key=key, value=value): + self.check([record], [], "malformed", malformed=1) + record["metadata"] = {key: True} + self.check([record], []) + record["metadata"] = {key: False} + self.check([record], [("human", SENTINEL)]) + + def test_client_authored_does_not_override_scaffold(self) -> None: + record = annotate(response(), [annotation("harness")]) + record["metadata"] = {"client_authored": True} + self.check([record], []) + + def test_delivered_markers_exclude_text_and_unavailable_suffix(self) -> None: + record = response("assistant") + record["metadata"] = {"delivered_assistant_message": "codex:code-mode-delivery:v1:complete"} + self.check([record], []) + record["metadata"]["delivered_assistant_message"] = "codex:code-mode-delivery:v1:incomplete:" + SENTINEL + result = self.check([record], [], "partial", unknown=1) + self.assertIn("codex_delivery_unavailable", result.error_categories) + record["metadata"]["delivered_assistant_message"] = "other" + self.check([record], [], "partial", unknown=1) + record["metadata"]["delivered_assistant_message"] = None + self.check([record], [], "malformed", malformed=1) + self.check([response("assistant", ("codex:code-mode-delivery:v1:complete",))], + [("assistant", "codex:code-mode-delivery:v1:complete")]) + + def test_metadata_objects_and_array_alignment(self) -> None: + for bad in (1, "bad", [], False): + for outer in (True, False): + record = response() + if outer: + record["metadata"] = bad + else: + record["payload"]["internal_chat_message_metadata_passthrough"] = bad + self.check([record], [], "malformed", malformed=1) + for key, bad in (("content_item_metadata", []), ("content_item_metadata", [None]), + ("content_item_metadata", [{}, {}]), ("content_item_metadata", {}), + ("content_item_kinds", [None]), ("content_item_kinds", []), ("content_item_kinds", "unknown")): + record = response() + record["payload"]["internal_chat_message_metadata_passthrough"][key] = bad + self.check([record], [], "malformed", malformed=1) + self.check([annotate(response(), None, None)], [("human", SENTINEL)]) + + def test_per_index_provenance_does_not_shift_after_media(self) -> None: + record = response(parts=(SCAFFOLD, SENTINEL, SCAFFOLD)) + record["payload"]["content"][0] = {"type": "input_image", "image_url": "synthetic-image"} + annotate(record, [annotation("harness"), annotation("user"), annotation("agents_md")]) + result = self.check([record], [("human", SENTINEL)]) + self.assertNotIn(SCAFFOLD, self.parser.format_for_extraction(result.messages)) + record["payload"]["internal_chat_message_metadata_passthrough"]["content_item_metadata"][0] = annotation("user") + self.check([record], [("human", SENTINEL)], "partial", unknown=1) + + def test_provenance_and_legacy_fields(self) -> None: + exclusions = [annotation("developer_instructions", from_additional_requirements=False), annotation("skills"), + annotation("agents_md"), annotation("harness", hook=True), annotation("tool", namespace="functions"), + {"harness_injected": True}, {"source_tool_namespace": "functions", "harness_injected": False}] + for attribution in exclusions: + self.check([annotate(response(), [attribution])], []) + for attribution in ({}, {"harness_injected": False}, {"source_tool_namespace": None}, annotation("user")): + self.check([annotate(response(), [attribution])], [("human", SENTINEL)]) + for kind in ("unknown", "user_configured", "future"): + self.check([annotate(response(), [annotation(kind)])], [], "partial", unknown=1) + + def test_nested_metadata_types_never_fall_back_to_user(self) -> None: + values = [{"provenance": None}, {"provenance": "user"}, {"provenance": {"type": []}}, + annotation("harness", hook=1), annotation("developer_instructions"), + annotation("developer_instructions", from_additional_requirements="false"), + annotation("tool", namespace=1), {"harness_injected": 1}, {"source_tool_namespace": []}] + for attribution in values: + self.check([annotate(response(), [attribution])], [], "malformed", malformed=1) + + def test_kinds_fallback_conflict_and_lookalike_prose(self) -> None: + for kind in ("agents_md.instructions", "hooks.additional_context", "guardian.retained_instructions"): + self.check([annotate(response(), None, [kind])], []) + self.check([annotate(response(), [annotation("user")], [kind])], [], "partial", unknown=1) + self.check([annotate(response(), None, ["user.future"])], [], "partial", unknown=1) + self.check([annotate(response(), [annotation("harness")], ["future.fragment"])], []) + self.check([response(parts=("# AGENTS.md ordinary quoted text",))], [("human", "# AGENTS.md ordinary quoted text")]) + for kind in ("", "unknown"): + self.check([annotate(response(), None, [kind])], [("human", SENTINEL)]) + + def test_excluded_equal_text_cannot_supply_echo_or_terminal_credit(self) -> None: + excluded = [response("assistant", ("done",)) for _ in range(5)] + excluded[0]["metadata"] = {"inherited_user_message": True} + excluded[1]["metadata"] = {"compaction_output": True} + excluded[2]["metadata"] = {"delivered_assistant_message": "codex:code-mode-delivery:v1:complete"} + annotate(excluded[3], [annotation("harness")]) + annotate(excluded[4], [annotation("unknown")]) + for index, record in enumerate(excluded): + self.check([start(), event("agent_message", message="done"), record, terminal()], [], + "partial", unknown=3 if index == 4 else 2) + + def test_record_level_counts_and_private_diagnostics(self) -> None: + record = response() + record["payload"]["content"] = [{"type": "future", "text": SENTINEL}, {"type": "input_image"}, + {"type": "input_text", "text": 3}, 5, + {"type": "input_text", "text": SENTINEL}] + record["timestamp"] = {SENTINEL: "bad"} + result = self.check([record], [("human", SENTINEL)], "partial", malformed=1, unknown=1) + self.assertEqual(result.messages[0].timestamp, "") + self.assertEqual(result.total_records, 1) + + def test_unknown_native_and_events_are_not_empty_complete(self) -> None: + for record in ({"type": "future", "payload": {"content": SENTINEL}}, + {"type": "realtime_item", "payload": {"text": SENTINEL}}, + {"type": "response_item", "payload": {"type": "future", "content": SENTINEL}}, + event("future", text=SENTINEL), event("user_message", message=SENTINEL), + event("item_completed", item={"type": "Future", "text": SENTINEL})): + self.check([record], [], "partial", unknown=1) + for record in ({"type": "response_item", "payload": []}, + {"type": "response_item", "payload": {"type": "message", "role": "user", "content": SENTINEL}}, + {"type": "event_msg", "payload": {"type": []}}): + self.check([record], [], "malformed", malformed=1) + for invalid in ([], {}, 1, None): + self.check([{"type": invalid, "payload": {}}], [], "malformed", malformed=1) + + def test_user_output_text_is_not_a_normal_user_projection(self) -> None: + record = response() + record["payload"]["content"][0]["type"] = "output_text" + self.check([record], [("human", SENTINEL)]) + self.check([record, event("user_message", message=SENTINEL)], [("human", SENTINEL)], "partial", unknown=1) + + def test_event_media_and_unknown_typed_items_stay_partial(self) -> None: + for key in ("images", "audio", "local_images", "file_ids"): + records = lifecycle() + records[2]["payload"][key] = [SENTINEL] + self.check(records, [("human", SENTINEL), ("assistant", "done")], "partial", unknown=1) + records = lifecycle(paginated=True) + records[3]["payload"]["item"]["content"].append({"type": "Text", "text": "extra"}) + self.check(records, [("human", SENTINEL), ("assistant", "done")], "partial", unknown=1) + + def test_claude_and_generic_compatibility(self) -> None: + records = [{"type": "user", "message": {"role": "user", "content": "hello"}}, + {"type": "assistant", "message": {"content": [{"type": "thinking", "thinking": SCAFFOLD}, + {"type": "text", "text": "answer"}]}}, + {"type": "user", "message": {"content": [{"type": "tool_result", "content": "tool"}]}}, + {"role": "user", "content": "generic"}, {"type": "progress"}] + result = self.check(records, [("human", "hello"), ("assistant", "answer"), ("tool_result", "tool"), ("human", "generic")]) + self.assertEqual(self.parser.format_for_extraction(result.messages), "User: hello\n\nAssistant: answer\n\nUser: generic") + + def test_generic_records_keep_incidental_payload_fields(self) -> None: + for payload in (None, {}, {"type": "message", "role": "assistant", "content": SCAFFOLD}): + for type_fields in ({}, {"type": None}, {"type": ""}): + records = [{"role": role, "content": text, "payload": payload, **type_fields} + for role, text in (("user", SENTINEL), ("assistant", "answer"))] + self.check(records, [("human", SENTINEL), ("assistant", "answer")]) + self.check([{"type": "future_native", "payload": {}, "role": "user", "content": SENTINEL}], + [], "partial", unknown=1) + self.check([{"role": "observer", "content": SENTINEL, "payload": None}], [("observer", SENTINEL)]) + + def test_index_preserves_absent_and_explicit_identity_compatibility(self) -> None: + first = response("assistant", ("same",), phase="commentary") + first["payload"].pop("id") + second = response("assistant", ("same",), phase="final_answer", item="second") + third = response("assistant", ("same",), phase="final_answer", item="third") + records = [start(), first, second, third, + completed("assistant", "same", item="second", phase="commentary"), + event("agent_message", message="same", phase="final_answer"), + completed("assistant", "same", item="third"), terminal("same")] + self.check(records, [("assistant", "same")] * 3) + records.insert(-1, completed("assistant", "same", item="third")) + self.check(records, [("assistant", "same")] * 3, "partial", unknown=1) + + def test_legacy_exclusions_keep_incidental_payload_fields(self) -> None: + for kind in sorted(self.parser._NON_CONVERSATION_TYPES): + for selector in ({"type": kind}, {"role": kind}, {"type": None, "role": kind}): + for extra in ({}, {"payload": None}, {"payload": {}}, + {"payload": {"type": "message", "role": "assistant", "content": SCAFFOLD}}): + with self.subTest(kind=kind, selector=selector, payload_present="payload" in extra): + self.check([{**selector, **extra}], []) + self.check([{"type": "future_native", "role": "summary", "payload": {}}], + [], "partial", unknown=1) + self.check([{"type": "response_item", "role": "summary", "payload": {}}], + [], "malformed", malformed=1) + + def test_identity_filters_do_not_rewind_role_cursors(self) -> None: + records = [response("assistant", ("same",), turn="first", item="first"), + response(parts=("same",), turn="second"), + response("assistant", ("same",), turn="second", item="second"), + completed("assistant", "same", turn="second", item="second"), + event("user_message", message="same"), + completed("assistant", "same", turn="first", item="first")] + self.check(records, [("assistant", "same"), ("human", "same"), ("assistant", "same")], "partial", unknown=1) + + def test_unmatched_echo_work_is_bounded_including_metadata_misses(self) -> None: + for mismatch in ("text", "phase", "item", "turn"): + previous = None + for size in (100, 200, 400): + with self.subTest(mismatch=mismatch, size=size): + originals = [response("assistant", ("same",), phase="commentary") for _ in range(size)] + if mismatch == "text": + echo = event("agent_message", message="different") + elif mismatch == "phase": + echo = event("agent_message", message="same", phase="final_answer") + elif mismatch == "item": + echo = completed("assistant", "same", item="different") + else: + echo = completed("assistant", "same", turn="different") + result, operations = counted_reconciliation(self.parser, originals + [echo] * size) + self.assertEqual((len(result.messages), result.unrecognized_records), (size, size)) + self.assertEqual(result.status, "partial") + # Eight entries per response; at most two indexed event + # queries. Allow linear key work plus binary-search reads. + self.assertLessEqual(operations, 96 * size + 4 * size * (size.bit_length() + 1)) + if previous is not None: + self.assertLessEqual(operations, 3 * previous + 32) + previous = operations + + def test_syntax_accounting_is_not_replaced_by_native_counts(self) -> None: + text = json.dumps(response()) + "\n{bad\n" + json.dumps(event("future")) + result = self.parser._parse_transcript_text_outcome(text) + self.assertEqual((result.status, result.total_records, result.malformed_records, result.unrecognized_records), + ("partial", 3, 1, 1)) + self.assertEqual(self.parser._parse_transcript_text_outcome("[bad").status, "salvaged") + + +class StopCountTests(unittest.TestCase): + def setUp(self) -> None: + self.parser = load_parser() + + def check(self, text: str, expected: bool, threshold: int = 5) -> None: + count, calls = load_stop_count(self.parser, text) + with patch.object(Path, "read_text", side_effect=AssertionError("Stop must not read directly")), \ + patch.object(Path, "stat", side_effect=AssertionError("Stop must not stat directly")): + self.assertIs(count("synthetic-transcript.jsonl", threshold), expected) + self.assertEqual(calls, [Path("synthetic-transcript.jsonl")]) + + def test_four_original_native_failures(self) -> None: + five = [response()] * 5 + self.check(json.dumps(five), True) + self.check("\n".join(json.dumps(item) for item in five), True) + self.check(json.dumps(response(parts=(SENTINEL * 40,))), False) + self.check(json.dumps({"type": "session_meta", "payload": {"metadata": SENTINEL * 40}}), False) + + def test_events_and_scaffolding_do_not_inflate_turns(self) -> None: + self.check(json.dumps([response()] + [event("user_message", message=SENTINEL)] * 5), False) + self.check(json.dumps([annotate(response(), [annotation("harness")])] * 5), False) + self.check(json.dumps([response(parts=("synthetic",))] * 5), False) + self.check(json.dumps([response(parts=(" \n",))] * 5), False) + self.check(json.dumps([response(parts=("one", "two", "three", "four", "five"))]), False) + + def test_actual_threshold_duplicates_and_partial_supported_turns(self) -> None: + self.check(json.dumps([response()] * 4), False) + self.check(json.dumps([response()] * 5), True) + self.check(json.dumps([response()] * 5 + [event("future")]), True) + self.check(json.dumps([response()] * 5), False, threshold=6) + self.check(json.dumps(lifecycle(paginated=True)), True, threshold=1) + + def test_claude_tools_plain_text_and_directives(self) -> None: + self.check(json.dumps([{"type": "user", "message": {"content": [{"type": "tool_result", "content": "result"}]}}] * 5), False) + self.check("Human: one\nAssistant: reply\nUser: two", True, threshold=2) + self.check("ordinary unmarked text " * 100, False) + self.check(json.dumps([response(parts=("Use the synthetic short format",))] * 5), True) + + def test_unreadable_is_false_even_at_zero_threshold(self) -> None: + with patch.object(self.parser, "_parse_transcript_text_outcome", return_value=self.parser.TranscriptOutcome([], "unreadable")): + self.check("", False, threshold=0) + + +class InMemoryPathTests(unittest.TestCase): + def test_native_explicit_file_path_preserves_version_and_privacy(self) -> None: + parser = load_parser() + data = ("\n".join(json.dumps(record) for record in lifecycle())).encode("utf-8") + info = types.SimpleNamespace(st_dev=1, st_ino=2, st_size=len(data), st_mtime_ns=3, st_ctime_ns=4) + + class Reader(io.BytesIO): + def fileno(self) -> int: + return 41 + + with patch.object(Path, "open", side_effect=lambda *args, **kwargs: Reader(data)), \ + patch.object(parser.os, "fstat", return_value=info), \ + patch.object(Path, "stat", side_effect=AssertionError("Explicit path must not probe")): + result = parser.parse_transcript_outcome(Path("synthetic-private-path.jsonl")) + self.assertTrue(result.complete) + self.assertTrue(result.file_version.stable) + self.assertEqual(result.total_records, 6) + self.assertEqual(len(result.messages), 2) + self.assertNotIn("synthetic-private-path", repr(result)) + + +class MutationControlTests(unittest.TestCase): + """The witnesses must reject the specific earlier unsafe implementations.""" + + def mutate(self, before: str, after: str) -> types.ModuleType: + source = (ROOT / "truememory/ingest/transcript.py").read_text(encoding="utf-8") + self.assertEqual(source.count(before), 1) + return load_parser(source.replace(before, after)) + + def outcome(self, parser: types.ModuleType, records: list[dict]) -> object: + return parser._parse_transcript_text_outcome(json.dumps(records)) + + def test_newline_or_trimmed_projection_breaks_ordinary_lifecycle(self) -> None: + for replacement in ('raw = "\\n".join(parts)', 'raw = "".join(parts).strip()'): + with self.subTest(replacement=replacement): + parser = self.mutate('raw = "".join(parts)', replacement) + self.assertEqual(self.outcome(parser, lifecycle(parts=(" a", "b "))).status, "partial") + + def test_removing_provenance_filter_leaks_scaffold(self) -> None: + parser = self.mutate('if attribution == "excluded":', 'if False:') + records = [annotate(response(parts=(SCAFFOLD,)), [annotation("harness")])] + self.assertEqual([message.content for message in self.outcome(parser, records).messages], [SCAFFOLD]) + self.assertEqual(self.outcome(load_parser(), records).messages, []) + + def test_ignoring_event_reconciliation_falsely_certifies_event_only(self) -> None: + parser = self.mutate('_reconcile_codex(native_records)', 'pass # synthetic mutation') + records = [start(), event("agent_message", message=SENTINEL), terminal(SENTINEL)] + self.assertTrue(self.outcome(parser, records).complete) + self.assertEqual(self.outcome(load_parser(), records).unrecognized_records, 2) + + def test_last_raw_row_only_rule_loses_valid_terminal(self) -> None: + parser = self.mutate('for before, candidate in reversed(candidates)', + 'for before, candidate in candidates[-1:]') + records = lifecycle(parts=("a",)) + records.insert(-1, response("assistant", ("b",))) + self.assertEqual(self.outcome(parser, records).status, "partial") + self.assertTrue(self.outcome(load_parser(), records).complete) + + def test_echoes_must_not_match_only_preceding_responses(self) -> None: + source = (ROOT / "truememory/ingest/transcript.py").read_text(encoding="utf-8") + source = source.replace('for _, event in items:', 'for event_index, event in items:') + target = 'position = _codex_next_echo(credit_index, event, cursors.get(event.role, 0))' + self.assertEqual(source.count(target), 1) + source = source.replace(target, target + '\n if position is not None and credits[position][0] >= event_index:\n position = None') + parser = load_parser(source) + self.assertEqual(self.outcome(parser, lifecycle()).status, "partial") + self.assertTrue(self.outcome(load_parser(), lifecycle()).complete) + + def test_terminal_must_not_require_final_phase(self) -> None: + parser = self.mutate('candidate.projection == terminal.projection and not candidate.malformed', + 'candidate.phase == "final_answer" and candidate.projection == terminal.projection and not candidate.malformed') + self.assertEqual(self.outcome(parser, lifecycle(phase="commentary")).status, "partial") + self.assertTrue(self.outcome(load_parser(), lifecycle(phase="commentary")).complete) + + def test_session_reset_is_necessary_even_without_turn_boundaries(self) -> None: + parser = self.mutate('if record.kind == "session_meta":', 'if False:') + records = [response("assistant", ("done",), turn=None), {"type": "session_meta", "payload": {}}, + event("agent_message", message="done")] + self.assertTrue(self.outcome(parser, records).complete) + self.assertEqual(self.outcome(load_parser(), records).status, "partial") + + def test_terminal_and_echo_credit_must_remain_independent(self) -> None: + parser = self.mutate('cursors[event.role] = position + 1', + 'cursors[event.role] = position + 1\n credits[position][1].projection = None') + self.assertEqual(self.outcome(parser, lifecycle()).status, "partial") + self.assertTrue(self.outcome(load_parser(), lifecycle()).complete) + + def test_echo_credit_must_be_consumed_once(self) -> None: + parser = self.mutate('cursors[event.role] = position + 1', 'cursors[event.role] = 0') + records = lifecycle() + records.insert(-1, event("agent_message", message="done")) + self.assertTrue(self.outcome(parser, records).complete) + self.assertEqual(self.outcome(load_parser(), records).status, "partial") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-dunbar-publication-753.py b/tests/test-dunbar-publication-753.py new file mode 100644 index 00000000..dcf6bee6 --- /dev/null +++ b/tests/test-dunbar-publication-753.py @@ -0,0 +1,596 @@ +"""Synthetic SQLite ownership and transaction checks for Dunbar publication.""" + +import ast +import random +import sqlite3 +import tempfile +import types +import unittest +from pathlib import Path +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] + + +def load_modules() -> tuple[types.ModuleType, types.ModuleType]: + modules = [] + for name in ("storage", "personality"): + module = types.ModuleType("synthetic_dunbar_" + name) + source = ROOT / "truememory" / (name + ".py") + tree = ast.parse(source.read_text()) + tree.body = [node for node in tree.body if not ( + isinstance(node, ast.ImportFrom) and node.module in {"truememory.storage", "truememory.fts_search"} + )] + if modules: + module._initialize_dunbar_ownership = modules[0]._initialize_dunbar_ownership + module._dunbar_ownership_ready = modules[0]._dunbar_ownership_ready + exec(compile(tree, str(source), "exec"), module.__dict__) + modules.append(module) + return tuple(modules) + + +STORAGE, PERSONALITY = load_modules() + + +def legacy_reference(conn: sqlite3.Connection, primary: str | None) -> dict: + """Frozen pre-checkpoint computation, without its destructive publication.""" + if primary is None: + return {} + primary = primary.lower() + rows = conn.execute(""" + SELECT LOWER(name) as name, COUNT(*) as cnt, MAX(ts) as last_ts FROM ( + SELECT recipient as name, timestamp as ts FROM messages WHERE LOWER(sender) = ? AND recipient != '' + UNION ALL + SELECT sender as name, timestamp as ts FROM messages WHERE LOWER(recipient) = ? AND sender != '' + ) GROUP BY LOWER(name) ORDER BY cnt DESC + """, (primary, primary)).fetchall() + if not rows: + return {} + maximum = rows[0][1] + result = {} + for name, count, last in rows: + score = count / maximum + layer = "intimate" if score > 0.6 else "close" if score > 0.3 else "friend" if score > 0.1 else "acquaintance" + result[name] = {"message_count": count, "last_interaction": last or "", + "dunbar_layer": layer, "strength": round(score, 3)} + return result + + +class DunbarFixture(unittest.TestCase): + def setUp(self) -> None: + directory = tempfile.TemporaryDirectory(prefix="synthetic-dunbar-") + self.addCleanup(directory.cleanup) + self.path = Path(directory.name) / "synthetic.sqlite" + self.conn = self.open_db() + + def open_db(self) -> sqlite3.Connection: + conn = STORAGE.create_db(self.path) + conn.execute("PRAGMA busy_timeout=100") + self.addCleanup(conn.close) + return conn + + def seed(self) -> None: + self.conn.executemany( + "INSERT INTO messages(content,sender,recipient,timestamp) VALUES ('synthetic',?,?,?)", + [("Synthetic-Primary", "Synthetic-Contact", "2026-01-01"), + ("synthetic-contact", "synthetic-primary", "2026-01-02"), + ("synthetic-primary", "synthetic-other", "2026-01-03")], + ) + self.conn.commit() + + def build(self, primary: str | None = "Synthetic-Primary") -> dict: + return PERSONALITY.build_dunbar_hierarchy(self.conn, primary) + + def snapshot(self, conn: sqlite3.Connection | None = None) -> tuple[list, list]: + conn = conn or self.conn + return (conn.execute("SELECT * FROM entity_relationships ORDER BY id").fetchall(), + conn.execute("SELECT * FROM dunbar_generated_relationships ORDER BY relationship_id").fetchall()) + + def compute_hook(self, action): + original = PERSONALITY._compute_dunbar_hierarchy + + def compute(rows): + action() + return original(rows) + + return patch.object(PERSONALITY, "_compute_dunbar_hierarchy", compute) + + +class TestDunbarOwnership(DunbarFixture): + def test_bounded_reference_corpus_preserves_full_dict_and_insertion_order(self) -> None: + rng = random.Random(753) + entities = ["synthetic-alpha", "SYNTHETIC-ALPHA", "synthetic-beta", "synthetic-gamma", "", None] + self.conn.executemany( + "INSERT INTO messages(content,sender,recipient,timestamp) VALUES ('synthetic',?,?,?)", + [(rng.choice(entities), rng.choice(entities), rng.choice([None, "", "2026-01-01", "2026-01-02"])) + for _ in range(120)], + ) + self.conn.commit() + for primary in entities: + with self.subTest(primary=primary): + expected = legacy_reference(self.conn, primary) + self.assertEqual(list(self.build(primary).items()), list(expected.items())) + + def test_reference_ties_thresholds_rounding_case_and_timestamps(self) -> None: + for name, count in (("alpha", 10), ("beta", 6), ("gamma", 3), ("delta", 1), ("epsilon", 6)): + self.conn.executemany( + "INSERT INTO messages(content,sender,recipient,timestamp) VALUES ('synthetic','PRIMARY',?,?)", + [(name.upper() if i % 2 else name, None if i == 0 else "2026-01-01") for i in range(count)], + ) + self.conn.commit() + expected = legacy_reference(self.conn, "Primary") + result = self.build("Primary") + self.assertEqual(list(result.items()), list(expected.items())) + self.assertEqual([result[name]["dunbar_layer"] for name in ("alpha", "beta", "gamma", "delta")], + ["intimate", "close", "friend", "acquaintance"]) + self.assertEqual(PERSONALITY.read_dunbar_coverage(self.conn)["managed_rows"], 5) + + def test_reference_preserves_self_contact_unicode_directive_and_empty_rules(self) -> None: + self.seed() + self.conn.executemany( + "INSERT INTO messages(content,sender,recipient,timestamp,directive) VALUES ('synthetic',?,?,?,?)", + [("synthetic-primary", "synthetic-primary", "", 1), ("Synthetic-Primary", "", "2026-01-01", 0), + ("İ", "synthetic-unicode", None, 0), ("synthetic-primary", None, None, 0)], + ) + self.conn.commit() + for primary in ("Synthetic-Primary", "İ", "", None): + with self.subTest(primary=primary): + expected = legacy_reference(self.conn, primary) + self.assertEqual(list(self.build(primary).items()), list(expected.items())) + + def test_legacy_contacts_and_other_producers_are_never_adopted_or_deleted(self) -> None: + self.seed() + for kind in ("contact", "synthetic-other", None): + self.conn.execute("INSERT INTO entity_relationships(entity_a,entity_b,relationship_type) VALUES (?,?,?)", + ("synthetic-primary", "synthetic-contact", kind)) + self.conn.commit() + legacy = self.snapshot()[0] + self.build() + self.build() + self.assertEqual(self.snapshot()[0][:3], legacy) + self.assertEqual(PERSONALITY.read_dunbar_coverage(self.conn), + {"status": "partial", "managed_rows": 2, "unowned_contacts": 1, "invalid_ownership": 0}) + self.assertEqual(len(self.snapshot()[0]), 5) + + def test_changed_primary_empty_and_none_retire_only_verified_owned_rows(self) -> None: + self.seed() + self.conn.execute("INSERT INTO entity_relationships(entity_a,entity_b,relationship_type) VALUES ('synthetic-legacy','synthetic-contact','contact')") + self.conn.commit() + legacy = self.snapshot()[0] + self.build() + self.build("Synthetic-Contact") + generated = self.conn.execute( + "SELECT r.entity_a FROM entity_relationships r JOIN dunbar_generated_relationships g ON r.id=g.relationship_id" + ).fetchall() + self.assertEqual(generated, [("synthetic-contact",)]) + self.assertEqual(self.build("synthetic-absent"), {}) + self.assertEqual(self.snapshot(), (legacy, [])) + self.build() + self.assertEqual(self.build(None), {}) + self.assertEqual(self.snapshot(), (legacy, [])) + + def test_every_generated_field_modification_relinquishes_ownership(self) -> None: + self.seed() + for column, value in (("entity_a", "synthetic-external"), ("entity_b", "synthetic-external"), + ("relationship_type", "synthetic-external"), ("strength", 0.123), + ("dunbar_layer", "synthetic-external"), ("last_interaction", None)): + with self.subTest(column=column): + self.build() + row_id = self.snapshot()[1][0][0] + self.conn.execute(f"UPDATE entity_relationships SET {column}=? WHERE id=?", (value, row_id)) + self.conn.commit() + changed = self.conn.execute("SELECT * FROM entity_relationships WHERE id=?", (row_id,)).fetchone() + self.assertIsNone(self.conn.execute("SELECT 1 FROM dunbar_generated_relationships WHERE relationship_id=?", (row_id,)).fetchone()) + self.build() + self.assertEqual(self.conn.execute("SELECT * FROM entity_relationships WHERE id=?", (row_id,)).fetchone(), changed) + self.assertIsNone(self.conn.execute("SELECT 1 FROM dunbar_generated_relationships WHERE relationship_id=?", (row_id,)).fetchone()) + + def test_reused_ids_with_disabled_foreign_keys_are_preserved(self) -> None: + self.seed() + self.build() + row_id = self.snapshot()[1][0][0] + self.conn.execute("PRAGMA foreign_keys=OFF") + self.conn.execute("DELETE FROM entity_relationships WHERE id=?", (row_id,)) + self.conn.execute("INSERT INTO entity_relationships(id,entity_a,entity_b,relationship_type) VALUES (?,'synthetic-reused','synthetic-contact','contact')", (row_id,)) + self.conn.commit() + self.build() + self.assertEqual(self.conn.execute("SELECT entity_a FROM entity_relationships WHERE id=?", (row_id,)).fetchone(), ("synthetic-reused",)) + self.assertIsNone(self.conn.execute("SELECT 1 FROM dunbar_generated_relationships WHERE relationship_id=?", (row_id,)).fetchone()) + + def test_identical_id_and_fields_replacement_never_inherits_ownership(self) -> None: + self.seed() + self.conn.execute("PRAGMA foreign_keys=OFF") + self.conn.execute("PRAGMA recursive_triggers=OFF") + for delete_first in (False, True): + with self.subTest(delete_first=delete_first): + self.build() + row_id = self.snapshot()[1][0][0] + row = self.conn.execute("SELECT * FROM entity_relationships WHERE id=?", (row_id,)).fetchone() + if delete_first: + self.conn.execute("DELETE FROM entity_relationships WHERE id=?", (row_id,)) + self.conn.execute("INSERT OR REPLACE INTO entity_relationships VALUES (?,?,?,?,?,?,?)", row) + self.conn.commit() + self.assertIsNone(self.conn.execute("SELECT 1 FROM dunbar_generated_relationships WHERE relationship_id=?", (row_id,)).fetchone()) + self.build(None) + self.assertEqual(self.conn.execute("SELECT * FROM entity_relationships WHERE id=?", (row_id,)).fetchone(), row) + + def test_edit_then_restore_relinquishes_but_noop_update_preserves_ownership(self) -> None: + self.seed() + self.build() + owned = self.snapshot()[1] + self.conn.execute("UPDATE entity_relationships SET strength=strength,last_interaction=last_interaction") + self.conn.commit() + self.assertEqual(self.snapshot()[1], owned) + row_id = owned[0][0] + row = self.conn.execute("SELECT * FROM entity_relationships WHERE id=?", (row_id,)).fetchone() + self.conn.execute("UPDATE entity_relationships SET id=? WHERE id=?", (row_id + 1000, row_id)) + self.conn.execute("UPDATE entity_relationships SET id=? WHERE id=?", (row_id, row_id + 1000)) + self.conn.commit() + self.build(None) + self.assertEqual(self.snapshot(), ([row], [])) + + def test_fingerprint_mismatch_and_exact_matching_legacy_row_are_preserved(self) -> None: + self.seed() + self.build() + row_id = self.snapshot()[1][0][0] + row = self.conn.execute("SELECT * FROM entity_relationships WHERE id=?", (row_id,)).fetchone() + self.conn.execute("UPDATE dunbar_generated_relationships SET row_fingerprint=? WHERE relationship_id=?", (bytes(32), row_id)) + legacy_id = self.conn.execute( + "INSERT INTO entity_relationships(entity_a,entity_b,relationship_type,strength,dunbar_layer,last_interaction) VALUES (?,?,?,?,?,?)", row[1:] + ).lastrowid + self.conn.commit() + self.assertEqual(PERSONALITY.read_dunbar_coverage(self.conn)["invalid_ownership"], 1) + self.build(None) + self.assertEqual([r[0] for r in self.snapshot()[0]], [row_id, legacy_id]) + self.assertEqual(self.snapshot()[1], []) + + def test_deleted_rows_cascade_and_orphaned_ownership_is_cleaned(self) -> None: + self.seed() + self.build() + first, second = [row[0] for row in self.snapshot()[1]] + self.conn.execute("DELETE FROM entity_relationships WHERE id=?", (first,)) + self.conn.commit() + self.assertIsNone(self.conn.execute("SELECT 1 FROM dunbar_generated_relationships WHERE relationship_id=?", (first,)).fetchone()) + self.conn.execute("PRAGMA foreign_keys=OFF") + self.conn.execute("DROP TRIGGER dunbar_relationships_ad") + self.conn.execute("DELETE FROM entity_relationships WHERE id=?", (second,)) + self.conn.commit() + self.assertEqual(PERSONALITY.read_dunbar_coverage(self.conn)["invalid_ownership"], 1) + self.build(None) + self.assertEqual(self.snapshot(), ([], [])) + + +class TestDunbarTransactions(DunbarFixture): + def test_source_and_ownership_changes_during_compute_reject_publication(self) -> None: + self.seed() + self.build() + writer = self.open_db() + changes = ( + "UPDATE messages SET sender='synthetic-changed' WHERE id=1", + "UPDATE messages SET recipient='synthetic-changed' WHERE id=1", + "UPDATE messages SET timestamp='2026-02-01' WHERE id=1", + "UPDATE messages SET id=id+100 WHERE id=2", + "INSERT INTO messages(content,sender,recipient) VALUES ('synthetic','synthetic-primary','synthetic-new')", + "DELETE FROM messages WHERE id=1", + "UPDATE entity_relationships SET strength=0.125 WHERE id=(SELECT MIN(id) FROM entity_relationships)", + "UPDATE dunbar_generated_relationships SET generation='synthetic-external'", + ) + for sql in changes: + with self.subTest(operation=sql): + captured = [] + + def mutate(): + writer.execute(sql) + writer.commit() + captured.append(self.snapshot(writer)) + + with self.compute_hook(mutate): + with self.assertRaises(sqlite3.OperationalError): + self.build() + self.assertEqual(self.snapshot(), captured[0]) + self.assertFalse(self.conn.in_transaction) + + def test_unrelated_writer_and_reader_can_complete_during_compute(self) -> None: + self.seed() + self.build() + writer = self.open_db() + old = self.snapshot() + + def observe(): + self.assertFalse(self.conn.in_transaction) + self.assertEqual(self.snapshot(writer), old) + writer.execute("INSERT INTO metadata(key,value) VALUES ('synthetic','committed')") + writer.commit() + + with self.compute_hook(observe): + self.assertEqual(self.build(), legacy_reference(self.conn, "Synthetic-Primary")) + + def test_caller_changes_and_outputs_remain_rollbackable(self) -> None: + self.seed() + self.build() + old = self.snapshot() + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('synthetic-pending','retained')") + self.conn.execute("UPDATE messages SET recipient='synthetic-caller' WHERE id=1") + self.build() + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + self.assertEqual(self.snapshot(), old) + self.assertIsNone(self.conn.execute("SELECT value FROM metadata WHERE key='synthetic-pending'").fetchone()) + + def test_stale_caller_snapshot_fails_without_committing_or_restarting_it(self) -> None: + self.seed() + self.build() + old = self.snapshot() + writer = self.open_db() + self.conn.execute("BEGIN") + self.conn.execute("SELECT count(*) FROM messages").fetchone() + + def mutate(): + writer.execute("UPDATE messages SET recipient='synthetic-new' WHERE id=1") + writer.commit() + + with self.compute_hook(mutate), self.assertRaises(sqlite3.OperationalError): + self.build() + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + self.assertEqual(self.snapshot(), old) + + def test_compute_failure_and_cancellation_preserve_previous_generation(self) -> None: + self.seed() + self.build() + old = self.snapshot() + for failure in (ValueError("synthetic"), KeyboardInterrupt()): + with self.subTest(failure=type(failure).__name__): + with patch.object(PERSONALITY, "_compute_dunbar_hierarchy", side_effect=failure): + with self.assertRaises(type(failure)): + self.build() + self.assertEqual(self.snapshot(), old) + + def test_cancellation_after_owned_rows_are_deleted_rolls_back_publication(self) -> None: + self.seed() + self.build() + old = self.snapshot() + with patch.object(PERSONALITY.uuid, "uuid4", side_effect=KeyboardInterrupt()): + with self.assertRaises(KeyboardInterrupt): + self.build() + self.assertEqual(self.snapshot(), old) + self.assertFalse(self.conn.in_transaction) + + def test_midpublication_failure_keeps_ledger_and_rows_atomic_after_caller_commit(self) -> None: + self.seed() + self.build() + old = self.snapshot() + self.conn.execute("CREATE TRIGGER synthetic_fail BEFORE INSERT ON dunbar_generated_relationships BEGIN SELECT RAISE(ABORT,'synthetic publication'); END") + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('synthetic-pending','retained')") + with self.assertRaises(sqlite3.IntegrityError): + self.build() + self.conn.commit() + self.assertEqual(self.snapshot(), old) + self.assertEqual(self.conn.execute("SELECT value FROM metadata WHERE key='synthetic-pending'").fetchone(), ("retained",)) + + def test_commit_and_release_failure_roll_back_before_cleanup(self) -> None: + self.seed() + self.build() + for borrowed in (False, True): + with self.subTest(borrowed=borrowed): + old = self.snapshot() + if borrowed: + self.conn.execute("INSERT OR REPLACE INTO metadata(key,value) VALUES ('synthetic-pending','retained')") + denied = [] + + def authorizer(action, operation, *unused): + expected = sqlite3.SQLITE_SAVEPOINT if borrowed else sqlite3.SQLITE_TRANSACTION + terminal = "RELEASE" if borrowed else "COMMIT" + if action == expected and operation == terminal and not denied: + denied.append(operation) + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + + with self.compute_hook(lambda: self.conn.set_authorizer(authorizer)): + try: + with self.assertRaises(sqlite3.DatabaseError): + self.build() + finally: + self.conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.assertEqual(len(denied), 1) + self.conn.commit() + self.assertEqual(self.snapshot(), old) + + def test_rollback_failure_does_not_release_failed_savepoint(self) -> None: + self.seed() + self.build() + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('synthetic-pending','retained')") + operations = [] + + def authorizer(action, operation, *unused): + if action == sqlite3.SQLITE_SAVEPOINT: + operations.append(operation) + if operation == "ROLLBACK": + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + + original = PERSONALITY._initialize_dunbar_ownership + + def fail(conn): + original(conn) + conn.set_authorizer(authorizer) + raise ValueError("synthetic publication") + + with patch.object(PERSONALITY, "_initialize_dunbar_ownership", fail): + try: + with self.assertRaises(sqlite3.DatabaseError): + self.build() + finally: + self.conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.conn.rollback() + self.assertEqual(operations, ["ROLLBACK"]) + + def test_reader_sees_old_output_and_ledger_until_publication_commits(self) -> None: + self.seed() + self.build() + old = self.snapshot() + reader = self.open_db() + original = PERSONALITY._dunbar_fingerprint + observed = [] + + def observe(rows): + if self.conn.in_transaction and self.conn.total_changes > before: + observed.append(self.snapshot(reader)) + return original(rows) + + before = self.conn.total_changes + with patch.object(PERSONALITY, "_dunbar_fingerprint", observe): + self.build("synthetic-contact") + self.assertTrue(observed) + self.assertTrue(all(snapshot == old for snapshot in observed)) + self.assertNotEqual(self.snapshot(reader), old) + + +class TestDunbarMigrationAndCoverage(DunbarFixture): + def test_additive_ledger_preserves_existing_rows_and_source_epoch(self) -> None: + self.seed() + for name in STORAGE._DUNBAR_OWNERSHIP_TRIGGERS: + self.conn.execute(f"DROP TRIGGER {name}") + self.conn.execute("DROP TABLE dunbar_generated_relationships") + self.conn.execute("INSERT INTO entity_relationships(entity_a,entity_b,relationship_type) VALUES ('synthetic-legacy','synthetic-contact','contact')") + self.conn.commit() + epoch = self.conn.execute("SELECT epoch FROM maintenance_source_state").fetchone() + self.assertEqual(PERSONALITY.read_dunbar_coverage(self.conn)["unowned_contacts"], 1) + self.assertFalse(PERSONALITY._dunbar_has_ledger(self.conn)) + reopened = self.open_db() + version = reopened.execute("PRAGMA schema_version").fetchone() + self.assertEqual(reopened.execute("SELECT epoch FROM maintenance_source_state").fetchone(), epoch) + self.open_db() + self.assertEqual(reopened.execute("PRAGMA schema_version").fetchone(), version) + self.assertEqual(PERSONALITY.read_dunbar_coverage(reopened)["unowned_contacts"], 1) + + def test_missing_or_replaced_trigger_invalidates_all_ownership_without_adoption(self) -> None: + self.seed() + epoch = self.conn.execute("SELECT epoch FROM maintenance_source_state").fetchone() + for index, name in enumerate(STORAGE._DUNBAR_OWNERSHIP_TRIGGERS): + with self.subTest(trigger=name): + self.build() + relationships, owned = self.snapshot() + self.assertTrue(owned) + self.conn.execute(f"DROP TRIGGER {name}") + if index == 1: + self.conn.execute(f"CREATE TRIGGER {name} AFTER UPDATE ON entity_relationships BEGIN SELECT 1; END") + self.conn.commit() + self.assertEqual(PERSONALITY.read_dunbar_coverage(self.conn)["managed_rows"], 0) + STORAGE._initialize_dunbar_ownership(self.conn) + self.assertTrue(STORAGE._dunbar_ownership_ready(self.conn)) + self.assertEqual(self.snapshot(), (relationships, [])) + self.assertEqual(self.conn.execute("SELECT epoch FROM maintenance_source_state").fetchone(), epoch) + version = self.conn.execute("PRAGMA schema_version").fetchone() + changes = self.conn.total_changes + STORAGE._initialize_dunbar_ownership(self.conn) + self.assertEqual(self.conn.execute("PRAGMA schema_version").fetchone(), version) + self.assertEqual(self.conn.total_changes, changes) + + def test_trigger_repair_failure_rolls_back_ledger_and_preserves_caller_work(self) -> None: + self.seed() + self.build() + self.conn.execute("DROP TRIGGER dunbar_relationships_ai") + self.conn.commit() + old = self.snapshot() + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('synthetic-pending','retained')") + + def deny_create(action, *unused): + return sqlite3.SQLITE_DENY if action == sqlite3.SQLITE_CREATE_TRIGGER else sqlite3.SQLITE_OK + + self.conn.set_authorizer(deny_create) + try: + with self.assertRaises(sqlite3.DatabaseError): + STORAGE._initialize_dunbar_ownership(self.conn) + finally: + self.conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.assertTrue(self.conn.in_transaction) + self.conn.commit() + self.assertEqual(self.snapshot(), old) + self.assertFalse(STORAGE._dunbar_ownership_ready(self.conn)) + self.assertEqual(self.conn.execute("SELECT value FROM metadata WHERE key='synthetic-pending'").fetchone(), ("retained",)) + + def test_trigger_migration_commit_or_release_failure_keeps_old_state(self) -> None: + self.seed() + self.build() + self.conn.execute("DROP TRIGGER dunbar_relationships_ai") + self.conn.commit() + old = self.snapshot() + for borrowed in (False, True): + with self.subTest(borrowed=borrowed): + if borrowed: + self.conn.execute("INSERT OR REPLACE INTO metadata(key,value) VALUES ('synthetic-pending','retained')") + denied = [] + + def authorizer(action, operation, name, *unused): + terminal = action == sqlite3.SQLITE_TRANSACTION and operation == "COMMIT" + if borrowed: + terminal = action == sqlite3.SQLITE_SAVEPOINT and operation == "RELEASE" and name == "truememory_dunbar_schema" + if terminal and not denied: + denied.append(operation) + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + + self.conn.set_authorizer(authorizer) + try: + with self.assertRaises(sqlite3.DatabaseError): + STORAGE._initialize_dunbar_ownership(self.conn) + finally: + self.conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.assertEqual(len(denied), 1) + self.assertEqual(self.conn.in_transaction, borrowed) + self.conn.commit() + self.assertEqual(self.snapshot(), old) + self.assertFalse(STORAGE._dunbar_ownership_ready(self.conn)) + + def test_minimal_schema_ledger_migration_and_output_rollback_with_caller(self) -> None: + conn = sqlite3.connect(":memory:") + self.addCleanup(conn.close) + conn.execute("CREATE TABLE messages(id INTEGER PRIMARY KEY,sender TEXT,recipient TEXT,timestamp TEXT)") + conn.execute("CREATE TABLE entity_relationships(id INTEGER PRIMARY KEY AUTOINCREMENT,entity_a TEXT,entity_b TEXT,relationship_type TEXT,strength REAL,dunbar_layer TEXT,last_interaction TEXT)") + conn.execute("INSERT INTO messages VALUES (1,'synthetic-primary','synthetic-contact','2026-01-01')") + self.assertEqual(len(PERSONALITY.build_dunbar_hierarchy(conn, "synthetic-primary")), 1) + self.assertTrue(conn.in_transaction) + self.assertTrue(PERSONALITY._dunbar_has_ledger(conn)) + conn.rollback() + self.assertFalse(PERSONALITY._dunbar_has_ledger(conn)) + self.assertEqual(conn.execute("SELECT * FROM messages").fetchall(), []) + self.assertEqual(conn.execute("SELECT * FROM entity_relationships").fetchall(), []) + + def test_coverage_is_one_snapshot_and_does_not_commit_caller(self) -> None: + self.seed() + self.build() + writer = self.open_db() + original = PERSONALITY._dunbar_owned_rows + + def interleave(conn): + yield from original(conn) + writer.execute("INSERT INTO entity_relationships(entity_a,entity_b,relationship_type) VALUES ('synthetic-external','synthetic-contact','contact')") + writer.commit() + + with patch.object(PERSONALITY, "_dunbar_owned_rows", interleave): + self.assertEqual(PERSONALITY.read_dunbar_coverage(self.conn)["unowned_contacts"], 0) + self.assertEqual(PERSONALITY.read_dunbar_coverage(self.conn)["unowned_contacts"], 1) + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('synthetic-pending','retained')") + PERSONALITY.read_dunbar_coverage(self.conn) + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + self.assertIsNone(self.conn.execute("SELECT value FROM metadata WHERE key='synthetic-pending'").fetchone()) + + def test_failed_coverage_read_ends_only_its_owned_snapshot(self) -> None: + self.seed() + for borrowed in (False, True): + with self.subTest(borrowed=borrowed): + if borrowed: + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('synthetic-pending','retained')") + with patch.object(PERSONALITY, "_dunbar_owned_rows", side_effect=ValueError("synthetic")): + with self.assertRaises(ValueError): + PERSONALITY.read_dunbar_coverage(self.conn) + self.assertEqual(self.conn.in_transaction, borrowed) + self.conn.rollback() + self.assertIsNone(self.conn.execute("SELECT value FROM metadata WHERE key='synthetic-pending'").fetchone()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-episode-index-writes-751.py b/tests/test-episode-index-writes-751.py new file mode 100644 index 00000000..5363b109 --- /dev/null +++ b/tests/test-episode-index-writes-751.py @@ -0,0 +1,577 @@ +"""Real SQLite regressions, with only the two stdlib production modules loaded.""" + +import ast +import sqlite3 +import tempfile +import threading +import types +import unittest +from collections.abc import Callable +from pathlib import Path +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] + + +def load_modules() -> tuple[types.ModuleType, types.ModuleType]: + storage = types.ModuleType("synthetic_episode_storage") + temporal = types.ModuleType("synthetic_episode_temporal") + for module, name in ((storage, "storage"), (temporal, "temporal")): + path = ROOT / "truememory" / f"{name}.py" + tree = ast.parse(path.read_text(), filename=str(path)) + # Avoid package initialization and optional native dependencies. All + # production functions and SQL in these two modules remain unchanged. + tree.body = [node for node in tree.body if not ( + isinstance(node, ast.ImportFrom) and node.module == "truememory.storage" + )] + if name == "temporal": + module._row_to_dict = storage._row_to_dict + module.select_message_cols = storage.select_message_cols + exec(compile(tree, str(path), "exec"), module.__dict__) + return storage, temporal + + +STORAGE, TEMPORAL = load_modules() + +LEGACY_TRIGGER = """ +CREATE TRIGGER messages_au AFTER UPDATE ON messages BEGIN + DELETE FROM messages_fts WHERE rowid = old.id; + INSERT INTO messages_fts(rowid, content, sender, recipient, category, modality) + VALUES (new.id, new.content, new.sender, new.recipient, new.category, new.modality); +END; +""" + +AUDIT_SQL = """ +CREATE TABLE synthetic_write_audit (event TEXT, row_id INTEGER); +CREATE TRIGGER synthetic_message_update AFTER UPDATE ON messages BEGIN + INSERT INTO synthetic_write_audit VALUES ('message_update', new.id); +END; +CREATE TRIGGER synthetic_episode_insert AFTER INSERT ON episodes BEGIN + INSERT INTO synthetic_write_audit VALUES ('episode_insert', new.id); +END; +CREATE TRIGGER synthetic_episode_update AFTER UPDATE ON episodes BEGIN + INSERT INTO synthetic_write_audit VALUES ('episode_update', new.id); +END; +CREATE TRIGGER synthetic_episode_delete AFTER DELETE ON episodes BEGIN + INSERT INTO synthetic_write_audit VALUES ('episode_delete', old.id); +END; +CREATE TRIGGER synthetic_fts_insert AFTER INSERT ON messages_fts_content BEGIN + INSERT INTO synthetic_write_audit VALUES ('fts_insert', new.id); +END; +CREATE TRIGGER synthetic_fts_delete AFTER DELETE ON messages_fts_content BEGIN + INSERT INTO synthetic_write_audit VALUES ('fts_delete', old.id); +END; +CREATE TRIGGER synthetic_fts_update AFTER UPDATE ON messages_fts_content BEGIN + INSERT INTO synthetic_write_audit VALUES ('fts_update', new.id); +END; +""" + + +class EpisodeFixture(unittest.TestCase): + def setUp(self) -> None: + self.temp = tempfile.TemporaryDirectory(prefix="synthetic-episodes-") + self.addCleanup(self.temp.cleanup) + self.path = Path(self.temp.name) / "synthetic.sqlite" + self.conn = self.open_db() + self.conn.executescript(AUDIT_SQL) + + def open_db(self) -> sqlite3.Connection: + conn = STORAGE.create_db(self.path) + self.addCleanup(conn.close) + return conn + + def add(self, timestamps: list[str | None]) -> list[int]: + ids = [] + for timestamp in timestamps: + cursor = self.conn.execute( + "INSERT INTO messages(content, sender, recipient, timestamp, category, modality) " + "VALUES ('synthetic cedar', 'senderold', 'recipientold', ?, 'categoryold', 'modalityold')", + (timestamp,), + ) + ids.append(cursor.lastrowid) + self.conn.commit() + self.clear_audit() + return ids + + def clear_audit(self) -> None: + self.conn.execute("DELETE FROM synthetic_write_audit") + self.conn.commit() + + def audit(self) -> dict[str, int]: + return dict(self.conn.execute( + "SELECT event, count(*) FROM synthetic_write_audit GROUP BY event" + )) + + def search(self, query: str, conn: sqlite3.Connection | None = None) -> list[int]: + return [row[0] for row in (conn or self.conn).execute( + "SELECT rowid FROM messages_fts WHERE messages_fts MATCH ? ORDER BY rowid", (query,) + )] + + def snapshot(self) -> tuple[list[tuple], list[tuple], list[tuple]]: + return ( + self.conn.execute("SELECT * FROM messages ORDER BY id").fetchall(), + self.conn.execute("SELECT * FROM episodes ORDER BY id").fetchall(), + self.conn.execute("SELECT rowid, * FROM messages_fts ORDER BY rowid").fetchall(), + ) + + def groups(self) -> list[tuple[int, tuple[int, ...]]]: + return [(row[0], tuple(member[0] for member in self.conn.execute( + "SELECT id FROM messages WHERE episode_id = ? ORDER BY id", (row[0],) + ))) for row in self.conn.execute("SELECT id FROM episodes ORDER BY id").fetchall()] + + def start_worker(self, action: Callable[[], object]) -> tuple[threading.Thread, list[object]]: + outcomes = [] + + def run() -> None: + try: + outcomes.append(action()) + except Exception as error: # Return worker failures to the asserting test. + outcomes.append(error) + + worker = threading.Thread(target=run) + worker.start() + self.addCleanup(worker.join, 3) + return worker, outcomes + + +class TestFtsUpdateWrites(EpisodeFixture): + def test_metadata_and_equal_indexed_values_do_not_rewrite_fts(self) -> None: + ids = self.add(["2026-01-01T00:00:00"]) + self.conn.execute( + "UPDATE messages SET episode_id = 7, timestamp = '2026-01-02', " + "emotional_valence = 0.5, directive = 1, metadata = '{\"synthetic\": true}'" + ) + self.conn.execute( + "UPDATE messages SET id = id, content = content, sender = sender, " + "recipient = recipient, category = category, modality = modality" + ) + self.assertEqual(self.audit(), {"message_update": 2}) + self.assertEqual(self.search("cedar"), ids) + + def test_each_indexed_value_updates_real_search_results(self) -> None: + ids = self.add(["2026-01-01"]) + for column in ("content", "sender", "recipient", "category", "modality"): + with self.subTest(column=column): + old = "cedar" if column == "content" else f"{column}old" + new = f"{column}new" + self.clear_audit() + self.conn.execute(f"UPDATE messages SET {column} = ?", (new,)) + self.assertEqual(self.search(f"{column}:{old}"), []) + self.assertEqual(self.search(f"{column}:{new}"), ids) + self.assertEqual(self.audit(), { + "fts_delete": 1, "fts_insert": 1, "message_update": 1, + }) + + def test_nullable_indexed_values_use_null_safe_comparison(self) -> None: + ids = self.add(["2026-01-01"]) + for column in ("sender", "recipient", "category", "modality"): + with self.subTest(column=column): + self.conn.execute(f"UPDATE messages SET {column} = NULL") + self.assertEqual(self.search(f"{column}:{column}old"), []) + self.clear_audit() + self.conn.execute(f"UPDATE messages SET {column} = NULL") + self.assertEqual(self.audit(), {"message_update": 1}) + self.clear_audit() + self.conn.execute(f"UPDATE messages SET {column} = ?", (f"{column}new",)) + self.assertEqual(self.search(f"{column}:{column}new"), ids) + self.assertEqual(self.audit().get("fts_insert"), 1) + + def test_id_and_all_rowid_aliases_keep_fts_rowids_current(self) -> None: + self.add(["2026-01-01"]) + for new_id, alias in enumerate(("id", "rowid", "_rowid_", "oid"), 101): + with self.subTest(alias=alias): + self.clear_audit() + self.conn.execute(f"UPDATE messages SET {alias} = ?", (new_id,)) + self.assertEqual(self.search("cedar"), [new_id]) + self.assertEqual(self.conn.execute( + "SELECT rowid FROM messages_fts" + ).fetchall(), [(new_id,)]) + self.assertEqual(self.audit().get("fts_delete"), 1) + + def install_legacy(self) -> None: + self.conn.execute("DROP TRIGGER messages_au") + self.conn.execute(LEGACY_TRIGGER) + self.conn.commit() + + def test_existing_database_migration_preserves_data_and_is_idempotent(self) -> None: + self.add(["2026-01-01"]) + self.install_legacy() + before = self.snapshot() + version = self.conn.execute("PRAGMA schema_version").fetchone()[0] + opened = self.open_db() + self.assertEqual(opened.execute("PRAGMA schema_version").fetchone()[0], version + 2) + self.assertEqual(self.snapshot(), before) + self.assertEqual(self.audit(), {"fts_delete": 1, "fts_insert": 1}) + self.clear_audit() + reopened = self.open_db() + self.assertEqual(reopened.execute("PRAGMA schema_version").fetchone()[0], version + 2) + self.assertEqual(self.audit(), {}) + reopened.execute("UPDATE messages SET episode_id = 5") + reopened.commit() + self.assertEqual(self.audit(), {"message_update": 1}) + self.assertEqual(self.search("cedar", reopened), [1]) + + def test_failed_migration_restores_old_trigger_and_caller_transaction(self) -> None: + self.add(["2026-01-01"]) + self.install_legacy() + before = self.snapshot() + old_sql = self.conn.execute("SELECT sql FROM sqlite_master WHERE name = 'messages_au'").fetchone()[0] + + def deny_create(action: int, name: str, *unused: str | None) -> int: + return sqlite3.SQLITE_DENY if action == sqlite3.SQLITE_CREATE_TRIGGER and name == "messages_au" else sqlite3.SQLITE_OK + + for caller_owned in (False, True): + with self.subTest(caller_owned=caller_owned): + if caller_owned: + self.conn.execute("INSERT INTO metadata(key, value) VALUES ('synthetic-uncommitted', 'pending')") + self.conn.set_authorizer(deny_create) + try: + with self.assertRaises(sqlite3.DatabaseError): + STORAGE._migrate_messages_fts_trigger(self.conn) + finally: + # Disabling with None is supported only on Python 3.11+. + self.conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.assertEqual(self.conn.in_transaction, caller_owned) + self.assertEqual(self.snapshot(), before) + self.assertEqual(self.conn.execute( + "SELECT sql FROM sqlite_master WHERE name = 'messages_au'" + ).fetchone()[0], old_sql) + if caller_owned: + self.assertEqual(self.conn.execute( + "SELECT value FROM metadata WHERE key = 'synthetic-uncommitted'" + ).fetchone(), ("pending",)) + self.conn.rollback() + + def test_simultaneous_migrators_recheck_after_writer_lock(self) -> None: + self.install_legacy() + second = sqlite3.connect(self.path, check_same_thread=False) + self.addCleanup(second.close) + second.execute("PRAGMA busy_timeout = 2000") + entered = threading.Event() + release = threading.Event() + second_started = threading.Event() + traces = [[], []] + + def first_trace(statement: str) -> None: + traces[0].append(statement) + if statement.startswith("DROP TRIGGER"): + entered.set() + release.wait(2) + + def second_trace(statement: str) -> None: + traces[1].append(statement) + if statement == "BEGIN IMMEDIATE": + second_started.set() + + self.conn.set_trace_callback(first_trace) + second.set_trace_callback(second_trace) + first, first_results = self.start_worker(lambda: STORAGE._migrate_messages_fts_trigger(self.conn)) + self.addCleanup(release.set) + self.assertTrue(entered.wait(2)) + other, other_results = self.start_worker(lambda: STORAGE._migrate_messages_fts_trigger(second)) + self.assertTrue(second_started.wait(2)) + release.set() + first.join(2) + other.join(2) + self.assertEqual(first_results, [None]) + self.assertEqual(other_results, [None]) + self.assertEqual(sum(sql.startswith("DROP TRIGGER") for sql in traces[0]), 1) + self.assertFalse(any(sql.startswith("DROP TRIGGER") for sql in traces[1])) + + +class TestEpisodeDeltas(EpisodeFixture): + def test_first_pass_assigns_once_and_unchanged_pass_writes_nothing(self) -> None: + ids = self.add([f"2026-01-01T{hour:02}:00:00" for hour in range(12)]) + self.assertEqual(TEMPORAL.detect_episodes(self.conn), 1) + self.assertEqual(self.audit(), {"episode_insert": 1, "message_update": 12}) + self.assertEqual(self.search("cedar"), ids) + before = self.snapshot() + self.clear_audit() + changes = self.conn.total_changes + self.assertEqual(TEMPORAL.detect_episodes(self.conn), 1) + self.assertEqual(self.conn.total_changes, changes) + self.assertEqual(self.audit(), {}) + self.assertEqual(self.snapshot(), before) + + def test_original_gap_timezone_invalid_and_empty_semantics(self) -> None: + ids = self.add([ + None, "", "0000", "2026-01-01T00:00:00+05:00", + "2026-01-01T06:00:00Z", "2026-01-01T12:00:01-05:00", + "2026-01-01T18:00:01", "not-a-time", "zzz", + ]) + self.assertEqual(TEMPORAL.detect_episodes(self.conn), 2) + self.assertEqual([members for _, members in self.groups()], [tuple(ids[2:5]), tuple(ids[5:])]) + self.assertEqual(self.conn.execute( + "SELECT id FROM messages WHERE episode_id IS NULL ORDER BY id" + ).fetchall(), [(ids[0],), (ids[1],)]) + + def test_addition_changes_only_affected_group_and_preserves_other_id(self) -> None: + ids = self.add(["2026-01-01T00:00:00", "2026-01-01T01:00:00", "2026-01-03T00:00:00"]) + TEMPORAL.detect_episodes(self.conn) + old = self.groups() + added = self.add(["2026-01-01T02:00:00"])[0] + TEMPORAL.detect_episodes(self.conn) + new = self.groups() + self.assertIn(old[1], new) + self.assertNotIn(old[0][0], [eid for eid, _ in new]) + self.assertIn(tuple(ids[:2] + [added]), [members for _, members in new]) + self.assertEqual(self.audit(), {"episode_delete": 1, "episode_insert": 1, "message_update": 3}) + + def test_merge_and_split_use_new_ids(self) -> None: + ids = self.add(["2026-01-01T00:00:00", "2026-01-01T12:00:00"]) + TEMPORAL.detect_episodes(self.conn) + old_ids = {eid for eid, _ in self.groups()} + bridge = self.add(["2026-01-01T06:00:00"])[0] + self.assertEqual(TEMPORAL.detect_episodes(self.conn), 1) + merged_id = self.groups()[0][0] + self.assertNotIn(merged_id, old_ids) + self.assertEqual(self.groups()[0][1], tuple(ids + [bridge])) + self.clear_audit() + self.assertEqual(TEMPORAL.detect_episodes(self.conn, gap_hours=5), 3) + self.assertTrue(all(eid not in old_ids | {merged_id} for eid, _ in self.groups())) + self.assertEqual(self.audit(), {"episode_delete": 1, "episode_insert": 3, "message_update": 3}) + + def test_deleted_member_does_not_reuse_incomplete_old_episode(self) -> None: + ids = self.add(["2026-01-01T00:00:00", "2026-01-01T01:00:00"]) + TEMPORAL.detect_episodes(self.conn) + old_id = self.groups()[0][0] + self.conn.execute("DELETE FROM messages WHERE id = ?", (ids[1],)) + self.clear_audit() + TEMPORAL.detect_episodes(self.conn) + self.assertNotEqual(self.groups()[0][0], old_id) + self.assertEqual(self.groups()[0][1], (ids[0],)) + self.assertEqual(self.audit(), {"episode_delete": 1, "episode_insert": 1, "message_update": 1}) + + def test_timestamp_change_with_same_members_updates_only_bounds(self) -> None: + self.add(["2026-01-01T00:00:00", "2026-01-01T01:00:00"]) + TEMPORAL.detect_episodes(self.conn) + old = self.groups() + self.conn.execute("UPDATE messages SET timestamp = '2026-01-01T02:00:00' WHERE id = 2") + self.clear_audit() + TEMPORAL.detect_episodes(self.conn) + self.assertEqual(self.groups(), old) + self.assertEqual(self.audit(), {"episode_update": 1}) + self.assertEqual(self.conn.execute("SELECT end_time FROM episodes").fetchone(), ("2026-01-01T02:00:00",)) + + def test_nonempty_summary_invalidated_once_even_with_same_member_ids(self) -> None: + self.add(["2026-01-01"]) + TEMPORAL.detect_episodes(self.conn) + old = self.groups() + self.conn.execute("UPDATE episodes SET summary = 'synthetic derived summary'") + self.conn.execute("UPDATE messages SET content = 'synthetic maple'") + self.clear_audit() + TEMPORAL.detect_episodes(self.conn) + self.assertEqual(self.groups(), old) + self.assertEqual(self.conn.execute("SELECT summary FROM episodes").fetchone(), ("",)) + self.assertEqual(self.audit(), {"episode_update": 1}) + self.clear_audit() + TEMPORAL.detect_episodes(self.conn) + self.assertEqual(self.audit(), {}) + + def test_zero_eligible_rows_remove_stale_assignments_without_deleting_messages(self) -> None: + ids = self.add(["2026-01-01", "2026-01-02"]) + TEMPORAL.detect_episodes(self.conn) + self.conn.execute("UPDATE messages SET timestamp = CASE id WHEN 1 THEN '' ELSE NULL END") + self.clear_audit() + self.assertEqual(TEMPORAL.detect_episodes(self.conn), 0) + self.assertEqual(self.groups(), []) + self.assertEqual(self.search("cedar"), ids) + self.assertEqual(self.audit(), {"episode_delete": 2, "message_update": 2}) + self.clear_audit() + TEMPORAL.detect_episodes(self.conn) + self.assertEqual(self.audit(), {}) + + def test_list_row_factory_does_not_create_false_metadata_deltas(self) -> None: + self.add(["2026-01-01"]) + TEMPORAL.detect_episodes(self.conn) + self.clear_audit() + self.conn.row_factory = lambda cursor, row: list(row) + TEMPORAL.detect_episodes(self.conn) + self.assertEqual(self.audit(), {}) + + +class TestEpisodeTransactions(EpisodeFixture): + def test_caller_owns_commit_and_can_rollback_all_changes(self) -> None: + self.add(["2026-01-01"]) + reader = self.open_db() + before = self.snapshot() + self.conn.execute("INSERT INTO metadata(key, value) VALUES ('synthetic-caller', 'pending')") + self.assertEqual(TEMPORAL.detect_episodes(self.conn), 1) + self.assertTrue(self.conn.in_transaction) + self.assertEqual(reader.execute("SELECT count(*) FROM episodes").fetchone(), (0,)) + self.assertIsNone(reader.execute("SELECT value FROM metadata WHERE key = 'synthetic-caller'").fetchone()) + self.conn.rollback() + self.assertEqual(self.snapshot(), before) + self.assertIsNone(self.conn.execute("SELECT value FROM metadata WHERE key = 'synthetic-caller'").fetchone()) + TEMPORAL.detect_episodes(self.conn) + self.assertFalse(self.conn.in_transaction) + self.assertEqual(reader.execute("SELECT count(*) FROM episodes").fetchone(), (1,)) + + def test_assignment_failure_rolls_back_partial_writes_and_preserves_caller_work(self) -> None: + self.add(["2026-01-01T00:00:00", "2026-01-01T01:00:00"]) + before = self.snapshot() + self.conn.executescript(""" + CREATE TRIGGER synthetic_fail_assignment BEFORE UPDATE OF episode_id ON messages + WHEN new.id = 2 BEGIN SELECT RAISE(ABORT, 'synthetic assignment failure'); END; + """) + for caller_owned in (False, True): + with self.subTest(caller_owned=caller_owned): + if caller_owned: + self.conn.execute("INSERT INTO metadata(key, value) VALUES ('synthetic-caller', 'pending')") + with self.assertRaisesRegex(sqlite3.IntegrityError, "synthetic assignment failure"): + TEMPORAL.detect_episodes(self.conn) + self.assertEqual(self.conn.in_transaction, caller_owned) + self.assertEqual(self.snapshot(), before) + self.assertEqual(self.audit(), {}) + if caller_owned: + self.assertEqual(self.conn.execute("SELECT value FROM metadata WHERE key = 'synthetic-caller'").fetchone(), ("pending",)) + self.conn.rollback() + + def test_episode_delete_failure_restores_old_assignments_and_ids(self) -> None: + self.add(["2026-01-01"]) + TEMPORAL.detect_episodes(self.conn) + self.add(["2026-01-01T01:00:00"]) + before = self.snapshot() + self.conn.executescript(""" + CREATE TRIGGER synthetic_fail_delete BEFORE DELETE ON episodes + BEGIN SELECT RAISE(ABORT, 'synthetic deletion failure'); END; + """) + with self.assertRaisesRegex(sqlite3.IntegrityError, "synthetic deletion failure"): + TEMPORAL.detect_episodes(self.conn) + self.assertEqual(self.snapshot(), before) + self.assertEqual(self.audit(), {}) + self.assertFalse(self.conn.in_transaction) + + def test_writer_commit_during_grouping_retries_fresh_snapshot(self) -> None: + self.add(["2026-01-01"]) + writer = self.open_db() + reader = self.open_db() + entered = threading.Event() + release = threading.Event() + calls = [] + group = TEMPORAL._group_episode_rows + + def paused_group(rows: list[tuple], gap: float) -> list[list[tuple]]: + calls.append(len(rows)) + if len(calls) == 1: + entered.set() + release.wait(2) + return group(rows, gap) + + with patch.object(TEMPORAL, "_group_episode_rows", paused_group): + worker, outcomes = self.start_worker(lambda: TEMPORAL.detect_episodes(self.conn)) + self.addCleanup(release.set) + self.assertTrue(entered.wait(2)) + writer.execute("INSERT INTO messages(content, timestamp) VALUES ('synthetic maple', '2026-01-01T01:00:00')") + writer.commit() + self.assertEqual(self.search("maple", reader), [2]) + release.set() + worker.join(2) + self.assertEqual(outcomes, [1]) + self.assertEqual(calls, [1, 2]) + self.assertEqual(self.groups()[0][1], (1, 2)) + self.assertFalse(self.conn.in_transaction) + + def test_caller_snapshot_conflict_is_not_restarted_or_committed(self) -> None: + self.add(["2026-01-01"]) + writer = self.open_db() + self.conn.execute("BEGIN") + self.conn.execute("SELECT count(*) FROM messages").fetchone() + writer.execute("INSERT INTO messages(content, timestamp) VALUES ('synthetic maple', '2026-01-01T01:00:00')") + writer.commit() + with self.assertRaises(sqlite3.OperationalError): + TEMPORAL.detect_episodes(self.conn) + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.conn.execute("SELECT count(*) FROM messages").fetchone(), (1,)) + self.assertEqual(self.conn.execute("SELECT count(*) FROM episodes").fetchone(), (0,)) + self.conn.rollback() + self.assertEqual(self.conn.execute("SELECT count(*) FROM messages").fetchone(), (2,)) + + def test_snapshot_retries_are_bounded_at_three_attempts(self) -> None: + self.add(["2026-01-01"]) + writer = self.open_db() + group = TEMPORAL._group_episode_rows + calls = [] + + def conflicting_group(rows: list[tuple], gap: float) -> list[list[tuple]]: + calls.append(1) + writer.execute("INSERT OR REPLACE INTO metadata(key, value) VALUES ('synthetic-writer', ?)", (str(len(calls)),)) + writer.commit() + return group(rows, gap) + + with patch.object(TEMPORAL, "_group_episode_rows", conflicting_group): + with self.assertRaises(sqlite3.OperationalError): + TEMPORAL.detect_episodes(self.conn) + self.assertEqual(len(calls), 3) + self.assertFalse(self.conn.in_transaction) + self.assertEqual(self.groups(), []) + self.assertEqual(self.audit(), {}) + + def test_python310_snapshot_retry_without_extended_error_attribute(self) -> None: + self.add(["2026-01-01"]) + writer = self.open_db() + reconcile = TEMPORAL._reconcile_episodes + calls = [] + + def legacy_error(conn: sqlite3.Connection, gap: float) -> int: + calls.append(1) + if len(calls) == 1: + conn.execute("SELECT count(*) FROM messages").fetchone() + writer.execute("INSERT INTO messages(content, timestamp) VALUES ('synthetic maple', '2026-01-01T01:00:00')") + writer.commit() + # Python 3.10 errors lack sqlite_errorcode. The changed + # connection data_version must justify retrying this error. + raise sqlite3.OperationalError("database is locked") + return reconcile(conn, gap) + + with patch.object(TEMPORAL, "_reconcile_episodes", legacy_error): + self.assertEqual(TEMPORAL.detect_episodes(self.conn), 1) + self.assertEqual(len(calls), 2) + self.assertEqual(self.groups()[0][1], (1, 2)) + + def test_ordinary_lock_errors_without_changed_snapshot_do_not_retry(self) -> None: + self.add(["2026-01-01"]) + for error_code in (None, 5): # SQLITE_BUSY; also works on Python 3.10. + with self.subTest(error_code=error_code): + error = sqlite3.OperationalError("database is locked") + if error_code is not None: + error.sqlite_errorcode = error_code + with patch.object(TEMPORAL, "_reconcile_episodes", side_effect=error) as reconcile: + with self.assertRaises(sqlite3.OperationalError): + TEMPORAL.detect_episodes(self.conn) + self.assertEqual(reconcile.call_count, 1) + self.assertFalse(self.conn.in_transaction) + + def test_wal_search_reader_proceeds_while_episode_writer_is_held(self) -> None: + ids = self.add(["2026-01-01"]) + reader = self.open_db() + entered = threading.Event() + release = threading.Event() + real_conn = self.conn + + class PausedAfterWrite: + def __getattr__(self, name: str) -> object: + return getattr(real_conn, name) + + def execute(self, sql: str, parameters: tuple = ()) -> sqlite3.Cursor: + cursor = real_conn.execute(sql, parameters) + if sql.startswith("INSERT INTO episodes"): + entered.set() + release.wait(2) + return cursor + + worker, outcomes = self.start_worker(lambda: TEMPORAL.detect_episodes(PausedAfterWrite())) + self.addCleanup(release.set) + self.assertTrue(entered.wait(2)) + search_worker, search_outcomes = self.start_worker(lambda: self.search("cedar", reader)) + search_worker.join(1) + self.assertFalse(search_worker.is_alive(), "WAL reader waited for the episode writer") + self.assertEqual(search_outcomes, [ids]) + release.set() + worker.join(2) + self.assertEqual(outcomes, [1]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-hermes-native-bridge-765.py b/tests/test-hermes-native-bridge-765.py new file mode 100644 index 00000000..d67cd039 --- /dev/null +++ b/tests/test-hermes-native-bridge-765.py @@ -0,0 +1,689 @@ +"""Synthetic native bridge controls using actual source and inert collaborators. + +No package entry point, native host, model or database is imported. Fixture +trees, including failed attempts, are retained. Local runs use an announced +exclusive TRUEMEMORY_HERMES_TEST_ROOT; hosted runs retain a temporary tree. +""" +from __future__ import annotations + +import ast +import io +import json +import os +import subprocess +import sys +import tempfile +import types +import unittest +from concurrent.futures import ThreadPoolExecutor +from contextlib import contextmanager +from pathlib import Path +from typing import Iterator +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] + + +def load_source(path: Path, name: str) -> types.ModuleType: + module = types.ModuleType(name) + module.__file__ = str(path) + sys.modules[name] = module + exec(compile(path.read_text(encoding="utf-8"), str(path), "exec"), module.__dict__) + return module + + +BRIDGE = load_source(ROOT / "truememory/ingest/hooks/hermes.py", "synthetic_hermes765") +TRANSCRIPT = load_source(ROOT / "truememory/ingest/transcript.py", "synthetic_transcript765") + + +@contextmanager +def collaborators(children: dict[str, types.ModuleType]) -> Iterator[None]: + modules: dict[str, types.ModuleType] = {} + for name in ("truememory", "truememory.ingest", "truememory.ingest.hooks", "truememory.hooks"): + module = types.ModuleType(name) + module.__path__ = [] + modules[name] = module + modules.update(children) + with patch.dict(sys.modules, modules): + yield + + +def inert_shared() -> types.ModuleType: + module = types.ModuleType("truememory.ingest.hooks._shared") + module.check_extraction_budget = lambda: True + module.refund_extraction_budget = lambda: None + return module + + +def payload(session: str = "synthetic-session", turn: str = "synthetic-turn", *, + history: list[dict[str, object]] | None = None, post: bool = False) -> dict[str, object]: + extra: dict[str, object] = { + "turn_id": turn, "task_id": "synthetic-task", "user_message": "I prefer synthetic blue widgets.", + "conversation_history": history or [], "sender_id": "untrusted-other-account", + } + if post: + extra["assistant_response"] = "Synthetic assistant reply." + return {"session_id": session, "extra": extra, "event": "untrusted-event", + "user_id": "untrusted-other-account", "transcript_path": "/never/read/native/input"} + + +class HermesNativeBridgeControls(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + explicit = os.environ.get("TRUEMEMORY_HERMES_TEST_ROOT") + cls.fixture_root = Path(explicit) if explicit else Path(tempfile.mkdtemp(prefix="hermes765-")) + if explicit: + cls.fixture_root.mkdir(parents=True, exist_ok=False) + + def setUp(self) -> None: + self.case = self.fixture_root / self._testMethodName + self.case.mkdir() + self.store = BRIDGE.Store(self.case / "owned", "synthetic-user", str(self.case / "synthetic.db"), + "synthetic-session") + + def test_exact_envelope_identity_and_configured_account(self) -> None: + key = self.store.capture(payload(), "pre_llm_call") + state = self.store.state() + snapshot = self.store.snapshot(next(iter(state["captures"]))) + self.assertEqual(snapshot["user"], "synthetic-user") + self.assertEqual(snapshot["db"], str(self.case / "synthetic.db")) + self.assertEqual(snapshot["native_user_message"], "I prefer synthetic blue widgets.") + self.assertEqual(state["turns"][key]["ordinal"], 1) + self.assertNotIn("transcript_path", snapshot) + + def test_same_turn_replay_and_equal_text_new_turn_session(self) -> None: + first = self.store.capture(payload(), "pre_llm_call") + self.assertEqual(first, self.store.capture(payload(), "pre_llm_call")) + self.assertEqual(len(self.store.state()["captures"]), 1) + second = self.store.capture(payload(turn="next-turn"), "pre_llm_call") + self.assertNotEqual(first, second) + self.assertEqual(self.store.state()["turns"][second]["ordinal"], 2) + other = BRIDGE.Store(self.store.root, self.store.user, self.store.db, "other-session") + other_key = other.capture(payload(session="other-session"), "pre_llm_call") + self.assertNotEqual(first, other_key) + self.assertEqual(len(other.state()["captures"]), 1) + + def test_missing_invalid_identity_has_no_unknown_fallback(self) -> None: + for value in (None, [], 1, "", " ", "unknown", "a\n", "x" * 1025): + with self.subTest(value_type=type(value).__name__): + with self.assertRaises(BRIDGE.BridgeError): + BRIDGE._identity(value, "session_id") + event = payload() + event["extra"]["turn_id"] = None + with self.assertRaises(BRIDGE.BridgeError): + self.store.capture(event, "pre_llm_call") + self.assertEqual(self.store.state()["captures"], {}) + + def test_capture_rejects_cross_session_payload(self) -> None: + with self.assertRaises(BRIDGE.BridgeError): + self.store.capture(payload(session="wrong-session"), "pre_llm_call") + self.assertEqual(self.store.state()["captures"], {}) + + def test_history_current_text_and_supported_parts_no_sidecars(self) -> None: + event = payload(history=[{"role": "user", "content": "Older native user.", + "api_content": "injected API sidecar"}]) + event["extra"]["user_message"] = [{"type": "text", "text": "Native current text."}, + {"type": "image_url", "image_url": "ignored"}] + self.store.capture(event, "pre_llm_call") + snapshot = self.store.snapshot(next(iter(self.store.state()["captures"]))) + self.assertEqual([row["content"] for row in snapshot["messages"]], + ["Older native user.", "Native current text."]) + self.assertEqual(snapshot["native_history"], event["extra"]["conversation_history"]) + self.assertNotIn("injected API sidecar", json.dumps(snapshot["messages"])) + + def test_repeated_history_text_retains_both_native_rows(self) -> None: + current = payload()["extra"]["user_message"] + event = payload(history=[{"role": "user", "content": current}, + {"role": "assistant", "content": "Earlier answer."}, + {"role": "user", "content": current}]) + self.store.capture(event, "pre_llm_call") + snapshot = self.store.snapshot(next(iter(self.store.state()["captures"]))) + self.assertEqual(len(snapshot["messages"]), 3) + self.assertEqual(sum(row["content"] == current for row in snapshot["messages"]), 2) + + def test_post_full_native_assistant_capture_and_end_flush(self) -> None: + event = payload(post=True) + event["extra"]["assistant_response"] = "Synthetic assistant " + "a" * 5000 + self.store.capture(event, "post_llm_call") + name = next(iter(self.store.state()["captures"])) + before = (self.store.path / name).read_bytes() + snapshot = self.store.snapshot(name) + self.assertEqual(snapshot["messages"][-1]["content"], event["extra"]["assistant_response"]) + self.store.flush() + self.assertTrue(self.store.state()["captures"][name]["flush"]) + self.assertEqual((self.store.path / name).read_bytes(), before) + + def test_malformed_supported_text_and_nonlist_history_rejected(self) -> None: + for value in (None, {"text": "wrong"}, [{"type": "text", "text": 7}], [{"type": "unknown"}]): + event = payload() + event["extra"]["user_message"] = value + with self.assertRaises(BRIDGE.BridgeError): + self.store.capture(event, "pre_llm_call") + event = payload() + event["extra"]["conversation_history"] = {} + with self.assertRaises(BRIDGE.BridgeError): + self.store.capture(event, "pre_llm_call") + self.assertEqual(self.store.state()["captures"], {}) + + def test_empty_current_text_does_not_recall_historical_input(self) -> None: + event = payload(history=[{"role": "user", "content": "Historical recall question?"}]) + event["extra"]["user_message"] = "" + self.store.capture(event, "pre_llm_call") + snapshot = self.store.snapshot(next(iter(self.store.state()["captures"]))) + session_start = types.ModuleType("truememory.ingest.hooks.session_start") + prompt = types.ModuleType("truememory.ingest.hooks.user_prompt_submit") + with collaborators({session_start.__name__: session_start, prompt.__name__: prompt}): + self.assertEqual(BRIDGE._recall(snapshot, 1), "") + self.assertEqual(snapshot["messages"][-1]["content"], "") + + def test_input_is_bounded_malformed_numbers_and_roots_rejected(self) -> None: + with patch.object(BRIDGE, "MAX_INPUT_BYTES", 20): + with self.assertRaises(BRIDGE.BridgeError): + BRIDGE._input(io.StringIO("x" * 21)) + with self.assertRaises(BRIDGE.BridgeError): + BRIDGE._input(io.StringIO('"' + "é" * 11 + '"')) + for text in ("[]", "null", '{"x":NaN}'): + with self.assertRaises(BRIDGE.BridgeError): + BRIDGE._input(io.StringIO(text)) + + def test_quota_aggregate_retains_accepted_bytes_and_releases_locks(self) -> None: + self.store.capture(payload(), "pre_llm_call") + before = {path: path.read_bytes() for path in self.store.path.iterdir() if path.suffix == ".json"} + second = BRIDGE.Store(self.store.root, self.store.user, self.store.db, "other-session") + size, _count = self.store.usage() + with patch.object(BRIDGE, "MAX_SCOPE_BYTES", size): + with self.assertRaises(BRIDGE.BridgeError): + second.capture(payload(session="other-session"), "pre_llm_call") + self.assertEqual(before, {path: path.read_bytes() for path in before}) + with BRIDGE._lock(self.store.root / "quota.lock") as allowed: + self.assertTrue(allowed) + with BRIDGE._lock(second.capture_lock) as allowed: + self.assertTrue(allowed) + self.assertEqual(second.state()["captures"], {}) + + def test_context_status_quota_is_also_bounded(self) -> None: + before = self.store.usage()[0] + with patch.object(BRIDGE, "MAX_SCOPE_BYTES", before + 10): + with self.assertRaises(BRIDGE.BridgeError): + self.store.write(self.store.path / "result.json", {"context": "c" * 100}) + self.assertFalse((self.store.path / "result.json").exists()) + + def test_owned_snapshot_hash_and_symlink_mismatch_rejected(self) -> None: + self.store.capture(payload(), "pre_llm_call") + name = next(iter(self.store.state()["captures"])) + wrong = self.store.path / ("0" * 64 + ".json") + wrong.write_bytes((self.store.path / name).read_bytes()) + with self.assertRaises(BRIDGE.BridgeError): + self.store.snapshot(wrong.name) + link = self.store.path / "link.json" + link.symlink_to(self.store.path / name) + with self.assertRaises(BRIDGE.BridgeError): + BRIDGE._read_owned(link) + with self.assertRaises(BRIDGE.BridgeError): + self.store.snapshot("../../foreign.json") + + def test_private_directory_and_lock_symlinks_rejected(self) -> None: + foreign = self.case / "foreign" + foreign.mkdir() + link = self.case / "linked-root" + link.symlink_to(foreign, target_is_directory=True) + with self.assertRaises(BRIDGE.BridgeError): + BRIDGE.Store(link, "u", "db", "s") + self.store.worker_lock.symlink_to(self.case / "foreign-lock") + with self.assertRaises(BRIDGE.BridgeError): + with BRIDGE._lock(self.store.worker_lock): + self.fail("symlink lock admitted") + + def test_two_os_lock_owners_cannot_take_over_live_lock(self) -> None: + lock = self.store.worker_lock + with BRIDGE._lock(lock) as first: + self.assertTrue(first) + with BRIDGE._lock(lock) as second: + self.assertFalse(second) + with BRIDGE._lock(lock) as after_release: + self.assertTrue(after_release) + + def test_child_process_cannot_reclaim_live_os_lock(self) -> None: + source = ROOT / "truememory/ingest/hooks/hermes.py" + code = ("import sys,types,pathlib; p=pathlib.Path(sys.argv[1]); " + "m=types.ModuleType('lock765');m.__file__=str(p);sys.modules[m.__name__]=m;" + "exec(compile(p.read_text(),str(p),'exec'),m.__dict__); " + "lock=m._lock(pathlib.Path(sys.argv[2])); admitted=lock.__enter__(); " + "lock.__exit__(None,None,None);sys.exit(7 if admitted else 0)") + with BRIDGE._lock(self.store.worker_lock) as held: + self.assertTrue(held) + result = subprocess.run([sys.executable, "-B", "-c", code, str(source), + str(self.store.worker_lock)], capture_output=True, timeout=5) + self.assertEqual(result.returncode, 0, result.stderr.decode()) + + def test_simultaneous_sessions_share_account_quota(self) -> None: + second = BRIDGE.Store(self.store.root, self.store.user, self.store.db, "other-session") + def admit(store: BRIDGE.Store) -> bool: + event = payload(session=store.session) + event["extra"]["user_message"] = "Synthetic quota content " + "x" * 4000 + try: + store.capture(event, "pre_llm_call") + return True + except BRIDGE.BridgeError: + return False + with patch.object(BRIDGE, "MAX_SCOPE_BYTES", 12000), ThreadPoolExecutor(max_workers=2) as pool: + accepted = list(pool.map(admit, (self.store, second))) + self.assertEqual(sum(accepted), 1) + self.assertLessEqual(self.store.usage()[0], 12000) + self.assertEqual(sum(len(store.state()["captures"]) for store in (self.store, second)), 1) + + def test_windows_lock_branch_and_release_selected_source(self) -> None: + calls: list[tuple[int, int]] = [] + primitive = types.ModuleType("msvcrt") + primitive.LK_NBLCK, primitive.LK_UNLCK = 11, 12 + primitive.locking = lambda _fd, op, count: calls.append((op, count)) + with patch.dict(sys.modules, {"msvcrt": primitive}), patch.object(sys, "platform", "win32"): + with BRIDGE._lock(self.store.path / "windows.lock") as allowed: + self.assertTrue(allowed) + self.assertEqual(calls, [(11, 1), (12, 1)]) + + def test_birth_mismatch_dead_and_access_denied_are_conservative(self) -> None: + with patch.object(BRIDGE, "_birth", return_value=12.0): + self.assertTrue(BRIDGE._alive({"pid": 123, "born": 12.0})) + self.assertFalse(BRIDGE._alive({"pid": 123, "born": 11.0})) + with patch.object(BRIDGE, "_birth", return_value=None): + self.assertFalse(BRIDGE._alive({"pid": 123, "born": 12.0})) + with patch.object(BRIDGE, "_birth", side_effect=PermissionError): + self.assertTrue(BRIDGE._alive({"pid": 123, "born": 12.0})) + + def ready_context(self, context: str = "Synthetic recall") -> str: + key = self.store.capture(payload(), "pre_llm_call") + state = self.store.state() + state["turns"][key]["context_state"] = "ready" + self.store.write(self.store.path / f"{key}.context.json", + {"account": self.store.account, "turn": key, "context": context, + "sha256": BRIDGE._digest(context)}) + self.store.save(state) + return key + + def test_context_native_output_and_full_flush_marker(self) -> None: + key = self.ready_context() + output = io.StringIO() + self.assertTrue(BRIDGE._emit_context(self.store, key, output)) + self.assertEqual(json.loads(output.getvalue()), {"context": "Synthetic recall"}) + self.assertEqual(self.store.state()["turns"][key]["context_state"], "written") + + def test_failed_context_write_and_flush_preserve_ready_cache(self) -> None: + key = self.ready_context() + class BrokenWriter(io.StringIO): + def flush(self) -> None: + raise BrokenPipeError("synthetic failed flush") + with self.assertRaises(BrokenPipeError): + BRIDGE._emit_context(self.store, key, BrokenWriter()) + self.assertEqual(self.store.state()["turns"][key]["context_state"], "ready") + self.assertTrue((self.store.path / f"{key}.context.json").is_file()) + class ShortWriter(io.StringIO): + def write(self, text: str) -> int: + return len(text) - 1 + with self.assertRaises(BRIDGE.BridgeError): + BRIDGE._emit_context(self.store, key, ShortWriter()) + self.assertEqual(self.store.state()["turns"][key]["context_state"], "ready") + + def pipeline_fixture(self, status: str = "complete", stored: int = 1, + *, missing_api: bool = False, throw: bool = False, + heuristic: bool = False, empty: bool = False + ) -> tuple[dict[str, types.ModuleType], dict[str, object], + list[tuple[tuple[object, ...], dict[str, object]]], + types.ModuleType, object, object]: + calls: list[tuple[tuple[object, ...], dict[str, object]]] = [] + extractor = types.ModuleType("truememory.ingest.extractor") + fact = object() + fact_list = [] if empty else [fact] + def outcome(*args: object, **kwargs: object) -> object: + calls.append((args, kwargs)) + return types.SimpleNamespace(facts=fact_list, complete=status == "complete", status=status) + if not missing_api: + extractor.extract_facts_outcome = outcome + pipeline = types.ModuleType("truememory.ingest.pipeline") + original = lambda *_args, **_kwargs: fact_list + pipeline.extract_facts = original + pipeline.extract_facts_simple = lambda _text: fact_list + config = object() + class Pipeline: + def __init__(self, **kwargs: object) -> None: + self.kwargs = kwargs + def ingest_text(self, text: str, *, session_id: str) -> object: + facts = pipeline.extract_facts_simple(text) if heuristic else pipeline.extract_facts(text, config) + if throw: + raise RuntimeError("synthetic pipeline failure") + return types.SimpleNamespace(facts_extracted=len(facts), facts_stored=stored, + facts_updated=0, facts_skipped_gate=0, + facts_skipped_dedup=0, facts_encoded=len(facts)) + pipeline.IngestionPipeline = Pipeline + stop = types.ModuleType("truememory.ingest.hooks.stop") + stop.GATE_THRESHOLD, stop.MIN_MESSAGES = 0.30, 5 + children = {extractor.__name__: extractor, pipeline.__name__: pipeline, + "truememory.ingest.transcript": TRANSCRIPT, stop.__name__: stop} + snapshot = {"messages": [{"role": "human", "content": "Synthetic long user fact " * 3}] + + [{"role": "assistant", "content": "Synthetic answer"}] * 4, + "user": self.store.user, "db": self.store.db, "session_id": self.store.session} + return children, snapshot, calls, pipeline, original, config + + def test_exact_single_outcome_default_call_and_terminal_accounting(self) -> None: + children, snapshot, calls, pipeline, original, config = self.pipeline_fixture() + with collaborators(children): + result = BRIDGE._run_pipeline(snapshot) + self.assertEqual(result["state"], "extraction_complete") + self.assertEqual(result["extraction_status"], "complete") + self.assertTrue(result["terminal_accounting_complete"]) + self.assertEqual(len(calls), 1) + self.assertIs(calls[0][0][1], config) + self.assertEqual(calls[0][1], {}) + self.assertIs(pipeline.extract_facts, original) + + def test_partial_limited_failed_and_storage_gap_never_complete(self) -> None: + for status, stored in (("partial", 1), ("limited", 1), ("failed", 1), ("complete", 0)): + children, snapshot, calls, pipeline, original, _config = self.pipeline_fixture(status, stored) + with collaborators(children): + result = BRIDGE._run_pipeline(snapshot) + self.assertEqual(result["state"], "worker_returned") + self.assertEqual(result["extraction_status"], status) + self.assertEqual(len(calls), 1) + self.assertIs(pipeline.extract_facts, original) + + def test_missing_outcome_api_retains_original_pipeline_and_unproven_coverage(self) -> None: + children, snapshot, calls, pipeline, original, _config = self.pipeline_fixture(missing_api=True) + with collaborators(children): + result = BRIDGE._run_pipeline(snapshot) + self.assertEqual(result["state"], "worker_returned") + self.assertEqual(result["extraction_status"], "coverage_unproven") + self.assertEqual(calls, []) + self.assertIs(pipeline.extract_facts, original) + + def test_heuristic_return_cannot_invent_complete_extraction_coverage(self) -> None: + children, snapshot, calls, pipeline, original, _config = self.pipeline_fixture(heuristic=True) + with collaborators(children): + result = BRIDGE._run_pipeline(snapshot) + self.assertEqual(result["state"], "worker_returned") + self.assertEqual(result["extraction_status"], "heuristic_returned") + self.assertTrue(result["terminal_accounting_complete"]) + self.assertEqual(calls, []) + self.assertIs(pipeline.extract_facts, original) + + def test_complete_empty_outcome_distinguished_from_failed_empty(self) -> None: + for status, expected in (("complete", "extraction_complete"), ("failed", "worker_returned")): + children, snapshot, calls, _pipeline, _original, _config = self.pipeline_fixture(status, 0, empty=True) + with collaborators(children): + result = BRIDGE._run_pipeline(snapshot) + self.assertEqual(result["state"], expected) + self.assertEqual(result["counts"]["facts_extracted"], 0) + self.assertEqual(len(calls), 1) + + def test_outcome_binding_restored_on_exception_and_original_threshold(self) -> None: + children, snapshot, calls, pipeline, original, _config = self.pipeline_fixture(throw=True) + with collaborators(children): + with self.assertRaises(RuntimeError): + BRIDGE._run_pipeline(snapshot) + self.assertEqual(len(calls), 1) + self.assertIs(pipeline.extract_facts, original) + snapshot["messages"] = snapshot["messages"][:4] + with collaborators(children): + self.assertEqual(BRIDGE._run_pipeline(snapshot)["state"], "deferred") + self.assertEqual(len(calls), 1) + + def test_worker_success_keeps_capture_and_distinct_context_extraction_states(self) -> None: + key = self.store.capture(payload(), "pre_llm_call") + self.store.flush() + state = self.store.state() + state["worker"] = {"token": "synthetic-token", "state": "reserved"} + self.store.save(state) + captures = {name: (self.store.path / name).read_bytes() for name in state["captures"]} + with collaborators({"truememory.ingest.hooks._shared": inert_shared()}), \ + patch.object(BRIDGE, "_owner", return_value={"pid": 99, "born": 12}), \ + patch.object(BRIDGE, "_recall", return_value="Synthetic bounded context"), \ + patch.object(BRIDGE, "_run_pipeline", return_value={"state": "extraction_complete"}): + BRIDGE._worker(self.store, "synthetic-token") + current = self.store.state() + self.assertEqual(current["turns"][key]["context_state"], "ready") + self.assertEqual(current["worker"]["state"], "returned") + self.assertTrue(all(item["state"] == "extraction_complete" for item in current["captures"].values())) + self.assertEqual(captures, {name: (self.store.path / name).read_bytes() for name in captures}) + self.assertTrue(all(item["attempts"] == 1 for item in current["captures"].values())) + + def test_worker_failure_retry_is_bounded_and_never_evicts_capture(self) -> None: + self.store.capture(payload(post=True), "post_llm_call") + before = next(iter(self.store.state()["captures"])) + raw = (self.store.path / before).read_bytes() + shared = inert_shared() + with collaborators({shared.__name__: shared}), \ + patch.object(BRIDGE, "_owner", return_value={"pid": 99, "born": 12}), \ + patch.object(BRIDGE, "_run_pipeline", side_effect=RuntimeError("synthetic worker failure")): + for attempt in range(BRIDGE.MAX_ATTEMPTS): + state = self.store.state() + state["worker"] = {"token": "synthetic-token", "state": "reserved"} + state["captures"][before]["retry_at"] = 0 + self.store.save(state) + BRIDGE._worker(self.store, "synthetic-token") + self.assertEqual(self.store.state()["captures"][before]["attempts"], attempt + 1) + state = self.store.state() + self.assertEqual(state["captures"][before]["state"], "failed") + self.assertGreater(state["captures"][before]["retry_at"], 0) + self.assertFalse(BRIDGE._due(state["captures"][before])) + self.assertEqual((self.store.path / before).read_bytes(), raw) + + def test_uncertain_attempt_can_retry_to_observed_complete(self) -> None: + self.store.capture(payload(post=True), "post_llm_call") + name = next(iter(self.store.state()["captures"])) + raw = (self.store.path / name).read_bytes() + results = [{"state": "worker_returned", "extraction_status": "partial"}, + {"state": "extraction_complete", "extraction_status": "complete"}] + shared = inert_shared() + with collaborators({shared.__name__: shared}), patch.object(BRIDGE, "_owner", return_value={}), \ + patch.object(BRIDGE, "_run_pipeline", side_effect=results): + for attempt in range(2): + state = self.store.state() + state["worker"] = {"token": "synthetic-token", "state": "reserved"} + state["captures"][name]["retry_at"] = 0 + self.store.save(state) + BRIDGE._worker(self.store, "synthetic-token") + self.assertEqual(self.store.state()["captures"][name]["attempts"], attempt + 1) + self.assertEqual(self.store.state()["captures"][name]["state"], "extraction_complete") + self.assertEqual((self.store.path / name).read_bytes(), raw) + + def test_intake_during_worker_is_retained_and_processed_with_separate_budget(self) -> None: + self.store.capture(payload(post=True), "post_llm_call") + state = self.store.state() + state["worker"] = {"token": "synthetic-token", "state": "reserved"} + self.store.save(state) + calls: list[str] = [] + budgets: list[bool] = [] + shared = inert_shared() + shared.check_extraction_budget = lambda: budgets.append(True) or True + def ingest(snapshot: dict[str, object]) -> dict[str, str]: + calls.append(snapshot["turn_id"]) + if len(calls) == 1: + self.store.capture(payload(turn="late-turn", post=True), "post_llm_call") + return {"state": "extraction_complete"} + with collaborators({shared.__name__: shared}), patch.object(BRIDGE, "_owner", return_value={}), \ + patch.object(BRIDGE, "_run_pipeline", side_effect=ingest): + BRIDGE._worker(self.store, "synthetic-token") + self.assertEqual(calls, ["synthetic-turn", "late-turn"]) + self.assertEqual(len(budgets), 1) + self.assertEqual(len(self.store.state()["captures"]), 2) + self.assertTrue(all(item["state"] == "extraction_complete" + for item in self.store.state()["captures"].values())) + + def test_worker_oversize_context_is_failed_not_written(self) -> None: + key = self.store.capture(payload(), "pre_llm_call") + state = self.store.state() + state["worker"] = {"token": "synthetic-token", "state": "reserved"} + self.store.save(state) + shared = inert_shared() + with collaborators({shared.__name__: shared}), patch.object(BRIDGE, "_owner", return_value={}), \ + patch.object(BRIDGE, "_recall", return_value="x" * (BRIDGE.MAX_CONTEXT_BYTES + 1)): + BRIDGE._worker(self.store, "synthetic-token") + self.assertEqual(self.store.state()["turns"][key]["context_state"], "failed") + self.assertEqual(self.store.state()["turns"][key]["context_attempts"], 1) + self.assertFalse((self.store.path / f"{key}.context.json").exists()) + + def test_context_timeout_preserves_pending_capture(self) -> None: + key = self.store.capture(payload(), "pre_llm_call") + output = io.StringIO() + with patch.object(BRIDGE, "CONTEXT_WAIT_SECONDS", 0): + self.assertFalse(BRIDGE._emit_context(self.store, key, output)) + self.assertEqual(output.getvalue(), "") + self.assertEqual(self.store.state()["turns"][key]["context_state"], "pending") + self.assertEqual(len(self.store.state()["captures"]), 1) + + def test_first_turn_session_context_later_targeted_recall_has_no_legacy_side_effects(self) -> None: + self.store.capture(payload(), "pre_llm_call") + snapshot = self.store.snapshot(next(iter(self.store.state()["captures"]))) + calls: list[tuple[str, object]] = [] + session = types.ModuleType("truememory.ingest.hooks.session_start") + session._intensity_budget = lambda _intensity: 8192 + session.recall_memories = lambda _input, **kwargs: calls.append(("first", kwargs)) or "first-context" + prompt = types.ModuleType("truememory.ingest.hooks.user_prompt_submit") + prompt._get_intensity_config = lambda: ("standard", "max") + prompt._try_proactive_recall = lambda *args, **kwargs: (None, False) + prompt._try_auto_recall = lambda *args, **kwargs: calls.append(("later", args)) or "later-context" + with collaborators({session.__name__: session, prompt.__name__: prompt}): + self.assertEqual(BRIDGE._recall(snapshot, 1), "first-context") + self.assertEqual(BRIDGE._recall(snapshot, 2), "later-context") + self.assertEqual(calls[0][1]["user_id"], self.store.user) + self.assertEqual(calls[0][1]["db_path"], self.store.db) + self.assertEqual(calls[0][1]["budget"], 8192) + self.assertEqual(calls[1][1][0], snapshot["native_user_message"]) + + def spawn_collaborators(self, allowed: bool = True) -> tuple[dict[str, types.ModuleType], list[str]]: + events: list[str] = [] + core = types.ModuleType("truememory.hooks.core") + @contextmanager + def gate() -> Iterator[bool]: + events.append("gate") + yield allowed + core.spawn_gate = gate + core.register_spawned_pid = lambda _pid: events.append("register") + shared = inert_shared() + shared.check_extraction_budget = lambda: events.append("budget") or True + shared.refund_extraction_budget = lambda: events.append("refund") + return {core.__name__: core, shared.__name__: shared}, events + + def test_spawn_cap_and_launch_failure_preserve_captured_retry_state(self) -> None: + self.store.capture(payload(), "pre_llm_call") + children, events = self.spawn_collaborators(allowed=False) + with collaborators(children), patch.object(BRIDGE, "_birth", return_value=12): + BRIDGE._spawn(self.store) + self.assertEqual(events, ["budget", "gate", "refund"]) + self.assertEqual(self.store.state()["worker"], {}) + children, events = self.spawn_collaborators() + with collaborators(children), patch.object(BRIDGE, "_birth", return_value=12), \ + patch.object(BRIDGE.subprocess, "Popen", side_effect=OSError("synthetic spawn failure")): + with self.assertRaises(OSError): + BRIDGE._spawn(self.store) + self.assertEqual(events, ["budget", "gate", "refund"]) + self.assertEqual(self.store.state()["worker"]["state"], "spawn_failed") + self.assertTrue(BRIDGE._work_available(self.store.state())) + + def test_spawn_live_lock_and_live_birth_deny_without_burning_budget(self) -> None: + self.store.capture(payload(), "pre_llm_call") + children, events = self.spawn_collaborators() + with collaborators(children), BRIDGE._lock(self.store.worker_lock) as held: + self.assertTrue(held) + BRIDGE._spawn(self.store) + self.assertEqual(events, []) + state = self.store.state() + state["worker"] = {"state": "reserved", "child": {"pid": 99, "born": 12}, "launcher": None} + self.store.save(state) + with collaborators(children), patch.object(BRIDGE, "_birth", return_value=12): + BRIDGE._spawn(self.store) + self.assertEqual(events, []) + self.assertEqual(self.store.state()["worker"]["state"], "reserved") + + def test_spawn_reused_pid_uses_fixed_worker_arguments_and_account(self) -> None: + self.store.capture(payload(), "pre_llm_call") + state = self.store.state() + state["worker"] = {"state": "reserved", "child": {"pid": 99, "born": 11}, "launcher": None} + self.store.save(state) + children, events = self.spawn_collaborators() + with collaborators(children), patch.object(BRIDGE, "_birth", return_value=12), \ + patch.object(BRIDGE.subprocess, "Popen", return_value=types.SimpleNamespace(pid=123)) as child: + BRIDGE._spawn(self.store) + argv = child.call_args.args[0] + self.assertEqual(argv[2:4], ["--worker", self.store.key]) + self.assertIn(self.store.user, argv) + self.assertIn(self.store.db, argv) + self.assertEqual(events, ["budget", "gate", "register"]) + self.assertEqual(self.store.state()["worker"]["child"], {"pid": 123, "born": 12}) + self.assertEqual(len(self.store.state()["captures"]), 1) + + def test_main_argv_ignores_stdin_event_account_paths_and_finalize_uses_owned_capture(self) -> None: + event = payload() + with patch.dict(os.environ, {"TRUEMEMORY_HERMES_SPOOL_DIR": str(self.store.root)}), \ + patch.object(sys, "argv", ["hermes.py", "--event", "pre_llm_call", "--user", self.store.user, + "--db", self.store.db]), \ + patch.object(sys, "stdin", io.StringIO(json.dumps(event))), \ + patch.object(BRIDGE, "_spawn") as spawn, patch.object(BRIDGE, "_emit_context") as emit: + self.assertEqual(BRIDGE.main(), 0) + self.assertEqual(spawn.call_args.args[0].account, self.store.account) + self.assertEqual(emit.call_args.args[0].session, self.store.session) + self.assertEqual(len(self.store.state()["captures"]), 1) + with patch.dict(os.environ, {"TRUEMEMORY_HERMES_SPOOL_DIR": str(self.store.root)}), \ + patch.object(sys, "argv", ["hermes.py", "--event", "on_session_finalize", "--user", self.store.user, + "--db", self.store.db]), \ + patch.object(sys, "stdin", io.StringIO(json.dumps({"session_id": self.store.session, + "transcript_path": "/never/read"}))), \ + patch.object(BRIDGE, "_spawn"), patch.object(BRIDGE, "_emit_context") as emit: + self.assertEqual(BRIDGE.main(), 0) + self.assertFalse(emit.called) + self.assertTrue(all(item["flush"] for item in self.store.state()["captures"].values())) + + def test_crash_claim_durable_attempt_and_live_lock_no_reclaim(self) -> None: + self.store.capture(payload(post=True), "post_llm_call") + state = self.store.state() + state["worker"] = {"token": "synthetic-token", "state": "reserved"} + self.store.save(state) + shared = inert_shared() + with collaborators({shared.__name__: shared}), patch.object(BRIDGE, "_owner", return_value={}): + with BRIDGE._lock(self.store.worker_lock) as held: + self.assertTrue(held) + BRIDGE._worker(self.store, "synthetic-token") + self.assertEqual(self.store.state()["worker"]["state"], "reserved") + with patch.object(BRIDGE, "_run_pipeline", side_effect=KeyboardInterrupt): + with self.assertRaises(KeyboardInterrupt): + BRIDGE._worker(self.store, "synthetic-token") + item = next(iter(self.store.state()["captures"].values())) + self.assertEqual(item["state"], "processing") + self.assertEqual(item["attempts"], 1) + with BRIDGE._lock(self.store.worker_lock) as released: + self.assertTrue(released) + + def test_owned_command_migration_preserves_foreign_entries(self) -> None: + source = ROOT / "truememory/hooks/adapters/hermes.py" + tree = ast.parse(source.read_text()) + tree.body = [node for node in tree.body if not isinstance(node, ast.ImportFrom) + or not (node.module or "").startswith("truememory")] + namespace = {"__file__": str(source), "__name__": "synthetic_adapter765", "CLIAdapter": object, + "atomic_write_text": lambda path, text: path.write_text(text)} + exec(compile(tree, str(source), "exec"), namespace) + config = self.case / "config.json" + namespace.update(_CONFIG=config, _yaml_safe_load=json.loads, _yaml_safe_dump=json.dumps) + adapter = namespace["HermesAdapter"]() + hooks = ROOT / "truememory/ingest/hooks" + legacy = adapter._build_command(sys.executable, hooks / "user_prompt_submit.py", "old-user", "old-db") + foreign = [{"command": "echo truememory"}, {"command": "helper --label truememory"}, + {"command": legacy + " # foreign comment"}, {"command": legacy + " ; foreign"}, + {"command": adapter._build_command(sys.executable, hooks / "stop.py")}] + config.write_text(json.dumps({"hooks": {"pre_llm_call": [*foreign, {"command": legacy}]}})) + adapter.install_hooks(sys.executable, "new-user", "new-db") + installed = json.loads(config.read_text()) + entries = installed["hooks"]["pre_llm_call"] + self.assertEqual(entries[:-1], foreign) + self.assertIn("hermes.py", entries[-1]["command"]) + self.assertIn("--event pre_llm_call", entries[-1]["command"]) + self.assertIn("new-user", entries[-1]["command"]) + self.assertIn("new-db", entries[-1]["command"]) + first = config.read_bytes() + adapter.install_hooks(sys.executable, "new-user", "new-db") + self.assertEqual(config.read_bytes(), first) + self.assertFalse(adapter.has_session_start) + adapter._remove_hook_entries() + self.assertEqual(json.loads(config.read_text())["hooks"]["pre_llm_call"], foreign) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-inference-ownership-738.py b/tests/test-inference-ownership-738.py new file mode 100644 index 00000000..66fe7733 --- /dev/null +++ b/tests/test-inference-ownership-738.py @@ -0,0 +1,331 @@ +"""Synthetic, event-driven ownership regressions; no models or server sockets.""" +from __future__ import annotations + +import sys +import threading +import time +import types +import unittest +from unittest.mock import patch + +from truememory import model_server as ms + + +class ObservedLock: + def __init__(self, attempted: threading.Event) -> None: + self.lock = threading.Lock() + self.attempted = attempted + + def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: + if blocking and threading.current_thread().name.startswith("competitor"): + self.attempted.set() + return self.lock.acquire(blocking, timeout) + + def release(self) -> None: + self.lock.release() + + def locked(self) -> bool: + return self.lock.locked() + + +class TrackedModel: + def __init__(self, attempted: threading.Event) -> None: + self.attempted = attempted + self.started = threading.Event() + self.release = threading.Event() + self.guard = threading.Lock() + self.calls: list[list] = [] + self.active = 0 + self.peak = 0 + self.errors: dict[int, Exception] = {} + self.block_on: int | None = None + self.moves: list[str] = [] + self.unsafe_moves = 0 + self.move_error: Exception | None = None + + def _call(self, items: list) -> None: + with self.guard: + self.calls.append(list(items)) + call = len(self.calls) + self.active += 1 + self.peak = max(self.peak, self.active) + if threading.current_thread().name.startswith("competitor"): + self.attempted.set() + try: + if call in self.errors: + raise self.errors[call] + if call == self.block_on: + self.started.set() + if not self.release.wait(timeout=5): + raise AssertionError("Synthetic inference was not released") + finally: + with self.guard: + self.active -= 1 + + def encode(self, texts: list[str], **_kwargs: object) -> list[list[float]]: + self._call(texts) + return [[float(len(text)), 0.5] for text in texts] + + def predict(self, pairs: list[tuple[str, str]], **_kwargs: object) -> list[float]: + self._call(pairs) + return [float(len(document)) for _, document in pairs] + + def to(self, device: str) -> None: + with self.guard: + self.unsafe_moves += int(self.active > 0) + self.moves.append(device) + if self.move_error is not None: + raise self.move_error + + +class TestIssue738InferenceOwnership(unittest.TestCase): + def setUp(self) -> None: + self.threads: list[threading.Thread] = [] + self.models: list[TrackedModel] = [] + self.reset_runtime() + mps = types.ModuleType("truememory.mps_utils") + mps.is_mps_oom = lambda error: "MPS backend out of memory" in str(error) + mps.flush_mps_cache = lambda: None + mps.auto_detect_device = lambda: "mps" + mps.resolve_device = lambda device: device + mps.ensure_mps_memory_budget = lambda device: None + reranker = types.ModuleType("truememory.reranker") + reranker.get_current_reranker_name = lambda: "primary" + transformers = types.ModuleType("sentence_transformers") + + def cross_encoder(name: str, device: str | None) -> TrackedModel: + self.loads.append(("rerank", name, device)) + return self.rerank_cpu if name == "primary" else self.alternate + + transformers.CrossEncoder = cross_encoder + patcher = patch.dict(sys.modules, { + "truememory.mps_utils": mps, "truememory.reranker": reranker, + "sentence_transformers": transformers, + }) + patcher.start() + self.addCleanup(patcher.stop) + self.addCleanup(self.finish_threads) + + def reset_runtime(self) -> None: + self.attempted = threading.Event() + self.server = ms.ModelServer() + self.server._inference_lock = ObservedLock(self.attempted) + self.server._write_status_file = lambda: None + self.loads: list[tuple[str, str, str | None]] = [] + self.embed = TrackedModel(self.attempted) + self.rerank_mps = TrackedModel(self.attempted) + self.rerank_cpu = TrackedModel(self.attempted) + self.fast = TrackedModel(self.attempted) + self.alternate = TrackedModel(self.attempted) + self.models.extend((self.embed, self.rerank_mps, self.rerank_cpu, + self.fast, self.alternate)) + self.server._embed_state = ms._EmbedState(self.embed, "primary", "qwen3_256") + self.server._reranker = self.rerank_mps + self.server._reranker_name = "primary" + self.server._resolve_embed_model_id = lambda tier: ( + "qwen3_256" if tier == "primary" else "synthetic-alternate" + ) + + def build(model_id: str, device: str | None) -> TrackedModel: + self.loads.append(("embed", model_id, device)) + return self.fast if model_id == "qwen3_256" else self.alternate + + self.server._build_embed_model = build + + def finish_threads(self) -> None: + for model in self.models: + model.release.set() + for thread in self.threads: + thread.join(timeout=3) + self.assertFalse(thread.is_alive(), "Synthetic worker did not finish") + + def request(self, op: str, count: int = 4, identity: str = "primary") -> dict: + texts = [f"fixture-{index}" for index in range(count)] + return {"op": op, "texts": texts, "pairs": [("fixture-query", text) for text in texts], + "tier": identity, "model_name": identity, "batch_size": 2} + + def start(self, request: dict, name: str) -> tuple[threading.Thread, dict]: + outcome: dict = {} + + def run() -> None: + try: + outcome["response"] = self.server.handle_request(request) + except Exception as error: + outcome["error"] = error + + thread = threading.Thread(target=run, name=name, daemon=True) + self.threads.append(thread) + thread.start() + return thread, outcome + + def assert_finished(self, worker: tuple[threading.Thread, dict]) -> dict: + thread, outcome = worker + thread.join(timeout=2) + self.assertFalse(thread.is_alive(), "Request did not finish") + self.assertNotIn("error", outcome) + return outcome["response"] + + def block_retry(self, op: str) -> tuple[TrackedModel, tuple[threading.Thread, dict]]: + failing = self.embed if op == "embed" else self.rerank_mps + failing.errors[1] = RuntimeError("MPS backend out of memory") + retrying = self.embed if op == "embed" else self.rerank_cpu + retrying.block_on = 2 if op == "embed" else 1 + owner = self.start(self.request(op), "owner") + self.assertTrue(retrying.started.wait(timeout=2), "CPU retry did not start") + return retrying, owner + + def assert_retry_owned_with_responsive_fast_lane(self, op: str) -> None: + retrying, owner = self.block_retry(op) + competitor = self.start(self.request(op, count=2), "competitor-main") + self.assertTrue(self.attempted.wait(timeout=2)) + self.assertEqual(retrying.peak, 1, "CPU retry overlapped same-instance inference") + acquired = self.server._lock.acquire(blocking=False) + self.assertTrue(acquired, "CPU retry retained the state lock") + if acquired: + self.server._lock.release() + query = self.start(self.request("embed", count=1), "query") + self.assertTrue(self.assert_finished(query)["ok"]) + self.assertEqual(len(self.fast.calls), 1, "Query bypassed the busy inference owner") + self.assertTrue(owner[0].is_alive()) + self.assertTrue(competitor[0].is_alive()) + retrying.release.set() + self.assertTrue(self.assert_finished(owner)["ok"]) + self.assertTrue(self.assert_finished(competitor)["ok"]) + self.assertEqual(retrying.peak, 1) + self.assertEqual(retrying.unsafe_moves, 0) + self.assertEqual(self.server._inflight, 0) + + def test_issue_738_embed_retry_owns_model_while_fast_query_completes(self) -> None: + self.assert_retry_owned_with_responsive_fast_lane("embed") + + def test_issue_738_rerank_retry_owns_reloaded_model_while_fast_query_completes(self) -> None: + self.assert_retry_owned_with_responsive_fast_lane("rerank") + + def assert_replacement_waits_for_owner(self, op: str) -> None: + retrying, owner = self.block_retry(op) + previous_loads = list(self.loads) + competitor = self.start(self.request(op, count=2, identity="alternate"), "competitor-tier") + self.assertTrue(self.attempted.wait(timeout=2)) + self.assertEqual(self.loads, previous_loads, "Replacement loaded during owned inference") + self.assertEqual(self.alternate.calls, []) + retrying.release.set() + self.assertTrue(self.assert_finished(owner)["ok"]) + self.assertTrue(self.assert_finished(competitor)["ok"]) + self.assertEqual(len(self.loads), len(previous_loads) + 1) + self.assertEqual(len(self.alternate.calls), 1) + + def test_issue_738_embedding_tier_replacement_waits_for_cpu_retry(self) -> None: + self.assert_replacement_waits_for_owner("embed") + + def test_issue_738_reranker_replacement_waits_for_cpu_retry(self) -> None: + self.assert_replacement_waits_for_owner("rerank") + + def test_issue_738_main_slot_also_serializes_different_model_kinds(self) -> None: + retrying, owner = self.block_retry("embed") + competitor = self.start(self.request("rerank", count=2), "competitor-rerank") + self.assertTrue(self.attempted.wait(timeout=2)) + self.assertEqual(self.rerank_mps.calls, []) + retrying.release.set() + self.assertTrue(self.assert_finished(owner)["ok"]) + self.assertTrue(self.assert_finished(competitor)["ok"]) + + def test_issue_738_fast_encoder_has_one_owner_and_one_cached_instance(self) -> None: + self.fast.block_on = 1 + self.server._fast_lock = ObservedLock(self.attempted) + self.server._inference_lock.acquire() + try: + first = self.start(self.request("embed", count=1), "first-fast") + self.assertTrue(self.fast.started.wait(timeout=2)) + second = self.start(self.request("embed", count=1), "competitor-fast") + self.assertTrue(self.attempted.wait(timeout=2)) + self.assertEqual(self.fast.peak, 1) + self.assertEqual(self.loads, [("embed", "qwen3_256", "cpu")]) + self.fast.release.set() + self.assertTrue(self.assert_finished(first)["ok"]) + self.assertTrue(self.assert_finished(second)["ok"]) + self.assertEqual(self.fast.peak, 1) + self.assertEqual(len(self.fast.calls), 2) + self.assertEqual(len(self.loads), 1) + finally: + self.server._inference_lock.release() + + def test_issue_738_deadline_waiter_cannot_release_owner_or_start_model_work(self) -> None: + for op in ("embed", "rerank"): + with self.subTest(op=op): + self.reset_runtime() + retrying, owner = self.block_retry(op) + calls = len(retrying.calls) + loads = list(self.loads) + request = self.request(op, count=2) + request["deadline"] = time.time() + 0.05 + waiter = self.start(request, "competitor-deadline") + response = self.assert_finished(waiter) + self.assertFalse(response["ok"]) + self.assertIn("deadline", response["error"]) + self.assertTrue(self.server._inference_lock.locked()) + self.assertEqual(len(retrying.calls), calls) + self.assertEqual(self.loads, loads) + self.assertTrue(owner[0].is_alive()) + retrying.release.set() + self.assertTrue(self.assert_finished(owner)["ok"]) + self.assertEqual(self.server._inflight, 0) + + def test_issue_738_exceptions_release_inference_and_state_locks(self) -> None: + for stage in ("embed-load", "embed-call", "embed-move", "embed-retry", + "single-call", "rerank-call", "rerank-retry", "rerank-load"): + with self.subTest(stage=stage): + self.reset_runtime() + error = ValueError("synthetic failure") + + def fail(*_args: object, **_kwargs: object) -> None: + raise error + + op = "rerank" if stage.startswith("rerank") else "embed" + if stage == "embed-load": + self.server._get_embed_model = fail + elif stage in ("embed-call", "single-call"): + self.embed.errors[1] = error + elif stage.startswith("embed"): + self.embed.errors[1] = RuntimeError("MPS backend out of memory") + if stage == "embed-move": + self.embed.move_error = error + else: + self.embed.errors[2] = error + elif stage == "rerank-call": + self.rerank_mps.errors[1] = error + else: + self.rerank_mps.errors[1] = RuntimeError("MPS backend out of memory") + if stage == "rerank-load": + sys.modules["sentence_transformers"].CrossEncoder = fail + else: + self.rerank_cpu.errors[1] = error + with self.assertRaisesRegex(ValueError, "synthetic failure"): + self.server.handle_request(self.request(op, count=1 if stage == "single-call" else 2)) + for lock in (self.server._inference_lock, self.server._lock, self.server._fast_lock): + self.assertTrue(lock.acquire(blocking=False)) + lock.release() + self.assertEqual(self.server._inflight, 0) + + def test_issue_738_fast_failure_releases_fast_owner_before_waiting_for_main(self) -> None: + self.fast.errors[1] = ValueError("synthetic fast failure") + self.server._inference_lock.acquire() + try: + waiter = self.start(self.request("embed", count=1), "competitor-fallback") + self.assertTrue(self.attempted.wait(timeout=2)) + self.assertTrue(self.server._fast_lock.acquire(blocking=False)) + self.server._fast_lock.release() + self.assertTrue(waiter[0].is_alive()) + finally: + self.server._inference_lock.release() + self.assertTrue(self.assert_finished(waiter)["ok"]) + self.assertEqual(len(self.embed.calls), 1) + + def test_issue_738_busy_state_lock_does_not_pin_main_permit_during_fast_work(self) -> None: + self.server._lock.acquire() + try: + query = self.start(self.request("embed", count=1), "query") + self.assertTrue(self.assert_finished(query)["ok"]) + self.assertEqual(len(self.fast.calls), 1) + self.assertFalse(self.server._inference_lock.locked()) + finally: + self.server._lock.release() diff --git a/tests/test-ingestion-completion-768.py b/tests/test-ingestion-completion-768.py new file mode 100644 index 00000000..6c5fd50e --- /dev/null +++ b/tests/test-ingestion-completion-768.py @@ -0,0 +1,572 @@ +"""Synthetic observed-byte completion controls; filesystem cases run on GPUBox.""" +from __future__ import annotations + +import ast +import contextlib +import datetime +import enum +import hashlib +import importlib.util +import json +import logging +import os +import sqlite3 +import stat +import sys +import tempfile +import time +import types +import unittest +from dataclasses import dataclass, field +from pathlib import Path +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location("ingestion_completion_768_controls", ROOT / "truememory/ingest/completion.py") +assert SPEC is not None and SPEC.loader is not None +COMPLETION = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = COMPLETION +SPEC.loader.exec_module(COMPLETION) + + +def result(action: str | None = None, *, parser: str = "complete", extractor: str = "complete") -> types.SimpleNamespace: + value = types.SimpleNamespace(facts_extracted=int(action is not None), facts_stored=int(action == "stored"), + facts_updated=int(action == "updated"), facts_skipped_gate=int(action == "skipped_gate"), + facts_skipped_dedup=int(action == "skipped_dedup"), facts_provenance_updated=0, + trace=[] if action is None else [{"action": action}], + coverage={"parser_status": parser, "extractor_status": extractor, "extractor_calls": 1, + "stable": True, "provenance_observed": True, + "parser_errors": [], "extractor_errors": [], + "malformed_records": 0, "unrecognized_records": 0, + "source_version": {"sha256": "0" * 64, "byte_count": 7}}) + return value + + +class InertCompletionControls(unittest.TestCase): + def test_rich_empty_and_exact_terminal_partitions(self) -> None: + for action in (None, "stored", "updated", "skipped_gate", "skipped_dedup"): + self.assertTrue(COMPLETION.strict_complete(result(action))) + metadata_only = result("skipped_dedup") + metadata_only.facts_provenance_updated = 1 + self.assertTrue(COMPLETION.strict_complete(metadata_only)) + + def test_partial_caps_heuristic_and_absent_outcomes_are_not_terminal(self) -> None: + for status in ("partial", "salvaged", "malformed", "unreadable", "unknown"): + self.assertFalse(COMPLETION.strict_complete(result(parser=status))) + for status in ("partial", "limited", "failed", "heuristic", "unobserved", "unknown"): + self.assertFalse(COMPLETION.strict_complete(result(extractor=status))) + self.assertFalse(COMPLETION.strict_complete(types.SimpleNamespace())) + + def test_unknown_storage_and_inconsistent_accounting_do_not_complete(self) -> None: + for action in ("storage_failed", "unknown", "stored"): + value = result(action) + value.trace[0]["storage_error"] = {"exception": "OperationalError"} + self.assertFalse(COMPLETION.strict_complete(value)) + value = result("stored") + value.facts_extracted = 2 + self.assertFalse(COMPLETION.strict_complete(value)) + value = result("stored") + value.trace = [{"action": "updated"}] + self.assertFalse(COMPLETION.strict_complete(value)) + value = result("stored") + value.trace.append({"action": "stored"}) + self.assertFalse(COMPLETION.strict_complete(value)) + + def test_bool_negative_and_missing_provenance_proof_are_rejected(self) -> None: + for field_name in ("facts_extracted", "facts_stored", "facts_updated", "facts_skipped_gate", "facts_skipped_dedup"): + value = result() + setattr(value, field_name, True) + self.assertFalse(COMPLETION.strict_complete(value)) + setattr(value, field_name, -1) + self.assertFalse(COMPLETION.strict_complete(value)) + for field_name, replacement in (("extractor_calls", True), ("extractor_calls", 0), + ("stable", False), ("provenance_observed", False)): + value = result() + value.coverage[field_name] = replacement + self.assertFalse(COMPLETION.strict_complete(value)) + + def test_complete_protocol_requires_typed_record_counts_and_errors(self) -> None: + for name in ("malformed_records", "unrecognized_records"): + for replacement in (False, None, "0", 1): + value = result() + value.coverage[name] = replacement + self.assertFalse(COMPLETION.strict_complete(value)) + value = result() + del value.coverage[name] + self.assertFalse(COMPLETION.strict_complete(value)) + for name in ("parser_errors", "extractor_errors"): + for replacement in (None, (), "", ["synthetic_partial"]): + value = result() + value.coverage[name] = replacement + self.assertFalse(COMPLETION.strict_complete(value)) + value = result() + del value.coverage[name] + self.assertFalse(COMPLETION.strict_complete(value)) + + def test_receipt_revalidates_entire_rich_completion_proof(self) -> None: + capture = COMPLETION.Capture(Path("/synthetic"), "1" * 64, + Path("/synthetic/capture.jsonl"), {"sha256": "0" * 64, "byte_count": 7}) + def receipt_value(): + value = result("stored") + return {"version": 1, "capture_key": capture.key, "scope": capture.metadata, + "coverage": "observed_bytes", "parser_status": "complete", "extractor_status": "complete", + "proof": {"coverage": value.coverage, "counts": {name: getattr(value, name) for name in COMPLETION._COUNTS}, + "actions": ["stored"]}} + def observed(value): + with patch.object(Path, "exists", return_value=True), \ + patch.object(COMPLETION, "_read_private", return_value=json.dumps(value).encode()): + return COMPLETION.has_receipt(capture) + self.assertTrue(observed(receipt_value())) + for name in ("parser_status", "extractor_status", "proof"): + value = receipt_value() + del value[name] + self.assertFalse(observed(value)) + for name, replacement in (("parser_status", "partial"), ("extractor_status", "failed"), + ("version", True), ("scope", {})): + value = receipt_value() + value[name] = replacement + self.assertFalse(observed(value)) + for name, replacement in (("parser_status", "partial"), ("extractor_status", "failed"), + ("provenance_observed", False), ("parser_errors", ["synthetic"]), + ("malformed_records", True)): + value = receipt_value() + value["proof"]["coverage"][name] = replacement + self.assertFalse(observed(value)) + value = receipt_value() + value["proof"]["counts"]["facts_stored"] = 0 + self.assertFalse(observed(value)) + value = receipt_value() + value["proof"]["actions"] = ["storage_failed"] + self.assertFalse(observed(value)) + + def test_capture_inventory_stops_at_first_excess_entry(self) -> None: + visited = [] + def entries(): + for index in range(5005): + visited.append(index) + yield types.SimpleNamespace(path="/synthetic/capture-" + str(index)) + with patch.object(COMPLETION.os, "scandir", return_value=contextlib.nullcontext(entries())), \ + patch.object(COMPLETION, "_open_private", return_value=41) as opened, \ + patch.object(COMPLETION.os, "fstat", return_value=types.SimpleNamespace(st_size=1)), \ + patch.object(COMPLETION.os, "close"): + with self.assertRaisesRegex(COMPLETION.CompletionError, "capture_count_limit"): + COMPLETION._capture_inventory(Path("/synthetic/captures")) + self.assertEqual(len(visited), COMPLETION.MAX_CAPTURES + 1) + self.assertEqual(opened.call_count, COMPLETION.MAX_CAPTURES) + + def test_source_ownership_requires_regular_current_account_or_fails_closed(self) -> None: + info = types.SimpleNamespace(st_mode=stat.S_IFREG | 0o600, st_uid=1001) + with patch.object(COMPLETION, "os", types.SimpleNamespace(name="posix", getuid=lambda: 1001)): + COMPLETION._source_owned(info, 41) + info.st_uid = 1002 + with self.assertRaisesRegex(COMPLETION.CompletionError, "source_not_owned"): + COMPLETION._source_owned(info, 41) + info.st_mode = stat.S_IFDIR | 0o700 + with self.assertRaisesRegex(COMPLETION.CompletionError, "source_not_regular"): + COMPLETION._source_owned(info, 41) + info.st_mode = stat.S_IFREG | 0o600 + with patch.object(COMPLETION, "os", types.SimpleNamespace(name="unverifiable")): + with self.assertRaisesRegex(COMPLETION.CompletionError, "source_ownership_unavailable"): + COMPLETION._source_owned(info, 41) + + def test_legacy_pointer_inventory_never_inspects_over_limit_or_retires(self) -> None: + shared = types.ModuleType("truememory.ingest.hooks._shared") + shared.backlog_transcript_source = lambda *args, **kwargs: None + platform = types.ModuleType("truememory._platform") + platform.pid_is_alive = lambda pid: True + visited = [] + def entries(): + for index in range(5005): + visited.append(index) + yield types.SimpleNamespace(path="/synthetic/pointer-" + str(index) + ".txt") + with patch.dict(sys.modules, {shared.__name__: shared, platform.__name__: platform}), \ + patch.object(Path, "exists", return_value=True), patch.object(Path, "is_symlink", return_value=False), \ + patch.object(Path, "unlink") as removed, \ + patch.object(COMPLETION.os, "scandir", return_value=contextlib.nullcontext(entries())), \ + patch.object(COMPLETION, "_open_private") as opened: + COMPLETION.convert_legacy_pointers("synthetic-account", "/synthetic/db", "claude") + self.assertEqual(len(visited), COMPLETION.MAX_CAPTURES) + opened.assert_not_called() + removed.assert_not_called() + + def test_legacy_conversion_requires_exact_scope_retains_pointer_and_avoids_live_workers(self) -> None: + shared = types.ModuleType("truememory.ingest.hooks._shared") + shared.backlog_transcript_source = lambda value, host: (value["transcript_path"], value["session_id"]) + platform = types.ModuleType("truememory._platform") + platform.pid_is_alive = lambda pid: pid == 42 + baseline = {"transcript_path": "/synthetic/source.jsonl", "session_id": "synthetic/a", + "user_id": "synthetic-account", "db_path": "/synthetic/db", "transcript_host": "claude"} + scenarios = [(baseline, ".json", True), ({**baseline, "user_id": "other-account"}, ".json", False), + ({**baseline, "db_path": "/synthetic/other-db"}, ".json", False), + ({**baseline, "transcript_host": "codex"}, ".json", False), + ({**baseline, "claimed_pid": 42}, ".processing", False), + (baseline, ".processing", False), ({**baseline, "claimed_pid": 43}, ".processing", True)] + for missing in ("user_id", "db_path", "transcript_host"): + value = dict(baseline) + del value[missing] + scenarios.append((value, ".json", False)) + for value, suffix, admitted in scenarios: + raw = json.dumps(value).encode() + info = types.SimpleNamespace(st_dev=1, st_ino=1, st_size=len(raw), st_mtime_ns=1, st_ctime_ns=1) + handle = types.SimpleNamespace(read=lambda count: raw[:count], fileno=lambda: 41) + entry = types.SimpleNamespace(path="/synthetic/pointer" + suffix) + with patch.dict(sys.modules, {shared.__name__: shared, platform.__name__: platform}), \ + patch.object(Path, "exists", return_value=True), patch.object(Path, "is_symlink", return_value=False), \ + patch.object(Path, "unlink") as removed, \ + patch.object(COMPLETION.os, "scandir", return_value=contextlib.nullcontext(iter([entry]))), \ + patch.object(COMPLETION, "_open_private", return_value=41), \ + patch.object(COMPLETION.os, "fdopen", side_effect=lambda *args: contextlib.nullcontext(handle)), \ + patch.object(COMPLETION.os, "fstat", return_value=info), \ + patch.object(COMPLETION, "_source_owned"), \ + patch.object(COMPLETION, "admit_capture", return_value=object()) as capture, \ + patch.object(COMPLETION, "dispatch", return_value=0) as dispatch: + COMPLETION.convert_legacy_pointers("synthetic-account", "/synthetic/db", "claude") + self.assertEqual(capture.call_count, int(admitted)) + self.assertEqual(dispatch.call_count, int(admitted)) + removed.assert_not_called() + if admitted: + self.assertEqual(capture.call_args.args, + ("/synthetic/source.jsonl", "synthetic/a", "synthetic-account", "/synthetic/db", "claude")) + + def test_automatic_maintenance_failure_never_falls_back_to_legacy_cleanup(self) -> None: + source = ROOT / "truememory/ingest/hooks/session_start.py" + node = next(node for node in ast.parse(source.read_text()).body + if isinstance(node, ast.FunctionDef) and node.name == "_drain_backlog") + namespace = {"os": os, "log": logging.getLogger(__name__)} + exec(compile(ast.fix_missing_locations(ast.Module(body=[node], type_ignores=[])), str(source), "exec"), namespace) + module = types.ModuleType("truememory.ingest.completion") + def retained_failure(*args): + raise COMPLETION.CompletionError("synthetic retained failure") + module.drain = retained_failure + calls = [] + module.convert_legacy_pointers = lambda *args: calls.append(args) + with patch.dict(sys.modules, {module.__name__: module}), \ + patch.dict(os.environ, {"TRUEMEMORY_MAINTENANCE_CHILD": "1"}, clear=True): + namespace["_drain_backlog"]("claude") + self.assertEqual(calls, []) + + def test_windows_owner_queries_require_insufficient_buffer_and_release_resources(self) -> None: + import ctypes + from unittest.mock import Mock + for query_error, size, expected in ((122, 16, True), (5, 16, False), (122, 65537, False)): + kernel = types.SimpleNamespace(GetCurrentProcess=Mock(return_value=11), + CloseHandle=Mock(return_value=True), LocalFree=Mock(return_value=None)) + def get_security(handle, kind, fields, owner, group, dacl, sacl, descriptor): + owner._obj.value = 101 + descriptor._obj.value = 202 + return 0 + def open_token(process, access, token): + token._obj.value = 23 + return True + def token_information(token, kind, buffer, capacity, needed): + needed._obj.value = size + return bool(buffer) + security = types.SimpleNamespace(GetSecurityInfo=Mock(side_effect=get_security), + OpenProcessToken=Mock(side_effect=open_token), GetTokenInformation=Mock(side_effect=token_information), + EqualSid=Mock(return_value=True), IsWellKnownSid=Mock(), GetAclInformation=Mock(), GetAce=Mock()) + with patch.object(ctypes, "WinDLL", side_effect=lambda name, **kwargs: security if name == "advapi32" else kernel, create=True), \ + patch.object(ctypes, "set_last_error", create=True), \ + patch.object(ctypes, "get_last_error", return_value=query_error, create=True): + self.assertEqual(COMPLETION._windows_security(41), expected) + self.assertEqual(kernel.CloseHandle.call_args.args[0].value, 23) + self.assertEqual(kernel.LocalFree.call_args.args[0].value, 202) + self.assertEqual(security.GetTokenInformation.call_count, 2 if expected else 1) + info = types.SimpleNamespace(st_mode=stat.S_IFREG | 0o600, st_uid=0) + platform = types.ModuleType("msvcrt") + platform.get_osfhandle = lambda fd: 41 + with patch.object(COMPLETION, "os", types.SimpleNamespace(name="nt", getuid=lambda: 0)), \ + patch.dict(sys.modules, {"msvcrt": platform}), patch.object(COMPLETION, "_windows_security", return_value=False): + with self.assertRaisesRegex(COMPLETION.CompletionError, "source_ownership_unavailable"): + COMPLETION._source_owned(info, 41) + + def test_pending_live_launcher_is_advisory_block_not_permission_to_take_attempt(self) -> None: + capture = COMPLETION.Capture(Path("/synthetic"), "1" * 64, Path("/synthetic/capture.jsonl"), {}) + entry = {"attempts": 1, "next_attempt_at": 0, "status": "pending", "worker_pid": 42} + platform = types.ModuleType("truememory._platform") + platform.pid_is_alive = lambda pid: pid == 42 + with patch.dict(sys.modules, {platform.__name__: platform}), \ + patch.object(COMPLETION, "_lock", side_effect=lambda *args, **kwargs: contextlib.nullcontext(True)), \ + patch.object(COMPLETION, "has_receipt", return_value=False), \ + patch.object(COMPLETION, "_state", return_value={"captures": {capture.key: entry}}), \ + patch.object(COMPLETION, "_save_state") as saved: + self.assertEqual(COMPLETION.reserve_inline_attempt(capture), 0) + self.assertEqual(entry["attempts"], 1) + saved.assert_not_called() + + def test_retained_state_complete_flag_is_not_a_receipt_authority(self) -> None: + entry = {"metadata": {"host": "claude"}, "status": "complete"} + capture = object() + with patch.object(Path, "exists", return_value=True), \ + patch.object(COMPLETION, "_account_directory"), \ + patch.object(COMPLETION, "_lock", side_effect=lambda *args, **kwargs: contextlib.nullcontext(True)), \ + patch.object(COMPLETION, "_state", return_value={"captures": {"synthetic-key": entry}}), \ + patch.object(COMPLETION, "_capture_from", return_value=capture), \ + patch.object(COMPLETION, "dispatch", return_value=0) as dispatched: + COMPLETION.drain("synthetic-account", "/synthetic/db", "claude") + dispatched.assert_called_once_with(capture) + + def test_capture_exact_byte_version_not_size_only(self) -> None: + metadata = {"sha256": "0" * 64, "byte_count": 7} + capture = COMPLETION.Capture(Path("/synthetic"), "1" * 64, Path("/synthetic/capture.jsonl"), metadata) + value = result() + self.assertTrue(COMPLETION.strict_complete(value, capture)) + value.coverage["source_version"]["sha256"] = "2" * 64 + self.assertFalse(COMPLETION.strict_complete(value, capture)) + value.coverage["source_version"]["sha256"] = "0" * 64 + value.coverage["source_version"]["byte_count"] = 8 + self.assertFalse(COMPLETION.strict_complete(value, capture)) + + def test_identity_keeps_exact_session_account_host_and_locator(self) -> None: + baseline = ["account-a", "/synthetic/db", "claude", "synthetic/a", "/synthetic/source"] + identity = COMPLETION._digest(baseline) + for index, value in enumerate(("account-b", "/synthetic/other-db", "codex", "synthetica", "/synthetic/other-source")): + changed = list(baseline) + changed[index] = value + self.assertNotEqual(identity, COMPLETION._digest(changed)) + with self.assertRaises(COMPLETION.CompletionError): + COMPLETION._text("x" * 257, 256) + self.assertEqual(COMPLETION.MAX_CAPTURE_BYTES, 16777216) + self.assertEqual(COMPLETION.MAX_ACCOUNT_BYTES, 67108864) + self.assertEqual(COMPLETION.MAX_CAPTURES, 1024) + self.assertEqual(COMPLETION.MAX_STATE_BYTES, 8388608) + self.assertEqual(COMPLETION.MAX_ATTEMPTS, 5) + + def test_failure_backoff_and_exhaustion_retain_outcomes(self) -> None: + entry = {"attempts": 1, "outcomes": []} + with patch.object(COMPLETION.time, "time", return_value=100): + COMPLETION._outcome(entry, "storage_unproven") + self.assertEqual(entry["next_attempt_at"], 102) + self.assertEqual(entry["status"], "retry") + entry["attempts"] = 5 + with patch.object(COMPLETION.time, "time", return_value=100): + COMPLETION._outcome(entry, "worker_unproven") + self.assertEqual(entry["next_attempt_at"], 132) + self.assertEqual(entry["status"], "exhausted") + self.assertEqual([item["category"] for item in entry["outcomes"]], ["storage_unproven", "worker_unproven"]) + + +class InertPipelineCompletionControls(unittest.TestCase): + def setUp(self) -> None: + source = ROOT / "truememory/ingest/pipeline.py" + names = {"IngestionResult", "_invalidate_recall_after_batch", "IngestionPipeline"} + nodes = [node for node in ast.parse(source.read_text()).body if isinstance(node, (ast.ClassDef, ast.FunctionDef)) and node.name in names] + action = enum.Enum("DedupAction", ["ADD", "UPDATE", "SKIP"]) + self.namespace = {"dataclass": dataclass, "field": field, "contextlib": contextlib, "Iterator": object, + "time": time, "datetime": datetime, "sqlite3": sqlite3, "Path": Path, "os": os, + "log": logging.getLogger(__name__), "_safe_log": str, "DedupAction": action, + "_dedup_store_lock": contextlib.nullcontext, + "format_for_extraction": lambda messages: "User: " + "synthetic durable source " * 4, + "SourceSegment": lambda *values: values, "source_context": lambda *values: None, + "fact_metadata": lambda *values: {}, "gate_observation": lambda *values: {}, + "ProvenanceError": type("SyntheticProvenanceError", (sqlite3.OperationalError,), {}), + "source_timestamp": lambda metadata: "", + "check_duplicate": lambda *args, **kwargs: types.SimpleNamespace(action=action.ADD, fact="Synthetic preference", reason="synthetic", existing_id=None)} + exec(compile(ast.fix_missing_locations(ast.Module(body=[ast.ImportFrom(module="__future__", names=[ast.alias(name="annotations")], level=0), *nodes], type_ignores=[])), str(source), "exec"), self.namespace) + self.pipeline = self.namespace["IngestionPipeline"].__new__(self.namespace["IngestionPipeline"]) + self.pipeline._record_source_provenance = True + self.pipeline.memory = types.SimpleNamespace() + self.pipeline.user_id = "synthetic-account" + self.pipeline.llm_config = object() + self.pipeline.use_llm_dedup = False + self.pipeline.gate_enabled = True + decision = types.SimpleNamespace(should_encode=True, encoding_score=1, novelty=1, salience=1, prediction_error=1, reason="synthetic") + self.pipeline.gate = types.SimpleNamespace(reset_batch=lambda: None, evaluate=lambda *args: decision) + self.parser = types.ModuleType("truememory.ingest.transcript") + self.parser_calls = [] + version = types.SimpleNamespace(stable=True, sha256="0" * 64, byte_count=7) + message = types.SimpleNamespace(role="human", timestamp="", content="synthetic") + self.parser.parse_transcript_outcome = lambda source: self.parser_calls.append(source) or types.SimpleNamespace(messages=[message], status="complete", file_version=version) + self.extractor = types.ModuleType("truememory.ingest.extractor") + self.extraction_calls = [] + self.extraction = types.SimpleNamespace(facts=[], status="complete") + self.extractor.extract_facts_outcome = lambda *args: self.extraction_calls.append(args) or self.extraction + self.runtime = types.ModuleType("truememory.tier_switch.runtime") + self.runtime.fact_operation = lambda memory: contextlib.nullcontext() + self.runtime.raise_if_serving_rejection = lambda error: None + self.shared = types.ModuleType("truememory.ingest.hooks._shared") + self.shared.invalidate_recall_cache = lambda: None + self.modules = {self.parser.__name__: self.parser, self.extractor.__name__: self.extractor, + self.runtime.__name__: self.runtime, self.shared.__name__: self.shared} + + def test_rich_extraction_is_observed_once_and_successful_empty_is_complete(self) -> None: + with patch.dict(sys.modules, self.modules): + value = self.pipeline.ingest_transcript("synthetic.jsonl", "exact/session", require_complete=True) + self.assertEqual(len(self.parser_calls), 1) + self.assertEqual(len(self.extraction_calls), 1) + self.assertTrue(COMPLETION.strict_complete(value)) + + def test_partial_extraction_and_heuristic_do_not_complete(self) -> None: + self.extraction.status = "limited" + with patch.dict(sys.modules, self.modules): + value = self.pipeline.ingest_transcript("synthetic.jsonl", require_complete=True) + self.assertFalse(COMPLETION.strict_complete(value)) + self.pipeline.llm_config = None + self.namespace["extract_facts_simple"] = lambda text: [] + with patch.dict(sys.modules, self.modules): + value = self.pipeline.ingest_transcript("synthetic.jsonl", require_complete=True) + self.assertFalse(COMPLETION.strict_complete(value)) + self.assertEqual(len(self.extraction_calls), 1) + + def test_commit_then_raise_keeps_fact_nonterminal(self) -> None: + self.extraction.facts = [types.SimpleNamespace(content="Synthetic preference", category="preference", confidence=0.9)] + connection = sqlite3.connect(":memory:") + connection.execute("CREATE TABLE synthetic_fact (content TEXT)") + def ambiguous_write(*args, **kwargs): + connection.execute("INSERT INTO synthetic_fact VALUES (?)", ("Synthetic preference",)) + connection.commit() + raise sqlite3.OperationalError("synthetic commit then raise") + self.pipeline._store_fact = ambiguous_write + try: + with patch.dict(sys.modules, self.modules): + value = self.pipeline.ingest_transcript("synthetic.jsonl", require_complete=True) + self.assertEqual(connection.execute("SELECT COUNT(*) FROM synthetic_fact").fetchone()[0], 1) + self.assertEqual(value.facts_stored, 0) + self.assertEqual(value.trace[0]["action"], "storage_failed") + self.assertFalse(COMPLETION.strict_complete(value)) + finally: + connection.close() + + def test_initialized_source_add_requires_exact_positive_acknowledgement(self) -> None: + self.extraction.facts = [types.SimpleNamespace(content="Synthetic preference", category="preference", confidence=0.9)] + calls = [] + def add_result(acknowledgement): + self.pipeline.memory = types.SimpleNamespace(_engine=types.SimpleNamespace( + add=lambda **values: calls.append(values) or acknowledgement)) + with patch.dict(sys.modules, self.modules): + return self.pipeline.ingest_transcript("synthetic.jsonl", "exact/session", require_complete=True) + correct = {"id": 1, "content": "[preference] Synthetic preference", "sender": "synthetic-account", + "timestamp": "", "category": "preference", "metadata": {}} + for acknowledgement in (None, {}, {**correct, "id": None}, {**correct, "id": 0}, + {**correct, "id": True}, {**correct, "metadata": {"dropped": True}}, + {**correct, "timestamp": "2025"}, {**correct, "content": "other"}): + value = add_result(acknowledgement) + self.assertEqual(value.facts_stored, 0) + self.assertEqual(value.trace[0]["action"], "storage_failed") + self.assertFalse(COMPLETION.strict_complete(value)) + value = add_result(correct) + self.assertEqual(value.facts_stored, 1) + self.assertTrue(COMPLETION.strict_complete(value)) + self.pipeline._record_source_provenance = False + value = add_result(None) + self.assertEqual(value.facts_stored, 1) + self.assertFalse(COMPLETION.strict_complete(value)) + + +class FilesystemCompletionControls(unittest.TestCase): + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory(prefix="tm-synthetic-completion-") + self.directory = Path(self.temporary.name) + self.root = self.directory / "private-completion" + self.source = self.directory / "source.jsonl" + self.data = b"\n".join(json.dumps({"role": "user", "content": "synthetic source turn"}).encode() for _ in range(6)) + self.source.write_bytes(self.data) + self.shared = types.ModuleType("truememory.ingest.hooks._shared") + self.shared.is_allowed_transcript = lambda source, host: Path(source).parent == self.directory + self.shared.normalize_transcript_host = lambda host: host if host in ("claude", "codex", "gemini", "cursor") else "" + self.modules = patch.dict(sys.modules, {self.shared.__name__: self.shared}) + self.modules.start() + + def tearDown(self) -> None: + self.modules.stop() + self.temporary.cleanup() + + def capture(self, session: str = "synthetic/a", user: str = "synthetic-account", db: str = "/synthetic/db"): + return COMPLETION.admit_capture(str(self.source), session, user, db, "claude", root=self.root) + + def complete_result(self, capture): + value = result() + value.coverage["source_version"] = {name: capture.metadata[name] for name in ("sha256", "byte_count")} + return value + + def test_capture_is_exact_idempotent_and_scoped_without_sanitization(self) -> None: + first = self.capture() + self.assertEqual(first.path.read_bytes(), self.data) + self.assertEqual(first.key, self.capture().key) + self.assertNotEqual(first.key, self.capture("synthetica").key) + self.assertNotEqual(first.directory, self.capture(user="other-account").directory) + self.assertNotEqual(first.directory, self.capture(db="/synthetic/other-db").directory) + self.assertEqual(len(list((first.directory / "captures").iterdir())), 2) + + def test_same_size_revision_never_uses_old_receipt(self) -> None: + first = self.capture() + attempt = COMPLETION.reserve_inline_attempt(first) + with COMPLETION.worker_claim(first, attempt) as allowed: + self.assertTrue(allowed) + self.assertTrue(COMPLETION.record_receipt(first, self.complete_result(first))) + replacement = self.data.replace(b"source", b"change") + self.assertEqual(len(replacement), len(self.data)) + self.source.write_bytes(replacement) + second = self.capture() + self.assertNotEqual(first.key, second.key) + self.assertFalse(COMPLETION.has_receipt(second)) + self.assertTrue(COMPLETION.has_receipt(first)) + self.assertEqual(first.path.read_bytes(), self.data) + + def test_limits_backpressure_preserve_original_and_prior_capture(self) -> None: + first = self.capture() + with patch.object(COMPLETION, "MAX_CAPTURE_BYTES", len(self.data) - 1): + with self.assertRaises(COMPLETION.CompletionError): + self.capture("other-session") + self.assertEqual(self.source.read_bytes(), self.data) + self.assertEqual(first.path.read_bytes(), self.data) + with patch.object(COMPLETION, "MAX_ACCOUNT_BYTES", len(self.data)): + with self.assertRaises(COMPLETION.CompletionError): + self.capture("other-session") + self.assertEqual(len(list((first.directory / "captures").iterdir())), 1) + + def test_five_failed_attempts_retain_capture_outcomes_and_no_receipt(self) -> None: + capture = self.capture() + for number in range(1, 6): + with patch.object(COMPLETION.time, "time", return_value=number * 1000): + attempt = COMPLETION.reserve_inline_attempt(capture) + self.assertEqual(attempt, number) + with COMPLETION.worker_claim(capture, attempt) as allowed: + self.assertTrue(allowed) + self.assertFalse(COMPLETION.record_receipt(capture, result(extractor="partial"))) + with patch.object(COMPLETION.time, "time", return_value=10000): + self.assertEqual(COMPLETION.reserve_inline_attempt(capture), 0) + state = json.loads((capture.directory / "state.json").read_text())["captures"][capture.key] + self.assertEqual(state["status"], "exhausted") + self.assertEqual(len(state["outcomes"]), 5) + self.assertEqual(capture.path.read_bytes(), self.data) + self.assertFalse(COMPLETION.has_receipt(capture)) + + def test_os_claim_is_exclusive_and_exception_does_not_complete(self) -> None: + capture = self.capture() + attempt = COMPLETION.reserve_inline_attempt(capture) + with self.assertRaises(RuntimeError): + with COMPLETION.worker_claim(capture, attempt) as first: + self.assertTrue(first) + with COMPLETION.worker_claim(capture, attempt) as second: + self.assertFalse(second) + raise RuntimeError("synthetic commit ambiguity") + self.assertFalse(COMPLETION.has_receipt(capture)) + state = json.loads((capture.directory / "state.json").read_text())["captures"][capture.key] + self.assertEqual(state["outcomes"][0]["category"], "worker_unproven") + self.assertEqual(capture.path.read_bytes(), self.data) + + def test_receipt_requires_owned_attempt_and_keeps_capture_without_it(self) -> None: + capture = self.capture() + with self.assertRaisesRegex(COMPLETION.CompletionError, "receipt_without_owned_attempt"): + COMPLETION.record_receipt(capture, self.complete_result(capture)) + self.assertFalse(COMPLETION.has_receipt(capture)) + self.assertEqual(capture.path.read_bytes(), self.data) + + def test_symlink_capture_and_corrupt_state_are_rejected(self) -> None: + capture = self.capture() + link = self.directory / "linked.jsonl" + try: + link.symlink_to(self.source) + except (OSError, NotImplementedError): + self.skipTest("synthetic symlink unavailable") + with self.assertRaises(COMPLETION.CompletionError): + COMPLETION.admit_capture(str(link), "synthetic", "synthetic-account", "/synthetic/db", root=self.root) + state = capture.directory / "state.json" + state.write_text("invalid synthetic state") + with self.assertRaises(COMPLETION.CompletionError): + self.capture() + self.assertEqual(capture.path.read_bytes(), self.data) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-ingestion-outcomes-768.py b/tests/test-ingestion-outcomes-768.py new file mode 100644 index 00000000..d36866c0 --- /dev/null +++ b/tests/test-ingestion-outcomes-768.py @@ -0,0 +1,550 @@ +"""Real parser/extractor coverage with synthetic files and a fake provider only.""" +from __future__ import annotations + +import builtins +import hashlib +import json +import logging +import sys +import tempfile +import types +import unittest +from dataclasses import asdict +from pathlib import Path +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] +SYNTHETIC_SECRET = "synthetic-private-diagnostic-sentinel" + + +class FakeLLMError(Exception): + pass + + +def load_module(name: str) -> types.ModuleType: + def forbidden_provider(*args: object, **kwargs: object) -> str: + raise AssertionError("A test must install its own synthetic provider") + + models = types.SimpleNamespace(LLMConfig=object, LLMError=FakeLLMError, complete=forbidden_provider) + + def safe_import(name: str, globals: dict | None = None, locals: dict | None = None, + fromlist: tuple = (), level: int = 0) -> object: + if name == "truememory.ingest.models": + return models + if name.startswith(("truememory", "torch", "numpy", "sentence_transformers")): + raise AssertionError("Unexpected package or model import") + return builtins.__import__(name, globals, locals, fromlist, level) + + module = types.ModuleType("synthetic_p01_" + name) + module.__dict__["__builtins__"] = dict(vars(builtins), __import__=safe_import) + path = ROOT / "truememory/ingest" / (name + ".py") + with patch.dict(sys.modules, {module.__name__: module}): + exec(compile(path.read_text(encoding="utf-8"), str(path), "exec"), module.__dict__) + return module + + +def capture_diagnostic(outcome: object) -> dict[str, object]: + """Report capture categories without source bytes, paths or identity values.""" + version = getattr(outcome, "file_version") + result = {"status": getattr(outcome, "status"), + "errors": getattr(outcome, "error_categories"), + "capture_present": version is not None} + if version is not None: + fields = ("device", "inode", "size", "mtime_ns", "ctime_ns") + result.update( + stable=version.stable, + byte_count_matches=version.byte_count == version.after.size, + path_after_present=version.path_after is not None, + changed_during_read=tuple(name for name in fields + if getattr(version.before, name) != getattr(version.after, name)), + changed_after_close=None if version.path_after is None else tuple( + name for name in fields + if getattr(version.after, name) != getattr(version.path_after, name)), + ) + return result + + +class TestSourceLoader(unittest.TestCase): + def test_source_loader_selects_utf8_under_cp1252_default(self) -> None: + source = "# synthetic locale control \u201d\nENCODING_SENTINEL = 'synthetic-marker'\n".encode("utf-8") + with self.assertRaises(UnicodeDecodeError): + source.decode("cp1252") + encodings = [] + + def read_source(path: Path, encoding: str | None = None, errors: str | None = None) -> str: + encodings.append(encoding) + return source.decode(encoding or "cp1252", errors or "strict") + + with patch.object(Path, "read_text", read_source): + loaded = load_module("extractor") + self.assertEqual(loaded.ENCODING_SENTINEL, "synthetic-marker") + self.assertEqual(encodings, ["utf-8"]) + + +class TestTranscriptOutcomes(unittest.TestCase): + def setUp(self) -> None: + self.parser = load_module("transcript") + self.directory = tempfile.TemporaryDirectory(prefix="synthetic-ingestion-") + self.addCleanup(self.directory.cleanup) + self.directory_path = Path(self.directory.name) + + def write(self, data: bytes, name: str = "transcript.jsonl") -> Path: + path = self.directory_path / name + path.write_bytes(data) + return path + + def test_valid_empty_and_known_nonconversation_records(self) -> None: + inputs = ("", " ", "[]", '{"type":"progress"}', '{"type":"file-history-snapshot"}', + '[{"type":"system","content":"synthetic metadata"}]', + '[{"role":"user","content":""}]') + for text in inputs: + with self.subTest(text=text): + outcome = self.parser.parse_transcript_outcome(text) + self.assertTrue(outcome.complete) + self.assertEqual(outcome.messages, []) + self.assertEqual(outcome.error_categories, ()) + self.assertTrue(self.parser.parse_transcript_outcome(self.write(b"")).complete) + + def test_explicit_missing_path_is_unreadable_but_string_remains_inline(self) -> None: + path = self.directory_path / "missing.jsonl" + outcome = self.parser.parse_transcript_outcome(path) + self.assertEqual((outcome.status, outcome.messages, outcome.file_version), ("unreadable", [], None)) + self.assertEqual(outcome.error_categories, ("source_unreadable",)) + self.assertNotIn(str(path), repr(outcome)) + inline = self.parser.parse_transcript_outcome(str(path)) + self.assertTrue(inline.complete) + self.assertEqual(inline.messages[0].content, str(path)) + self.assertEqual(self.parser.parse_transcript(path), []) + + def test_file_version_binds_captured_bytes_not_later_file(self) -> None: + data = b'{"role":"user","content":"synthetic amber station"}\n' + path = self.write(data) + outcome = self.parser.parse_transcript_outcome(path) + version = outcome.file_version + self.assertTrue(outcome.complete) + self.assertTrue(version.stable) + self.assertEqual(version.byte_count, len(data)) + self.assertEqual(version.sha256, hashlib.sha256(data).hexdigest()) + self.assertEqual(version.before.inode, path.stat().st_ino) + path.write_bytes(data + data) + self.assertEqual(version.byte_count, len(data)) + self.assertNotEqual(version.after.size, path.stat().st_size) + self.assertNotIn("synthetic amber station", repr(outcome)) + + def test_file_change_during_capture_is_not_complete(self) -> None: + path = self.write(b'{"role":"user","content":"synthetic station"}\n') + real_open = Path.open + + class ChangingReader: + def __enter__(self) -> ChangingReader: + self.handle = real_open(path, "rb") + return self + + def __exit__(self, *args: object) -> None: + self.handle.close() + + def fileno(self) -> int: + return self.handle.fileno() + + def read(self) -> bytes: + data = self.handle.read() + with real_open(path, "ab") as writer: + writer.write(b'{"role":"user","content":"synthetic later row"}\n') + return data + + with patch.object(Path, "open", return_value=ChangingReader()): + outcome = self.parser.parse_transcript_outcome(path) + self.assertFalse(outcome.complete) + self.assertFalse(outcome.file_version.stable) + self.assertEqual(len(outcome.messages), 1) + self.assertIn("source_changed_during_read", outcome.error_categories) + + def test_windows_stat_and_fstat_ctime_semantics_do_not_reject_stable_file(self) -> None: + path = self.write(b'{"role":"user","content":"synthetic stable file"}\n') + real = path.stat() + common = {name: getattr(real, name) for name in ("st_dev", "st_ino", "st_size", "st_mtime_ns")} + by_handle = types.SimpleNamespace(**common, st_ctime_ns=731001) + by_path = types.SimpleNamespace(**common, st_ctime_ns=731002) + with patch.object(self.parser.os, "fstat", return_value=by_handle) as fd_stat, \ + patch.object(Path, "stat", return_value=by_path): + outcome = self.parser.parse_transcript_outcome(path) + self.assertTrue(outcome.complete, capture_diagnostic(outcome)) + self.assertTrue(outcome.file_version.stable) + self.assertEqual(outcome.file_version.after.ctime_ns, 731001) + self.assertEqual(outcome.file_version.path_after.ctime_ns, 731001) + self.assertEqual(fd_stat.call_count, 3) + + def test_ctime_only_change_during_read_or_final_reopen_is_not_complete(self) -> None: + path = self.write(b'{"role":"user","content":"synthetic timestamp change"}\n') + real = path.stat() + common = {name: getattr(real, name) for name in ("st_dev", "st_ino", "st_size", "st_mtime_ns")} + for changed_call in (2, 3): + with self.subTest(changed_call=changed_call): + states = [types.SimpleNamespace(**common, st_ctime_ns=731001 + int(call >= changed_call)) + for call in (1, 2, 3)] + with patch.object(self.parser.os, "fstat", side_effect=states): + outcome = self.parser.parse_transcript_outcome(path) + self.assertFalse(outcome.complete) + self.assertEqual(outcome.error_categories, ("source_changed_during_read",)) + self.assertEqual(outcome.file_version.byte_count, real.st_size) + self.assertEqual(len(outcome.messages), 1) + diagnostic = capture_diagnostic(outcome) + field = "changed_during_read" if changed_call == 2 else "changed_after_close" + self.assertEqual(diagnostic[field], ("ctime_ns",)) + + def test_replaced_path_after_read_is_not_the_captured_file(self) -> None: + data = b'{"role":"user","content":"synthetic replacement race"}\n' + path = self.write(data) + replacement = self.write(data, "replacement.jsonl") + real_open = Path.open + handles = [] + + def open_with_replacement(target: Path, mode: str) -> object: + self.assertEqual(target, path) + self.assertEqual(mode, "rb") + if handles: + self.assertTrue(handles[0].closed) + replacement.replace(path) + handle = real_open(target, mode) + handles.append(handle) + return handle + + with patch.object(Path, "open", open_with_replacement): + outcome = self.parser.parse_transcript_outcome(path) + self.assertEqual(len(handles), 2) + self.assertTrue(all(handle.closed for handle in handles)) + self.assertFalse(outcome.complete) + self.assertEqual(outcome.error_categories, ("source_changed_during_read",)) + self.assertNotEqual(outcome.file_version.after.inode, outcome.file_version.path_after.inode) + self.assertEqual(outcome.file_version.sha256, hashlib.sha256(data).hexdigest()) + self.assertEqual(outcome.messages[0].content, "synthetic replacement race") + + def test_deleted_or_denied_path_after_read_preserves_captured_messages(self) -> None: + data = b'{"role":"user","content":"synthetic unavailable path"}\n' + real_open = Path.open + for cause in ("deleted", "denied"): + with self.subTest(cause=cause): + path = self.write(data) + handles = [] + attempts = [] + + def open_with_failure(target: Path, mode: str) -> object: + self.assertEqual((target, mode), (path, "rb")) + attempts.append(True) + if handles: + self.assertTrue(handles[0].closed) + if cause == "deleted": + path.unlink() + else: + raise PermissionError(SYNTHETIC_SECRET) + handle = real_open(target, mode) + handles.append(handle) + return handle + + with patch.object(Path, "open", open_with_failure): + outcome = self.parser.parse_transcript_outcome(path) + self.assertEqual(len(attempts), 2) + self.assertTrue(all(handle.closed for handle in handles)) + self.assertEqual(outcome.status, "partial") + self.assertEqual(outcome.error_categories, ("source_changed_during_read",)) + self.assertIsNone(outcome.file_version.path_after) + self.assertEqual(outcome.file_version.sha256, hashlib.sha256(data).hexdigest()) + self.assertEqual(outcome.messages[0].content, "synthetic unavailable path") + self.assertNotIn(SYNTHETIC_SECRET, repr(outcome)) + + def test_final_identity_handle_never_reads_and_closes_on_fstat_failure(self) -> None: + path = self.write(b'{"role":"user","content":"synthetic final handle"}\n') + real_open, real_fstat = Path.open, self.parser.os.fstat + for failed_stat in (False, True): + with self.subTest(failed_stat=failed_stat): + handles = [] + stat_calls = [] + + class IdentityProbe: + def __init__(self, handle: object) -> None: + self.handle = handle + + def __enter__(self) -> IdentityProbe: + return self + + def __exit__(self, *args: object) -> None: + self.handle.close() + + def fileno(self) -> int: + return self.handle.fileno() + + def read(self, *args: object) -> bytes: + raise AssertionError("Final identity probe must not reread source bytes") + + def observed_open(target: Path, mode: str) -> object: + self.assertEqual((target, mode), (path, "rb")) + handle = real_open(target, mode) + handles.append(handle) + return handle if len(handles) == 1 else IdentityProbe(handle) + + def observed_fstat(fd: int) -> object: + stat_calls.append(True) + if failed_stat and len(stat_calls) == 3: + raise OSError(SYNTHETIC_SECRET) + return real_fstat(fd) + + with patch.object(Path, "open", observed_open), \ + patch.object(self.parser.os, "fstat", observed_fstat): + outcome = self.parser.parse_transcript_outcome(path) + self.assertEqual(len(handles), 2) + self.assertEqual(len(stat_calls), 3) + self.assertTrue(all(handle.closed for handle in handles)) + self.assertEqual(outcome.complete, not failed_stat, capture_diagnostic(outcome)) + if failed_stat: + self.assertIsNone(outcome.file_version.path_after) + self.assertEqual(outcome.error_categories, ("source_changed_during_read",)) + self.assertNotIn(SYNTHETIC_SECRET, repr(outcome)) + + def test_capture_diagnostic_reports_fields_without_identity_or_content(self) -> None: + before = self.parser.FileState(731001, 731002, 731003, 731004, 731005) + after = self.parser.FileState(731001, 731002, 731006, 731007, 731005) + path_after = self.parser.FileState(731001, 731008, 731006, 731007, 731009) + version = self.parser.TranscriptFileVersion(731003, SYNTHETIC_SECRET, before, after, path_after) + outcome = self.parser.TranscriptOutcome( + [self.parser.Message("human", SYNTHETIC_SECRET)], "partial", + error_categories=("source_changed_during_read",), file_version=version, + ) + diagnostic = capture_diagnostic(outcome) + self.assertEqual(diagnostic, { + "status": "partial", "errors": ("source_changed_during_read",), + "capture_present": True, "stable": False, "byte_count_matches": False, + "path_after_present": True, "changed_during_read": ("size", "mtime_ns"), + "changed_after_close": ("inode", "ctime_ns"), + }) + rendered = repr(diagnostic) + self.assertNotIn(SYNTHETIC_SECRET, rendered) + self.assertNotIn("73100", rendered) + missing = self.parser.TranscriptOutcome([], "unreadable", error_categories=("source_unreadable",)) + self.assertEqual(capture_diagnostic(missing), { + "status": "unreadable", "errors": ("source_unreadable",), "capture_present": False, + }) + + def test_unreadable_and_invalid_utf8_do_not_leak_diagnostics(self) -> None: + path = self.write(b"User: synthetic \xff station", "transcript.txt") + outcome = self.parser.parse_transcript_outcome(path) + self.assertEqual(outcome.status, "partial") + self.assertIn("invalid_utf8", outcome.error_categories) + self.assertEqual(outcome.file_version.sha256, hashlib.sha256(path.read_bytes()).hexdigest()) + with patch.object(Path, "open", side_effect=PermissionError(SYNTHETIC_SECRET)): + unreadable = self.parser.parse_transcript_outcome(path) + self.assertEqual(unreadable.status, "unreadable") + self.assertNotIn(SYNTHETIC_SECRET, repr(unreadable)) + with patch.object(Path, "stat", side_effect=PermissionError(SYNTHETIC_SECRET)): + denied_probe = self.parser.parse_transcript_outcome(str(path)) + self.assertEqual((denied_probe.status, denied_probe.messages), ("unreadable", [])) + + def test_malformed_array_preserves_legacy_salvage_without_complete_claim(self) -> None: + text = '[{"role":"user","content":"synthetic"}, BROKEN]' + outcome = self.parser.parse_transcript_outcome(text) + self.assertEqual(outcome.status, "salvaged") + self.assertEqual(outcome.malformed_records, 1) + self.assertEqual(outcome.messages, self.parser.parse_transcript(text)) + + def test_jsonl_malformed_first_line_and_wrong_record_types_are_counted(self) -> None: + path = self.write(b'BROKEN\n{"role":"user","content":"synthetic station"}\nnull\n') + outcome = self.parser.parse_transcript_outcome(path) + self.assertEqual((outcome.status, outcome.total_records, outcome.malformed_records), ("partial", 3, 2)) + self.assertEqual(len(outcome.messages), 1) + for text in ('{"bad":"shape"}', '[17, {"role":"user","content":false}]', '{BROKEN', + '{"role":"user","content":[null,17]}'): + with self.subTest(text=text): + self.assertEqual(self.parser.parse_transcript_outcome(text).status, "malformed") + + def test_valid_supported_formats_match_legacy_messages(self) -> None: + entries = [ + {"type": "file-history-snapshot"}, + {"type": "user", "message": {"content": "synthetic human message"}}, + {"type": "assistant", "message": {"content": [ + {"type": "thinking", "thinking": "synthetic excluded"}, + {"type": "text", "text": "synthetic reply"}]}}, + {"type": "user", "message": {"content": [ + {"type": "tool_result", "content": "synthetic tool"}]}}, + ] + for text in (json.dumps(entries), "\n".join(map(json.dumps, entries)), + "User: synthetic station\nAssistant: synthetic reply"): + with self.subTest(format=text[:1]): + path = self.write(text.encode(), "supported.txt") + outcome = self.parser.parse_transcript_outcome(path) + self.assertTrue(outcome.complete, capture_diagnostic(outcome)) + self.assertEqual(outcome.messages, self.parser.parse_transcript(path)) + + def test_top_level_legacy_tool_call_retains_its_message(self) -> None: + text = json.dumps({"type": "tool_use", "name": "synthetic-tool", "input": {"value": "synthetic"}}) + outcome = self.parser.parse_transcript_outcome(text) + self.assertTrue(outcome.complete) + self.assertEqual(len(outcome.messages), 1) + self.assertEqual(outcome.messages, self.parser.parse_transcript(text)) + self.assertEqual(outcome.messages[0].role, "tool_use") + + def test_missing_text_is_malformed_but_recognized_empty_and_thinking_are_complete(self) -> None: + for block in ({"type": "text"}, {"type": "text", "text": None}): + with self.subTest(block=block): + outcome = self.parser.parse_transcript_outcome(json.dumps({"role": "user", "content": [block]})) + self.assertEqual((outcome.status, outcome.malformed_records), ("malformed", 1)) + self.assertEqual(outcome.messages, []) + for block in ({"type": "text", "text": ""}, {"type": "thinking", "thinking": "synthetic excluded"}): + with self.subTest(kind=block["type"]): + outcome = self.parser.parse_transcript_outcome(json.dumps({"role": "assistant", "content": [block]})) + self.assertTrue(outcome.complete) + self.assertEqual(outcome.messages, []) + + def test_unknown_and_nontext_payloads_are_incomplete_without_guessing_text(self) -> None: + for kind in ("image", "audio", "document", "synthetic-unknown"): + with self.subTest(kind=kind): + text = json.dumps({"role": "user", "content": [{"type": kind, "text": "synthetic unrecognized"}]}) + outcome = self.parser.parse_transcript_outcome(text) + self.assertEqual(outcome.status, "partial") + self.assertEqual(outcome.unrecognized_records, 1) + self.assertEqual(outcome.error_categories, ("unrecognized_content",)) + self.assertEqual(outcome.messages, []) + self.assertEqual(self.parser.parse_transcript(text), []) + + def test_mixed_blocks_retain_supported_text_and_count_record_loss_once(self) -> None: + entries = [ + {"role": "user", "content": [{"type": "text", "text": "synthetic retained"}, + {"type": "text"}, {"type": "image"}, {"type": "unknown"}]}, + {"type": "user", "message": {"content": [{"type": "tool_result", "content": [ + {"type": "text", "text": "synthetic tool result"}, {"type": "text"}, {"type": "image"}]}]}}, + ] + path = self.write(json.dumps(entries).encode(), "mixed.json") + outcome = self.parser.parse_transcript_outcome(path) + self.assertEqual(outcome.status, "partial") + self.assertEqual((outcome.malformed_records, outcome.unrecognized_records), (2, 2)) + self.assertEqual([message.content for message in outcome.messages], ["synthetic retained", "synthetic tool result"]) + self.assertEqual(outcome.messages, self.parser.parse_transcript(path)) + self.assertEqual(outcome.messages[1].role, "tool_result") + self.assertEqual(outcome.error_categories, ("malformed_records", "unrecognized_content")) + + +class TestExtractionOutcomes(unittest.TestCase): + def setUp(self) -> None: + self.extractor = load_module("extractor") + self.config = types.SimpleNamespace(provider="synthetic") + self.addCleanup(logging.disable, logging.NOTSET) + logging.disable(logging.CRITICAL) + + def run_responses(self, responses: list, *, max_facts: int = 50, max_chunks: int = 20, + wrapper: bool = False) -> tuple: + # One over-budget message stays intact, preserving the production + # chunker's deliberate no-splitting policy. + transcript = "\n\n".join("User: synthetic " + str(index) + "x" * 20001 + for index in range(len(responses))) + fn = self.extractor.extract_facts if wrapper else self.extractor.extract_facts_outcome + with patch.object(self.extractor, "complete", side_effect=responses) as provider: + result = fn(transcript, self.config, max_facts=max_facts, max_chunks=max_chunks) + return result, provider.call_count + + def test_valid_empty_responses_and_empty_input(self) -> None: + for response in ("[]", '{"facts":[]}', '{"items":[]}', "```json\n[]\n```", "Facts: [] done"): + with self.subTest(response=response): + outcome, calls = self.run_responses([response]) + self.assertEqual((outcome.status, outcome.facts, calls), ("complete", [], 1)) + self.assertEqual((outcome.attempted_chunks, outcome.successful_chunks), (1, 1)) + with patch.object(self.extractor, "complete") as provider: + empty = self.extractor.extract_facts_outcome(" ", self.config) + self.assertTrue(empty.complete) + self.assertEqual(empty.total_chunks, 0) + provider.assert_not_called() + + def test_failed_response_is_distinct_from_successful_empty(self) -> None: + for response in ("garbage", "{}", '{"unexpected":[]}', '[null,17,{}, {"content":" "}]', None): + with self.subTest(response=response): + outcome, calls = self.run_responses([response]) + self.assertEqual((outcome.status, calls, outcome.failed_chunks), ("failed", 1, 1)) + self.assertEqual(outcome.invalid_responses, 1) + self.assertFalse(outcome.complete) + self.assertTrue(outcome.error_categories) + + def test_mixed_provider_failure_and_valid_empty_is_partial(self) -> None: + responses = ['[{"content":"synthetic amber"}]', FakeLLMError(SYNTHETIC_SECRET), "[]"] + outcome, calls = self.run_responses(responses) + self.assertEqual((outcome.status, calls), ("partial", 3)) + self.assertEqual((outcome.successful_chunks, outcome.partial_chunks, outcome.failed_chunks), (2, 0, 1)) + self.assertEqual([fact.content for fact in outcome.facts], ["synthetic amber"]) + self.assertEqual(outcome.error_categories, ("provider_error",)) + self.assertNotIn(SYNTHETIC_SECRET, repr(outcome)) + self.assertNotIn("synthetic amber", repr(outcome)) + legacy, legacy_calls = self.run_responses(responses, wrapper=True) + self.assertEqual(legacy, outcome.facts) + self.assertEqual(legacy_calls, calls) + + def test_all_provider_failures_are_failed_without_retry_or_error_text(self) -> None: + logging.disable(logging.NOTSET) + with self.assertLogs(self.extractor.log, level="ERROR") as captured: + outcome, calls = self.run_responses([RuntimeError(SYNTHETIC_SECRET), FakeLLMError(SYNTHETIC_SECRET)]) + self.assertEqual((outcome.status, calls, outcome.failed_chunks), ("failed", 2, 2)) + self.assertNotIn(SYNTHETIC_SECRET, "\n".join(captured.output) + repr(outcome)) + self.assertEqual(outcome.error_categories, ("provider_exception", "provider_error")) + + def test_mixed_invalid_items_preserve_coercion_and_order(self) -> None: + response = '[{"content":"synthetic first","category":"INVALID"},17,{}, {"content":42}]' + outcome, _ = self.run_responses([response]) + self.assertEqual((outcome.status, outcome.invalid_items, outcome.partial_chunks), ("partial", 2, 1)) + self.assertEqual([fact.content for fact in outcome.facts], ["synthetic first", "42"]) + self.assertEqual(outcome.facts[0].category, "general") + + def test_salvage_is_visible_and_legacy_selection_is_preserved(self) -> None: + response = '[{"content":"synthetic first"}, BROKEN, {"content":"synthetic second"}]' + outcome, calls = self.run_responses([response], max_facts=1) + self.assertEqual((outcome.status, calls, outcome.salvaged_responses), ("partial", 1, 1)) + self.assertEqual(outcome.merged_fact_omissions, 1) + self.assertEqual([fact.content for fact in outcome.facts], ["synthetic first"]) + # The legacy response salvage is uncapped; the extraction merge then + # enforces max_facts. Do not silently change this compatibility rule. + self.assertEqual(len(self.extractor._parse_extraction_response(response, 1)), 2) + + def test_per_chunk_cap_counts_valid_omissions_before_slicing(self) -> None: + response = '[{"content":"synthetic first"}, {}, {"content":"synthetic omitted"},17]' + outcome, _ = self.run_responses([response], max_facts=1) + self.assertEqual((outcome.omitted_items, outcome.chunk_fact_omissions), (3, 1)) + self.assertEqual(outcome.invalid_items, 2) + self.assertFalse(outcome.complete) + clean, _ = self.run_responses(['[{"content":"one"},{"content":"two"}]'], max_facts=1) + self.assertEqual((clean.status, clean.chunk_fact_omissions), ("limited", 1)) + + def test_merge_dedup_order_and_global_cap_match_legacy(self) -> None: + responses = ['[{"content":"First"},{"content":"DUP"}]', + '[{"content":"dup"},{"content":"Last"}]'] + outcome, calls = self.run_responses(responses, max_facts=2) + legacy, legacy_calls = self.run_responses(responses, max_facts=2, wrapper=True) + self.assertEqual([fact.content for fact in outcome.facts], ["First", "DUP"]) + self.assertEqual((outcome.status, outcome.merged_fact_omissions, outcome.chunk_fact_omissions), ("limited", 1, 0)) + self.assertEqual((legacy, legacy_calls), (outcome.facts, calls)) + + def test_default_chunk_limit_does_not_spend_tail_calls(self) -> None: + outcome, calls = self.run_responses(["[]"] * 21) + self.assertEqual((outcome.total_chunks, outcome.attempted_chunks, outcome.deferred_chunks), (21, 20, 1)) + self.assertEqual((outcome.status, calls), ("limited", 20)) + self.assertEqual(outcome.error_categories, ("chunk_limit",)) + + def test_zero_and_negative_controls_preserve_legacy_slicing(self) -> None: + for max_facts, max_chunks in ((0, 20), (-1, 20), (50, 0), (50, -1)): + with self.subTest(max_facts=max_facts, max_chunks=max_chunks): + responses = ['[{"content":"one"},{"content":"two"}]'] * 2 + outcome, calls = self.run_responses(responses, max_facts=max_facts, max_chunks=max_chunks) + legacy, legacy_calls = self.run_responses(responses, max_facts=max_facts, max_chunks=max_chunks, wrapper=True) + self.assertEqual((legacy, legacy_calls), (outcome.facts, calls)) + self.assertEqual(outcome.attempted_chunks + outcome.deferred_chunks, outcome.total_chunks) + self.assertFalse(outcome.complete) + + def test_wrong_object_envelope_is_not_complete_even_when_legacy_finds_array(self) -> None: + response = '{"unexpected":[{"content":"synthetic compatibility fact"}]}' + outcome, _ = self.run_responses([response]) + self.assertEqual(outcome.status, "failed") + self.assertEqual(outcome.error_categories, ("response_shape",)) + self.assertEqual(len(outcome.facts), 1) + self.assertEqual(self.extractor._parse_extraction_response(response, 50), outcome.facts) + + def test_fact_response_records_use_only_public_schema_fields(self) -> None: + outcome, _ = self.run_responses(['[{"content":"synthetic","category":"PREFERENCE","extra":"ignored"}]']) + self.assertEqual(asdict(outcome.facts[0]), {"content": "synthetic", "category": "preference", + "confidence": "medium", "source_role": "user"}) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-ingestion-recall-invalidation-769.py b/tests/test-ingestion-recall-invalidation-769.py new file mode 100644 index 00000000..78a8c758 --- /dev/null +++ b/tests/test-ingestion-recall-invalidation-769.py @@ -0,0 +1,342 @@ +"""Exercise both real ingestion routes with synthetic SQLite and recall files.""" +from __future__ import annotations + +import ast +import builtins +import contextlib +import datetime +import enum +import json +import logging +import os +import re +import sqlite3 +import sys +import tempfile +import time +import types +import unittest +from collections.abc import Iterator +from dataclasses import dataclass, field +from pathlib import Path +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] +TEXT = "User: Synthetic station uses amber panels and stores reusable engineering notes." + + +class Action(enum.Enum): + ADD = "add" + UPDATE = "update" + SKIP = "skip" + + +def selected_source(path: Path, names: set[str]) -> object: + tree = ast.parse(path.read_text()) + nodes = [node for node in tree.body + if isinstance(node, ast.ImportFrom) and node.module == "__future__" + or isinstance(node, (ast.FunctionDef, ast.ClassDef)) and node.name in names] + return compile(ast.Module(body=nodes, type_ignores=[]), str(path), "exec") + + +def load_cache(directory: Path) -> types.ModuleType: + module = types.ModuleType("synthetic_p02_cache") + module.__dict__.update(Path=Path, json=json, os=os, time=time, + RECALL_CACHE_PATH=directory / "recall-cache.json", + RECALL_CACHE_TTL=300, _TRUEMEMORY_ROOT=directory) + names = {"_secure_mkdir", "_atomic_write_text", "get_recall_cache", "set_recall_cache", + "invalidate_recall_cache", "_normalize_db_path", "_recall_cache_key_prefix", "_recall_cache_key"} + exec(selected_source(ROOT / "truememory/ingest/hooks/_shared.py", names), module.__dict__) + return module + + +def load_runtime() -> types.ModuleType: + serving = types.ModuleType("synthetic_p02_serving") + exec(selected_source(ROOT / "truememory/tier_switch/serving.py", + {"ServingLeaseError", "ServingLeaseTimeout", "ServingLeaseCancelled"}), + serving.__dict__) + runtime = types.ModuleType("synthetic_p02_runtime") + runtime.__dict__.update(contextmanager=contextlib.contextmanager, sys=sys, serving=serving) + exec(selected_source(ROOT / "truememory/tier_switch/runtime.py", + {"TierRuntimeError", "fact_operation", "raise_if_serving_rejection"}), + runtime.__dict__) + return runtime + + +def load_pipeline(cache: types.ModuleType) -> types.ModuleType: + runtime = load_runtime() + parser = types.ModuleType("synthetic_p02_transcript") + with patch.dict(sys.modules, {parser.__name__: parser}): + path = ROOT / "truememory/ingest/transcript.py" + exec(compile(path.read_text(), str(path), "exec"), parser.__dict__) + + def synthetic_import(name: str, globals: dict | None = None, locals: dict | None = None, + fromlist: tuple = (), level: int = 0) -> object: + if name == "truememory.ingest.hooks._shared": + return cache + if name == "truememory.tier_switch.runtime": + return runtime + if name.startswith("truememory"): + raise AssertionError("unexpected package/native import") + return builtins.__import__(name, globals, locals, fromlist, level) + + module = types.ModuleType("synthetic_p02_pipeline") + module.__dict__.update( + contextlib=contextlib, datetime=datetime, json=json, logging=logging, re=re, + sqlite3=sqlite3, time=time, Iterator=Iterator, dataclass=dataclass, field=field, Path=Path, + log=logging.getLogger("synthetic.p02"), DedupAction=Action, + _LOG_UNSAFE_RE=re.compile(r"[\x00-\x1f\x7f]"), _dedup_store_lock=contextlib.nullcontext, + parse_transcript=parser.parse_transcript, format_for_extraction=parser.format_for_extraction, + __builtins__=dict(vars(builtins), __import__=synthetic_import), + ) + names = {"_safe_log", "IngestionResult", "_invalidate_recall_after_batch", "IngestionPipeline"} + with patch.dict(sys.modules, {module.__name__: module}): + exec(selected_source(ROOT / "truememory/ingest/pipeline.py", names), module.__dict__) + return module + + +class SyntheticEngine: + def __init__(self, conn: sqlite3.Connection) -> None: + self.conn = conn + self.before_add = None + + def add(self, content: str, **fields: object) -> dict: + if self.before_add: + self.before_add(content) + cursor = self.conn.execute("INSERT INTO messages(content) VALUES (?)", (content,)) + self.conn.commit() + return {"id": cursor.lastrowid, "content": content} + + +class SyntheticMemory: + def __init__(self, conn: sqlite3.Connection) -> None: + self._engine = SyntheticEngine(conn) + self.before_update = None + + def add(self, content: str, **fields: object) -> dict: + return self._engine.add(content) + + def update(self, memory_id: int, content: str) -> dict: + if self.before_update: + self.before_update(content) + self._engine.conn.execute("UPDATE messages SET content=? WHERE id=?", (content, memory_id)) + self._engine.conn.commit() + return {"id": memory_id, "content": content} + + +class TestIngestionRecallInvalidation(unittest.TestCase): + def setUp(self) -> None: + self.temp = tempfile.TemporaryDirectory(prefix="synthetic-ingestion-cache-") + self.addCleanup(self.temp.cleanup) + self.directory = Path(self.temp.name) + self.path = self.directory / "synthetic.sqlite" + self.conn = sqlite3.connect(self.path, timeout=0) + self.addCleanup(self.conn.close) + self.conn.execute("CREATE TABLE messages(id INTEGER PRIMARY KEY,content TEXT)") + self.conn.commit() + self.reader = sqlite3.connect(self.path, timeout=0) + self.addCleanup(self.reader.close) + self.transcript = self.directory / "synthetic-transcript.txt" + self.transcript.write_text(TEXT) + self.cache = load_cache(self.directory) + self.module = load_pipeline(self.cache) + self.pipeline = self.module.IngestionPipeline.__new__(self.module.IngestionPipeline) + self.memory = SyntheticMemory(self.conn) + self.pipeline.memory = self.memory + self.pipeline.user_id = "synthetic-user" + self.pipeline.llm_config = None + self.pipeline.use_llm_dedup = False + self.pipeline.gate_enabled = True + self.pipeline.gate = types.SimpleNamespace(reset_batch=lambda: None, evaluate=self.gate) + self.module.check_duplicate = self.dedup + self.facts = [] + self.module.extract_facts_simple = lambda text: self.facts + self.module.extract_facts = lambda text, config: self.facts + self.invalidations = [] + self.cache_error = None + invalidate = self.cache.invalidate_recall_cache + + def observed_invalidate(*args: object, **kwargs: object) -> None: + # A second connection witnesses durable commits before invalidation. + self.invalidations.append(self.reader.execute("SELECT content FROM messages ORDER BY id").fetchall()) + if self.cache_error: + raise self.cache_error + invalidate(*args, **kwargs) + + self.cache.invalidate_recall_cache = observed_invalidate + + def gate(self, content: str, category: str) -> types.SimpleNamespace: + return types.SimpleNamespace(should_encode=not content.startswith("reject"), encoding_score=1.0, + novelty=1.0, salience=1.0, prediction_error=1.0, reason="synthetic") + + def dedup(self, content: str, memory: object, **kwargs: object) -> types.SimpleNamespace: + action = Action.SKIP if content.startswith("skip") else Action.UPDATE if content.startswith("replace") else Action.ADD + return types.SimpleNamespace(action=action, fact=content, existing_id=1, reason="synthetic") + + def set_facts(self, *contents: str) -> None: + self.facts = [types.SimpleNamespace(content=content, category="general", confidence="high") for content in contents] + + def seed_cache(self) -> None: + self.cache.set_recall_cache("synthetic stale", str(self.path), "synthetic-user") + self.assertEqual(self.cache.get_recall_cache(str(self.path), "synthetic-user"), "synthetic stale") + + def run_route(self, route: str) -> object: + return (self.pipeline.ingest_text(TEXT, session_id="synthetic-session") if route == "text" + else self.pipeline.ingest_transcript(self.transcript, session_id="synthetic-session")) + + def reset(self) -> None: + self.reader.rollback() + self.conn.rollback() + self.conn.execute("DELETE FROM messages") + self.conn.commit() + self.invalidations.clear() + self.memory._engine.before_add = None + self.memory.before_update = None + self.seed_cache() + + def test_both_routes_refresh_recall_after_committed_add(self) -> None: + for route in ("text", "transcript"): + with self.subTest(route=route): + self.reset() + self.set_facts("synthetic first fact", "synthetic second fact") + other_db = str(self.directory / "other-synthetic.sqlite") + self.cache.set_recall_cache("synthetic other scope", other_db, "other-synthetic-user", intensity="max", budget=2048) + result = self.run_route(route) + self.assertEqual((result.facts_extracted, result.facts_stored, result.facts_updated), (2, 2, 0)) + self.assertEqual(len(self.invalidations), 1) + self.assertEqual(len(self.invalidations[0]), 2) + self.assertIsNone(self.cache.get_recall_cache(str(self.path), "synthetic-user")) + self.assertIsNone(self.cache.get_recall_cache(other_db, "other-synthetic-user", intensity="max", budget=2048)) + fresh = " | ".join(row[0] for row in self.reader.execute("SELECT content FROM messages ORDER BY id")) + self.cache.set_recall_cache(fresh, str(self.path), "synthetic-user") + self.assertEqual(self.cache.get_recall_cache(str(self.path), "synthetic-user"), fresh) + + def test_updates_invalidate_after_commit_on_both_routes(self) -> None: + for route in ("text", "transcript"): + with self.subTest(route=route): + self.reset() + self.conn.execute("INSERT INTO messages VALUES(1,'synthetic old')") + self.conn.commit() + self.set_facts("replace synthetic old") + result = self.run_route(route) + self.assertEqual((result.facts_stored, result.facts_updated), (0, 1)) + self.assertEqual(self.invalidations, [[("replace synthetic old",)]]) + self.assertIsNone(self.cache.get_recall_cache(str(self.path), "synthetic-user")) + + def test_noop_batches_do_not_invalidate(self) -> None: + for route in ("text", "transcript"): + for contents in ((), ("reject synthetic",), ("skip synthetic",), ("reject synthetic", "skip synthetic")): + with self.subTest(route=route, size=len(contents)): + self.reset() + self.set_facts(*contents) + result = self.run_route(route) + self.assertEqual((result.facts_stored, result.facts_updated), (0, 0)) + self.assertEqual(self.invalidations, []) + self.assertEqual(self.cache.get_recall_cache(str(self.path), "synthetic-user"), "synthetic stale") + + def test_mixed_batch_sqlite_lock_failure_invalidates_only_confirmed_writes(self) -> None: + for route in ("text", "transcript"): + with self.subTest(route=route): + self.reset() + self.conn.execute("INSERT INTO messages VALUES(1,'synthetic old')") + self.conn.commit() + self.set_facts("synthetic added", "replace synthetic updated", "skip synthetic", "reject synthetic", "synthetic locked") + + def lock_last(content: str) -> None: + if content == "synthetic locked": + self.reader.execute("BEGIN IMMEDIATE") + + self.memory._engine.before_add = lock_last + with self.assertLogs("synthetic.p02", level="ERROR"): + result = self.run_route(route) + self.reader.rollback() + self.assertEqual((result.facts_extracted, result.facts_encoded, result.facts_stored, + result.facts_updated, result.facts_skipped_gate, result.facts_skipped_dedup), + (5, 4, 1, 1, 1, 1)) + self.assertEqual(len(self.invalidations), 1) + self.assertEqual(len(self.invalidations[0]), 2) + self.assertIsNone(self.cache.get_recall_cache(str(self.path), "synthetic-user")) + self.assertEqual(self.conn.execute("SELECT count(*) FROM messages").fetchone(), (2,)) + + def test_all_failed_adds_and_updates_leave_cache_unchanged(self) -> None: + for route in ("text", "transcript"): + with self.subTest(route=route): + self.reset() + self.conn.execute("INSERT INTO messages VALUES(1,'synthetic old')") + self.conn.commit() + self.reader.execute("BEGIN IMMEDIATE") + self.set_facts("synthetic locked", "replace synthetic locked") + with self.assertLogs("synthetic.p02", level="ERROR"): + result = self.run_route(route) + self.reader.rollback() + self.assertEqual((result.facts_stored, result.facts_updated), (0, 0)) + self.assertEqual(self.invalidations, []) + self.assertEqual(self.cache.get_recall_cache(str(self.path), "synthetic-user"), "synthetic stale") + + def test_later_gate_or_dedup_exception_preserves_original_and_invalidates(self) -> None: + for route in ("text", "transcript"): + for step in ("gate", "dedup"): + for failure in (RuntimeError("synthetic later failure"), KeyboardInterrupt("synthetic cancellation")): + with self.subTest(route=route, step=step, exception=type(failure).__name__): + self.reset() + self.set_facts("synthetic committed", "synthetic interrupted") + + def gate(content: str, category: str) -> object: + if content == "synthetic interrupted" and step == "gate": + raise failure + return self.gate(content, category) + + def dedup(content: str, memory: object, **kwargs: object) -> object: + if content == "synthetic interrupted" and step == "dedup": + raise failure + return self.dedup(content, memory, **kwargs) + + self.pipeline.gate.evaluate = gate + self.module.check_duplicate = dedup + with self.assertRaises(type(failure)) as raised: + self.run_route(route) + self.assertIs(raised.exception, failure) + self.assertEqual(self.invalidations, [[("synthetic committed",)]]) + self.assertIsNone(self.cache.get_recall_cache(str(self.path), "synthetic-user")) + + def test_cache_failure_is_diagnostic_and_does_not_replay_success(self) -> None: + for route in ("text", "transcript"): + with self.subTest(route=route): + self.reset() + self.set_facts("synthetic committed") + self.cache_error = OSError("synthetic cache unavailable") + with self.assertLogs("synthetic.p02", level="WARNING") as captured: + result = self.run_route(route) + self.assertEqual(result.facts_stored, 1) + self.assertEqual(self.conn.execute("SELECT count(*) FROM messages").fetchone(), (1,)) + self.assertEqual(len(self.invalidations), 1) + self.assertEqual(len(captured.output), 1) + self.assertIn("Recall cache invalidation failed", captured.output[0]) + self.assertNotIn("synthetic cache unavailable", captured.output[0]) + self.cache_error = None + + def test_cache_failure_cannot_replace_later_ingestion_exception(self) -> None: + for route in ("text", "transcript"): + with self.subTest(route=route): + self.reset() + self.set_facts("synthetic committed", "synthetic interrupted") + original = RuntimeError("synthetic original failure") + self.cache_error = OSError("synthetic cache unavailable") + + def gate(content: str, category: str) -> object: + if content == "synthetic interrupted": + raise original + return self.gate(content, category) + + self.pipeline.gate.evaluate = gate + with self.assertLogs("synthetic.p02", level="WARNING"): + with self.assertRaises(RuntimeError) as raised: + self.run_route(route) + self.assertIs(raised.exception, original) + self.assertEqual(self.conn.execute("SELECT count(*) FROM messages").fetchone(), (1,)) + self.assertEqual(len(self.invalidations), 1) + self.cache_error = None + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-installer-model-readiness-767.py b/tests/test-installer-model-readiness-767.py new file mode 100644 index 00000000..d207f50e --- /dev/null +++ b/tests/test-installer-model-readiness-767.py @@ -0,0 +1,459 @@ +"""Synthetic installer/setup outcome checks. No model or package downloads.""" +from __future__ import annotations + +import argparse +import ast +import builtins +import contextlib +import io +import itertools +import os +import runpy +from pathlib import Path +import shutil +import subprocess +import sys +import tempfile +import threading +import types +import unittest +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] +POWERSHELL = shutil.which("pwsh") or shutil.which("powershell") +LABELS = ("Edge reranker", "Base/Pro embedder", "Base/Pro reranker") + +FAKE_UV = """#!/bin/sh +case "$*" in + --version) printf 'synthetic uv\\n' ;; + 'tool dir') printf '%s\\n' "$SYNTHETIC_TOOL_DIR" ;; + 'python install '*|'tool uninstall '*|'tool install '*|'tool update-shell') ;; + *) exit 99 ;; +esac +""" + +FAKE_TOOL_PYTHON = """#!/bin/sh +case "$*" in + *ms-marco-MiniLM-L-6-v2*) printf 'synthetic-model-call edge-reranker\\n'; exit "$SYNTHETIC_EDGE_RERANKER" ;; + *Qwen/Qwen3-Embedding-0.6B*) printf 'synthetic-model-call base-embedder\\n'; exit "$SYNTHETIC_BASE_EMBEDDER" ;; + *Alibaba-NLP/gte-reranker-modernbert-base*) printf 'synthetic-model-call base-reranker\\n'; exit "$SYNTHETIC_BASE_RERANKER" ;; + *importlib.metadata*) printf '0.0.0-synthetic\\n' ;; + '-m truememory.mcp_server --setup') exit "$SYNTHETIC_SETUP_EXIT" ;; + '-m truememory.ingest.cli install') exit "$SYNTHETIC_HOOK_EXIT" ;; + *) exit 99 ;; +esac +""" + +# Functions shadow external tools in this child PowerShell process only. +# The actual installer body still controls statuses, summaries, and branches. +POWERSHELL_WRAPPER = r""" +[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false) +$OutputEncoding = [Console]::OutputEncoding +function Get-ExecutionPolicy { param($Scope) return 'Bypass' } +function uv { + $global:LASTEXITCODE = 0 + if (($args -join ' ') -eq '--version') { 'synthetic uv' } + elseif (($args -join ' ') -eq 'tool dir') { $env:SYNTHETIC_TOOL_DIR } +} +function Join-Path { + param($Path, $ChildPath) + if ($ChildPath -eq 'truememory\Scripts\python.exe') { return 'Invoke-SyntheticPython' } + Microsoft.PowerShell.Management\Join-Path -Path $Path -ChildPath $ChildPath +} +function Test-Path { + param($Path, $LiteralPath) + $candidate = if ($LiteralPath) { $LiteralPath } else { $Path } + if ($candidate -eq 'Invoke-SyntheticPython') { return $env:SYNTHETIC_MISSING_PYTHON -ne '1' } + Microsoft.PowerShell.Management\Test-Path -LiteralPath $candidate +} +function Invoke-SyntheticPython { + $command = $args -join ' ' + $global:LASTEXITCODE = 0 + if ($command -match 'ms-marco-MiniLM-L-6-v2') { + 'synthetic-model-call edge-reranker' + if ($env:SYNTHETIC_MODEL_THROW -eq '1') { throw 'synthetic launch failure' } + $global:LASTEXITCODE = [int]$env:SYNTHETIC_EDGE_RERANKER + } elseif ($command -match 'Qwen/Qwen3-Embedding-0.6B') { + 'synthetic-model-call base-embedder' + $global:LASTEXITCODE = [int]$env:SYNTHETIC_BASE_EMBEDDER + } elseif ($command -match 'Alibaba-NLP/gte-reranker-modernbert-base') { + 'synthetic-model-call base-reranker' + $global:LASTEXITCODE = [int]$env:SYNTHETIC_BASE_RERANKER + } elseif ($command -match 'importlib.metadata') { '0.0.0-synthetic' } + elseif ($command -eq '-m truememory.mcp_server --setup') { $global:LASTEXITCODE = [int]$env:SYNTHETIC_SETUP_EXIT } + elseif ($command -eq '-m truememory.ingest.cli install') { $global:LASTEXITCODE = [int]$env:SYNTHETIC_HOOK_EXIT } + else { throw 'unexpected synthetic command' } +} +& $env:SYNTHETIC_INSTALLER +""" + + +class InstallerFixture(unittest.TestCase): + def setUp(self) -> None: + self.temporary = tempfile.TemporaryDirectory(prefix="truememory-installer-767-") + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) + self.bin_dir = self.root / "bin" + self.bin_dir.mkdir() + self.home = self.root / "home" + self.home.mkdir() + self.tool_dir = self.root / "tools" + self.tool_python = self.tool_dir / "truememory" / "bin" / "python" + self.tool_python.parent.mkdir(parents=True) + self._executable(self.bin_dir / "uv", FAKE_UV) + self._executable(self.bin_dir / "curl", "#!/bin/sh\nexit 99\n") + self._executable(self.tool_python, FAKE_TOOL_PYTHON) + + @staticmethod + def _executable(path: Path, text: str) -> None: + path.write_text(text, encoding="utf-8") + path.chmod(0o700) + + def environment(self, outcomes: tuple[bool, bool, bool], **extra: str) -> dict[str, str]: + result = { + "PATH": str(self.bin_dir) + os.pathsep + os.defpath, + "HOME": str(self.home), + "USERPROFILE": str(self.home), + "LOCALAPPDATA": str(self.home), + "SYNTHETIC_TOOL_DIR": str(self.tool_dir), + "SYNTHETIC_EDGE_RERANKER": "0" if outcomes[0] else "1", + "SYNTHETIC_BASE_EMBEDDER": "0" if outcomes[1] else "1", + "SYNTHETIC_BASE_RERANKER": "0" if outcomes[2] else "1", + "SYNTHETIC_SETUP_EXIT": "0", + "SYNTHETIC_HOOK_EXIT": "0", + } + # Windows needs its system directory for native process startup. + for key in ("SYSTEMROOT", "WINDIR", "COMSPEC"): + if key in os.environ: + result[key] = os.environ[key] + result.update(extra) + return result + + def assert_summary(self, completed: subprocess.CompletedProcess[str], outcomes: tuple[bool, bool, bool]) -> str: + output = completed.stdout + completed.stderr + self.assertEqual(completed.returncode, 0, output) + self.assertIn("package installed.", output) + self.assertIn(f"Model pre-download checks: {sum(outcomes)}/3 succeeded.", output) + for label, ready in zip(LABELS, outcomes): + self.assertIn(f"{label}: {'ready' if ready else 'failed'}", output) + self.assertEqual("All three requested model pre-download checks passed." in output, all(outcomes)) + self.assertEqual("Model readiness incomplete" in output, not all(outcomes)) + self.assertEqual(output.count("Retry this model only:"), outcomes.count(False)) + self.assertEqual(output.count("synthetic-model-call"), 3) + self.assertNotIn("tier switching is instant", output) + self.assertNotIn("all models pre-downloaded", output) + self.assertIn("re-embedding stored memories", output) + self.assertIn("Edge embedder was not checked", output) + return output + + +@unittest.skipIf(os.name == "nt", "POSIX installer runs on Mac/Linux") +class TestShellInstallerReadiness(InstallerFixture): + def run_installer(self, outcomes: tuple[bool, bool, bool], **extra: str) -> subprocess.CompletedProcess[str]: + return subprocess.run( + ["/bin/sh", str(ROOT / "install.sh")], + env=self.environment(outcomes, **extra), cwd=self.root, + capture_output=True, text=True, encoding="utf-8", timeout=30, + ) + + def test_all_model_outcome_combinations(self) -> None: + for outcomes in itertools.product((False, True), repeat=3): + with self.subTest(outcomes=outcomes): + self.assert_summary(self.run_installer(outcomes), outcomes) + + def test_skipped_setup_does_not_claim_registration_or_hooks(self) -> None: + output = self.assert_summary(self.run_installer((True, True, True), TRUEMEMORY_SKIP_SETUP="1"), (True, True, True)) + self.assertIn("Claude registration: skipped", output) + self.assertIn("Hooks: skipped", output) + + def test_setup_failures_remain_visible_when_models_succeed(self) -> None: + output = self.assert_summary(self.run_installer((True, True, True), SYNTHETIC_SETUP_EXIT="1", SYNTHETIC_HOOK_EXIT="1"), (True, True, True)) + self.assertIn("Claude registration: failed", output) + self.assertIn("Hooks: failed", output) + + def test_missing_tool_python_is_unverified(self) -> None: + completed = self.run_installer((True, True, True), SYNTHETIC_TOOL_DIR=str(self.root / "missing-tools")) + output = completed.stdout + completed.stderr + self.assertEqual(completed.returncode, 0, output) + self.assertIn("Model pre-download checks: 0/3 succeeded.", output) + self.assertIn("Model readiness incomplete", output) + self.assertIn("uv tool dir", output) + self.assertIn("Claude registration: skipped", output) + self.assertIn("Hooks: skipped", output) + self.assertNotIn("synthetic-model-call", output) + for label in LABELS: + self.assertIn(f"{label}: not checked", output) + + +@unittest.skipUnless(POWERSHELL, "Requires PowerShell; exercised by Windows CI") +class TestPowerShellInstallerReadiness(InstallerFixture): + def run_installer(self, outcomes: tuple[bool, bool, bool], **extra: str) -> subprocess.CompletedProcess[str]: + wrapper = self.root / "synthetic-installer-wrapper.ps1" + wrapper.write_text(POWERSHELL_WRAPPER, encoding="utf-8") + return subprocess.run( + [str(POWERSHELL), "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", str(wrapper)], + env=self.environment(outcomes, SYNTHETIC_INSTALLER=str(ROOT / "install.ps1"), **extra), + cwd=self.root, capture_output=True, text=True, encoding="utf-8", timeout=30, + ) + + def test_all_model_outcome_combinations(self) -> None: + for outcomes in itertools.product((False, True), repeat=3): + with self.subTest(outcomes=outcomes): + self.assert_summary(self.run_installer(outcomes), outcomes) + + def test_skipped_setup_does_not_claim_registration_or_hooks(self) -> None: + output = self.assert_summary(self.run_installer((True, True, True), TRUEMEMORY_SKIP_SETUP="1"), (True, True, True)) + self.assertIn("Claude registration: skipped", output) + self.assertIn("Hooks: skipped", output) + + def test_setup_failures_remain_visible_when_models_succeed(self) -> None: + output = self.assert_summary(self.run_installer((True, True, True), SYNTHETIC_SETUP_EXIT="1", SYNTHETIC_HOOK_EXIT="1"), (True, True, True)) + self.assertIn("Claude registration: failed", output) + self.assertIn("Hooks: failed", output) + + def test_missing_tool_python_is_unverified(self) -> None: + completed = self.run_installer((True, True, True), SYNTHETIC_MISSING_PYTHON="1") + output = completed.stdout + completed.stderr + self.assertEqual(completed.returncode, 0, output) + self.assertIn("Model pre-download checks: 0/3 succeeded.", output) + self.assertIn("Model readiness incomplete", output) + self.assertIn("Claude registration: skipped", output) + self.assertIn("Hooks: skipped", output) + self.assertNotIn("synthetic-model-call", output) + for label in LABELS: + self.assertIn(f"{label}: not checked", output) + + def test_model_launch_exception_is_a_failed_check(self) -> None: + self.assert_summary(self.run_installer((False, True, True), SYNTHETIC_MODEL_THROW="1"), (False, True, True)) + + +class TestSelectedTierSetupReadiness(unittest.TestCase): + def run_setup( + self, tier: str, failures: set[str], fail_on_load: bool = False, + *, cli: str = "", integration_outcomes: dict[str, bool] | None = None, + manager_action: str = "noop", + ) -> tuple[int, str, str, list[dict], list[str]]: + # Execute both real setup functions while replacing filesystem, adapter + # installation, and model boundaries. Package __init__ is never imported. + source = ast.parse((ROOT / "truememory" / "ingest" / "cli.py").read_text(encoding="utf-8")) + functions = [node for node in source.body if isinstance(node, ast.FunctionDef) + and node.name in ("_setup_cli_integrations", "_setup_model_readiness", "_run_setup")] + program = ast.fix_missing_locations(ast.Module(body=functions, type_ignores=[])) + saved: list[dict] = [] + calls: list[str] = [] + config_path = Path("synthetic-setup/config.json") + config_state = {"tier": tier, "user_id": "synthetic-user"} + + class SyntheticModel: + def encode(self, texts: list[str], **_kwargs: object) -> list[list[float]]: + calls.append("encode") + self_check.assertEqual(texts, ["TrueMemory setup readiness check."]) + if "embed" in failures: + raise RuntimeError("synthetic embedding failure") + return [[1.0, 0.0]] + + def predict(self, pairs: list[tuple[str, str]], **_kwargs: object) -> list[float]: + calls.append("predict") + self_check.assertEqual(pairs, [("TrueMemory setup readiness check.", "TrueMemory setup readiness check.")]) + if "rerank" in failures: + raise RuntimeError("synthetic reranker failure") + return [0.5] + + self_check = self + + def load(kind: str) -> SyntheticModel: + calls.append("load-" + kind) + if fail_on_load and kind in failures: + raise OSError("synthetic model download failure") + return SyntheticModel() + + package = types.ModuleType("truememory") + package.__path__ = [] + vectors = types.ModuleType("truememory.vector_search") + vectors.set_embedding_model = lambda value: calls.append("embed-tier-" + value) + vectors.get_model = lambda: load("embed") + vectors._encode_with_mps_fallback = lambda model, texts, **kwargs: model.encode(texts, **kwargs) + reranker = types.ModuleType("truememory.reranker") + reranker.set_active_tier = lambda value: calls.append("rerank-tier-" + value) + reranker.get_reranker = lambda: load("rerank") + hooks = types.ModuleType("truememory.hooks") + hooks.__path__ = [] + registry = types.ModuleType("truememory.hooks.registry") + adapters = { + cli_id: types.SimpleNamespace(cli_id=cli_id, name=name, is_configured=lambda: False) + for cli_id, name in (("claude", "Claude Code"), ("codex", "Codex")) + } + registry.get_adapter = adapters.get + registry.detect_installed = lambda: list(adapters.values()) + hooks_cli = types.ModuleType("truememory.hooks.cli") + + def install_cli(cli_id: str, user_id: str) -> bool: + calls.append("install-cli-" + cli_id) + self.assertEqual(user_id, "synthetic-user") + return (integration_outcomes or {}).get(cli_id, True) + + hooks_cli.install_cli = install_cli + namespace = { + "argparse": argparse, "os": os, "sys": sys, "_SAURON_BANNER": "synthetic banner", + "_load_truememory_config": lambda: config_state.copy(), + "_save_truememory_config": lambda config: saved.append(dict(config)), + "_TRUEMEMORY_CONFIG_PATH": config_path, + } + bridge = runpy.run_path(str(ROOT / "tests/test-tier-runtime-bridge-795.py")) + fixture = bridge["TestRuntimeBridge"]() + fixture.setUp() + fixture.vector.resolve_tier = lambda: tier + fixture.vector.get_model = vectors.get_model + fixture.vector._encode_with_mps_fallback = vectors._encode_with_mps_fallback + fixture.reranker.get_reranker = reranker.get_reranker + primitives = runpy.run_path(str(ROOT / "tests/test-maintenance-source-revision-753.py")) + maintenance = primitives["MAINTENANCE"] + + class SyntheticManager: + _last_action = manager_action + + def run_rebuild_sync(self, requested_tier: str) -> bool: + self_check.assertEqual(requested_tier, tier) + vectors.set_embedding_model(requested_tier) + reranker.set_active_tier(requested_tier) + return True + + modules = { + "truememory.vector_search": fixture.vector, "truememory.reranker": fixture.reranker, + "truememory.maintenance": maintenance, + "truememory.tier_switch.runtime": fixture.api, + "truememory.tier_switch.manager": types.SimpleNamespace( + RebuildManager=SyntheticManager, _get_db_path=lambda: None, _open_db=lambda **kwargs: fixture.conn), + "truememory.hooks.registry": registry, "truememory.hooks.cli": hooks_cli, + } + + def synthetic_import(name, globals=None, locals=None, fromlist=(), level=0): + if name in modules: + return modules[name] + if name.startswith("truememory") or name in {"torch", "numpy", "sqlite_vec", "sentence_transformers"}: + raise AssertionError("Unexpected setup dependency: " + name) + return builtins.__import__(name, globals, locals, fromlist, level) + + namespace["__builtins__"] = dict(vars(builtins), __import__=synthetic_import) + projection = types.ModuleType("synthetic_setup_projection") + projection.__dict__["__builtins__"] = namespace["__builtins__"] + projection_path = ROOT / "truememory/tier_switch/projection.py" + exec(compile(projection_path.read_text(encoding="utf-8"), str(projection_path), "exec"), projection.__dict__) + config_file_lock = threading.Lock() + + @contextlib.contextmanager + def locked_config(path: Path, *, strict: bool = False): + self_check.assertEqual(path, config_path.with_name("config.json.lock")) + self_check.assertTrue(strict) + with config_file_lock: + yield + + def read_config(path: Path) -> dict: + self_check.assertEqual(path, config_path) + self_check.assertTrue(config_file_lock.locked()) + self_check.assertTrue(projection.CONFIG_WRITE_LOCK.locked()) + return config_state.copy() + + def write_config(path: Path, config: dict) -> None: + self_check.assertEqual(path, config_path) + self_check.assertTrue(config_file_lock.locked()) + self_check.assertTrue(projection.CONFIG_WRITE_LOCK.locked()) + config_state.clear() + config_state.update(config) + saved.append(config_state.copy()) + + projection.ConfigFileLock = locked_config + projection._read_config = read_config + projection._write_config = write_config + modules["truememory.tier_switch.projection"] = projection + exec(compile(program, "synthetic-cli-setup", "exec"), namespace) + stdout, stderr = io.StringIO(), io.StringIO() + exit_code = 0 + try: + with patch.dict(os.environ, {}, clear=True), contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): + try: + namespace["_run_setup"](argparse.Namespace(non_interactive=True, cli=cli)) + except SystemExit as exc: + exit_code = int(exc.code) + finally: + fixture.doCleanups() + return exit_code, stdout.getvalue(), stderr.getvalue(), saved, calls + + def test_selected_models_fail_truthfully_and_retain_configuration(self) -> None: + for fail_on_load in (False, True): + for failures in ({"embed"}, {"rerank"}, {"embed", "rerank"}): + with self.subTest(failures=failures, fail_on_load=fail_on_load): + code, output, error, saved, calls = self.run_setup("base", failures, fail_on_load) + self.assertEqual(code, 1) + self.assertIn("model readiness is incomplete", output) + self.assertIn("truememory-ingest setup --non-interactive", output) + self.assertNotIn("Setup complete!", output) + self.assertIn("readiness check failed", error) + self.assertEqual(saved, [{"tier": "base", "user_id": "synthetic-user"}]) + self.assertIn("install-cli-claude", calls) + self.assertIn("install-cli-codex", calls) + if "embed" in failures: + self.assertNotIn("Matryoshka ready", output) + if "rerank" in failures: + self.assertNotIn("Cross-encoder reranker ready", output) + + def test_successful_setup_checks_and_preserves_each_selected_tier(self) -> None: + for tier in ("edge", "base", "pro"): + with self.subTest(tier=tier): + code, output, error, saved, calls = self.run_setup(tier, set()) + self.assertEqual(code, 0) + self.assertIn("Setup complete!", output) + self.assertEqual(error, "") + self.assertEqual(saved[0]["tier"], tier) + self.assertIn("embed-tier-" + tier, calls) + self.assertIn("rerank-tier-" + tier, calls) + self.assertEqual(calls.count("encode"), 1) + self.assertEqual(calls.count("predict"), 1) + + def test_requested_integration_outcomes_control_setup_completion(self) -> None: + for cli in ("", "claude,codex"): + for outcomes in itertools.product((False, True), repeat=2): + with self.subTest(cli=cli or "detected", outcomes=outcomes): + results = dict(zip(("claude", "codex"), outcomes)) + code, output, error, saved, calls = self.run_setup( + "base", set(), cli=cli, integration_outcomes=results, + ) + self.assertEqual(code, 0 if all(outcomes) else 1) + self.assertEqual("Setup complete!" in output, all(outcomes)) + self.assertEqual(saved, [{"tier": "base", "user_id": "synthetic-user"}]) + self.assertEqual(error, "") + self.assertIn("Matryoshka ready", output) + self.assertIn("Cross-encoder reranker ready", output) + for cli_id in results: + self.assertEqual(calls.count("install-cli-" + cli_id), 1) + if not all(outcomes): + self.assertIn("Failed CLI integrations:", output) + self.assertIn("truememory-ingest setup --non-interactive --cli", output) + self.assertNotIn("TrueMemory is ready", output) + + def test_unknown_requested_integration_prevents_setup_completion(self) -> None: + code, output, _error, saved, calls = self.run_setup("edge", set(), cli="claude,unknown") + self.assertEqual(code, 1) + self.assertIn("Unknown CLI: unknown", output) + self.assertIn("Failed CLI integrations: unknown", output) + self.assertNotIn("Setup complete!", output) + self.assertEqual(saved[0]["tier"], "edge") + self.assertIn("install-cli-claude", calls) + self.assertNotIn("install-cli-unknown", calls) + + def test_config_only_setup_reports_no_unperformed_model_readiness(self) -> None: + code, output, error, saved, calls = self.run_setup("pro", set(), manager_action="config_only") + self.assertEqual(code, 0) + self.assertEqual(error, "") + self.assertEqual(saved, [{"tier": "pro", "user_id": "synthetic-user"}]) + self.assertNotIn("encode", calls) + self.assertNotIn("predict", calls) + self.assertNotIn("load-embed", calls) + self.assertNotIn("load-rerank", calls) + self.assertNotIn("Matryoshka ready", output) + self.assertNotIn("Cross-encoder reranker ready", output) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-kimi-toml-serialization-833.py b/tests/test-kimi-toml-serialization-833.py new file mode 100644 index 00000000..c5cfd0e1 --- /dev/null +++ b/tests/test-kimi-toml-serialization-833.py @@ -0,0 +1,366 @@ +"""Synthetic Kimi command/TOML roundtrips using actual adapter and writer AST.""" +from __future__ import annotations + +import ast +import __future__ +import builtins +import json +import logging +import shlex +import subprocess +import unittest +from contextlib import contextmanager +from dataclasses import dataclass, field +from pathlib import Path, PurePath, PurePosixPath, PureWindowsPath +from types import SimpleNamespace +from typing import Iterator, Protocol, cast +from unittest.mock import patch + +try: + import tomllib +except ModuleNotFoundError: + tomllib = None + +ROOT = Path(__file__).resolve().parents[1] +KIMI_SOURCE = ROOT / "truememory/hooks/adapters/kimi.py" +BASE_SOURCE = ROOT / "truememory/hooks/adapters/base.py" +EVENTS = { + "SessionStart": ("session_start.py", 10), + "Stop": ("stop.py", 5), + "UserPromptSubmit": ("user_prompt_submit.py", 5), + "PreCompact": ("compact.py", 5), +} +FOREIGN_TOML = ( + '# Synthetic foreign settings\ntheme = "plain"\n\n' + '[[hooks]]\nevent = "SessionStart"\ncommand = "printf foreign-hook"\n' + 'timeout = 37\n\n[features]\ntelemetry = false\n' +) +FOREIGN_MCP = {"mcpServers": {"foreign": {"command": "synthetic-foreign"}}, "theme": "plain"} + + +@dataclass +class MemoryFS: + platform: str + files: dict[str, str] = field(default_factory=dict) + directories: set[str] = field(default_factory=set) + replace_calls: int = 0 + fail_next_replace: bool = False + + def path(self, value: str | MemoryPath) -> MemoryPath: + if isinstance(value, MemoryPath): + if value.fs is not self: + raise AssertionError("Path belongs to another synthetic filesystem") + return value + pure = PureWindowsPath(value) if self.platform == "win32" else PurePosixPath(value) + return MemoryPath(self, pure) + + def replace(self, source: MemoryPath, destination: MemoryPath) -> None: + self.replace_calls += 1 + if self.fail_next_replace: + self.fail_next_replace = False + raise OSError("Synthetic atomic replace failure") + self.files[str(destination)] = self.files.pop(str(source)) + + +@dataclass(frozen=True) +class MemoryPath: + fs: MemoryFS + pure: PurePath + + def __str__(self) -> str: + return str(self.pure) + + def __truediv__(self, component: str) -> MemoryPath: + return MemoryPath(self.fs, self.pure / component) + + @property + def parent(self) -> MemoryPath: + return MemoryPath(self.fs, self.pure.parent) + + @property + def name(self) -> str: + return self.pure.name + + def with_name(self, name: str) -> MemoryPath: + return MemoryPath(self.fs, self.pure.with_name(name)) + + def mkdir(self, *, parents: bool = False, exist_ok: bool = False) -> None: + if not parents or not exist_ok: + raise AssertionError("Unexpected synthetic mkdir arguments") + self.fs.directories.add(str(self)) + + def exists(self) -> bool: + return str(self) in self.fs.files or str(self) in self.fs.directories + + def is_dir(self) -> bool: + return str(self) in self.fs.directories + + def read_text(self, *, encoding: str) -> str: + if encoding != "utf-8": + raise AssertionError("Unexpected encoding") + try: + return self.fs.files[str(self)] + except KeyError as error: + raise FileNotFoundError(str(self)) from error + + def write_text(self, text: str, *, encoding: str) -> int: + if encoding != "utf-8": + raise AssertionError("Unexpected encoding") + text.encode(encoding) + self.fs.files[str(self)] = text + return len(text) + + def unlink(self) -> None: + try: + del self.fs.files[str(self)] + except KeyError as error: + raise FileNotFoundError(str(self)) from error + + +@dataclass +class MemoryPathFactory: + fs: MemoryFS + + def __call__(self, value: str | MemoryPath) -> MemoryPath: + return self.fs.path(value) + + def home(self) -> MemoryPath: + home = r"C:\synthetic-home" if self.fs.platform == "win32" else "/synthetic-home" + return self.fs.path(home) + + +class HookAdapter(Protocol): + def install_hooks(self, python_path: str, user_id: str = "", db_path: str = "") -> None: ... + def install_mcp(self, python_path: str) -> None: ... + def verify(self) -> bool: ... + def uninstall(self) -> None: ... + def _parse_existing_hooks(self, text: str) -> list[dict]: ... + + +@dataclass +class Fixture: + fs: MemoryFS + adapter: HookAdapter + hook_config: MemoryPath + mcp_config: MemoryPath + hooks_dir: MemoryPath + + def text(self) -> str: + return self.hook_config.read_text(encoding="utf-8") + + +def load_fixture(platform: str) -> Fixture: + """Execute source AST with memory-only paths and the actual atomic writer.""" + fs = MemoryFS(platform) + path_factory = MemoryPathFactory(fs) + source_file = ( + r'C:\synthetic modules "quoted"\truememory\hooks\adapters\kimi.py' + if platform == "win32" + else '/synthetic modules "quoted"/truememory/hooks/adapters/kimi.py' + ) + namespace: dict[str, object] = { + "__name__": "synthetic_kimi_adapter", + "__file__": source_file, + "Path": path_factory, + "CLIAdapter": object, + "json": json, + "logging": logging, + "shlex": shlex, + "shutil": SimpleNamespace(which=lambda executable: None), + "sys": SimpleNamespace(platform=platform, executable="synthetic-python"), + "os": SimpleNamespace(getpid=lambda: 833, replace=fs.replace), + } + base_tree = ast.parse(BASE_SOURCE.read_text(encoding="utf-8")) + writer = next( + node for node in base_tree.body + if isinstance(node, ast.FunctionDef) and node.name == "atomic_write_text" + ) + writer_module = ast.Module(body=[writer], type_ignores=[]) + exec(compile(writer_module, str(BASE_SOURCE), "exec", flags=__future__.annotations.compiler_flag), namespace) + adapter_tree = ast.parse(KIMI_SOURCE.read_text(encoding="utf-8")) + adapter_tree.body = [ + node for node in adapter_tree.body + if not isinstance(node, (ast.Import, ast.ImportFrom)) + ] + exec(compile(adapter_tree, str(KIMI_SOURCE), "exec", flags=__future__.annotations.compiler_flag), namespace) + adapter_type = namespace["KimiAdapter"] + if not isinstance(adapter_type, type): + raise AssertionError("Actual Kimi adapter class was not loaded") + hook_config = cast(MemoryPath, namespace["_HOOK_CONFIG"]) + mcp_config = cast(MemoryPath, namespace["_MCP_CONFIG"]) + fs.files[str(hook_config)] = FOREIGN_TOML + fs.files[str(mcp_config)] = json.dumps(FOREIGN_MCP) + hooks_dir = fs.path(source_file).parent.parent.parent / "ingest" / "hooks" + return Fixture(fs, cast(HookAdapter, adapter_type()), hook_config, mcp_config, hooks_dir) + + +@contextmanager +def scoped_imports(*, no_tomllib: bool) -> Iterator[None]: + original_import = builtins.__import__ + + def checked_import( + name: str, globals: dict | None = None, locals: dict | None = None, + fromlist: tuple | list = (), level: int = 0, + ) -> object: + if name == "tomllib" and no_tomllib: + raise ModuleNotFoundError("Synthetic Python 3.10 tomllib absence") + if name == "truememory" or name.startswith("truememory."): + raise AssertionError("Actual application import is forbidden in this fixture") + return original_import(name, globals, locals, fromlist, level) + + with patch("builtins.__import__", side_effect=checked_import): + yield + + +def expected_commands(fixture: Fixture, python: str, user: str, db: str) -> list[str]: + commands: list[str] = [] + for script, _ in EVENTS.values(): + parts = [python, str(fixture.hooks_dir / script)] + if user: + parts.extend(["--user", user]) + if db: + parts.extend(["--db", db]) + commands.append( + subprocess.list2cmdline(parts) if fixture.fs.platform == "win32" + else " ".join(shlex.quote(part) for part in parts) + ) + return commands + + +def strict_generated_commands(text: str) -> list[str]: + """Validate emitted JSON-compatible TOML scalars without a TOML dependency.""" + commands: list[str] = [] + for line in text.split("\n"): + if not line.startswith("command = "): + continue + scalar = line[len("command = "):] + if any(ord(char) < 0x20 or ord(char) == 0x7F for char in scalar): + raise AssertionError("Raw TOML-forbidden control character") + decoded = json.loads(scalar) + if not isinstance(decoded, str): + raise AssertionError("Generated command must be a string") + if any(0xD800 <= ord(char) <= 0xDFFF for char in decoded): + raise AssertionError("Surrogate is not a TOML Unicode scalar") + commands.append(decoded) + return commands + + +class TestKimiTomlSerialization(unittest.TestCase): + def assert_roundtrip( + self, fixture: Fixture, python: str, user: str, db: str, *, fallback: bool, + ) -> None: + expected = expected_commands(fixture, python, user, db) + with scoped_imports(no_tomllib=fallback): + fixture.adapter.install_hooks(python, user, db) + hooks = fixture.adapter._parse_existing_hooks(fixture.text()) + installed = fixture.text() + replacements = fixture.fs.replace_calls + fixture.adapter.install_hooks(python, user, db) + self.assertEqual(fixture.text(), installed) + self.assertEqual(fixture.fs.replace_calls, replacements) + self.assertEqual([hook["command"] for hook in hooks[1:]], expected) + self.assertEqual([hook["event"] for hook in hooks[1:]], list(EVENTS)) + self.assertEqual([int(hook["timeout"]) for hook in hooks[1:]], [10, 5, 5, 5]) + self.assertEqual(strict_generated_commands(fixture.text())[1:], expected) + self.assertTrue(fixture.text().startswith(FOREIGN_TOML.rstrip() + "\n")) + if tomllib is not None: + parsed = tomllib.loads(fixture.text()) + self.assertEqual([hook["command"] for hook in parsed["hooks"][1:]], expected) + self.assertEqual([hook["timeout"] for hook in parsed["hooks"][1:]], [10, 5, 5, 5]) + self.assertEqual(parsed["features"], {"telemetry": False}) + + def test_native_and_fallback_commands_preserve_platform_quoting(self) -> None: + for platform in ("linux", "win32"): + for fallback in (False, True): + with self.subTest(platform=platform, fallback=fallback): + fixture = load_fixture(platform) + python = r'C:\Synthetic Python "quoted"\python.exe' if platform == "win32" else '/synthetic Python "quoted"/python' + self.assert_roundtrip( + fixture, python, 'synthetic user "quoted" \\ trailing\\', + r'C:\synthetic db "quoted"\store.db' if platform == "win32" else '/synthetic db "quoted"/store.db', + fallback=fallback, + ) + + def test_all_control_and_unicode_characters_roundtrip(self) -> None: + characters = [chr(codepoint) for codepoint in range(32)] + [ + chr(0x7F), chr(0x85), chr(0x2028), chr(0x2029), chr(0xE9), chr(0x4E2D), chr(0x20000), + '"', "'", "\\", "#", + ] + for platform in ("linux", "win32"): + for fallback in (False, True): + for character in characters: + with self.subTest(platform=platform, fallback=fallback, codepoint=ord(character)): + fixture = load_fixture(platform) + self.assert_roundtrip( + fixture, "synthetic-python", "synthetic" + character + "user", + "synthetic" + character + "db", fallback=fallback, + ) + + def test_reinstall_verify_uninstall_preserve_foreign_configuration(self) -> None: + for platform in ("linux", "win32"): + for fallback in (False, True): + with self.subTest(platform=platform, fallback=fallback): + fixture = load_fixture(platform) + with scoped_imports(no_tomllib=fallback): + fixture.adapter.install_mcp("synthetic-python") + fixture.adapter.install_hooks("synthetic-python", 'user "quoted"', "db\\path") + first = fixture.text() + first_mcp = fixture.mcp_config.read_text(encoding="utf-8") + replacements = fixture.fs.replace_calls + fixture.adapter.install_hooks("synthetic-python", 'user "quoted"', "db\\path") + self.assertEqual(fixture.text(), first) + self.assertEqual(fixture.fs.replace_calls, replacements) + self.assertEqual(fixture.mcp_config.read_text(encoding="utf-8"), first_mcp) + self.assertTrue(fixture.adapter.verify()) + self.assertEqual(len(fixture.adapter._parse_existing_hooks(first)), 5) + fixture.adapter.uninstall() + self.assertFalse(fixture.adapter.verify()) + remaining = fixture.adapter._parse_existing_hooks(fixture.text()) + self.assertEqual(len(remaining), 1) + self.assertEqual(remaining[0]["command"], "printf foreign-hook") + self.assertTrue(fixture.text().startswith(FOREIGN_TOML)) + self.assertNotIn("truememory", fixture.text()) + self.assertEqual(json.loads(fixture.mcp_config.read_text(encoding="utf-8")), FOREIGN_MCP) + + def test_atomic_failure_preserves_original_and_retry_installs_once(self) -> None: + for platform in ("linux", "win32"): + for fallback in (False, True): + with self.subTest(platform=platform, fallback=fallback): + fixture = load_fixture(platform) + original_files = dict(fixture.fs.files) + fixture.fs.fail_next_replace = True + with scoped_imports(no_tomllib=fallback): + with self.assertRaisesRegex(OSError, "Synthetic atomic replace failure"): + fixture.adapter.install_hooks("synthetic-python", "synthetic\\user", "synthetic\\db") + self.assertEqual(fixture.fs.files, original_files) + self.assertEqual(fixture.fs.replace_calls, 1) + self.assert_roundtrip(fixture, "synthetic-python", "synthetic\\user", "synthetic\\db", fallback=fallback) + self.assertEqual(fixture.fs.replace_calls, 2) + self.assertFalse(any(name.endswith(".tmp") for name in fixture.fs.files)) + + def test_fallback_rejects_malformed_complete_strings(self) -> None: + malformed = [ + r'"truememory\q"', r'"truememory\u12zz"', '"truememory" trailing', + '"truememory" "second"', '"truememory', + ] + for scalar in malformed: + with self.subTest(scalar=scalar): + fixture = load_fixture("win32") + text = '[[hooks]]\nevent = "SessionStart"\ncommand = ' + scalar + '\ntimeout = 10\n' + with scoped_imports(no_tomllib=True): + self.assertEqual(fixture.adapter._parse_existing_hooks(text), []) + + def test_fallback_allows_complete_string_and_trailing_comment(self) -> None: + fixture = load_fixture("win32") + command = r'"C:\Synthetic Python\python.exe" truememory --user "quoted user"' + text = '[[hooks]]\nevent = "SessionStart"\ncommand = ' + json.dumps(command) + ' # "synthetic comment"\ntimeout = 10\n' + with scoped_imports(no_tomllib=True): + hooks = fixture.adapter._parse_existing_hooks(text) + self.assertEqual(len(hooks), 1) + self.assertEqual(hooks[0]["command"], command) + self.assertEqual(hooks[0]["event"], "SessionStart") + self.assertEqual(hooks[0]["timeout"], "10") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/tests/test-l0-style-accumulator-747.py b/tests/test-l0-style-accumulator-747.py new file mode 100644 index 00000000..0f9d85f6 --- /dev/null +++ b/tests/test-l0-style-accumulator-747.py @@ -0,0 +1,755 @@ +"""A04 raw-sum regressions using stdlib source execution and in-memory SQLite.""" +from __future__ import annotations + +import ast +import builtins +import itertools +import json +import logging +import math +import random +import sqlite3 +import struct +import sys +import threading +import types +import unittest +import uuid +from collections.abc import Iterator +from contextlib import AbstractContextManager, contextmanager, nullcontext +from pathlib import Path +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] + + +def load_stdlib_source(name: str) -> types.ModuleType: + def safe_import(name: str, globals: dict | None = None, locals: dict | None = None, + fromlist: tuple[str, ...] = (), level: int = 0) -> object: + if name.split(".", 1)[0] not in sys.stdlib_module_names: + raise AssertionError("Non-stdlib import forbidden: " + name) + return builtins.__import__(name, globals, locals, fromlist, level) + + module = types.ModuleType("synthetic_style_" + name) + module.__dict__["__builtins__"] = dict(vars(builtins), __import__=safe_import) + source = ROOT / "truememory" / (name + ".py") + with patch.dict(sys.modules, {module.__name__: module}): + exec(compile(source.read_text(encoding="utf-8"), str(source), "exec"), module.__dict__) + return module + + +STYLE = load_stdlib_source("personality_style_vec") +STORAGE = load_stdlib_source("storage") +LEGACY_SCHEMA = """CREATE TABLE entity_style_vectors ( + entity TEXT PRIMARY KEY, vector TEXT, message_count INTEGER DEFAULT 0, updated_at TEXT +)""" +SOURCES = """CREATE TABLE messages ( + id INTEGER PRIMARY KEY, sender TEXT, content TEXT, timestamp TEXT, directive INTEGER +); CREATE TABLE sentinel (value TEXT);""" + +# Unchanged updater body from dependency base 8d7249c. It is executed with +# synthetic vectors to prove the old behavior fails the new numeric contract. +LEGACY_INCREMENTAL = '''def update_entity_style_vector_incremental( + conn: sqlite3.Connection, entity: str, new_message: str, + *, _pre_computed_vec: list[float] | None = None, +) -> None: + """Incrementally update an entity's style vector with a new message. + + Uses a running weighted average: given the existing mean vector and + its message count, the new mean is:: + + new_mean = (existing * count + new_vec) / (count + 1) + + Then re-L2-normalized. + + Args: + conn: Open database connection. + entity: Entity name (sender). + new_message: The new message text. + _pre_computed_vec: If provided, skip compute_style_vector() call. + """ + if not entity or not new_message: + return + + # Normalize entity name to lowercase for case-insensitive matching (#467) + entity = entity.lower() + + conn.execute( + """CREATE TABLE IF NOT EXISTS entity_style_vectors ( + entity TEXT PRIMARY KEY, + vector TEXT, + message_count INTEGER DEFAULT 0, + updated_at TEXT + )""" + ) + + new_vec = _pre_computed_vec if _pre_computed_vec is not None else compute_style_vector(new_message) + now = datetime.now(timezone.utc).isoformat() + + row = conn.execute( + "SELECT vector, message_count FROM entity_style_vectors WHERE entity = ?", + (entity,), + ).fetchone() + + if row is None or row[0] is None: + conn.execute( + """INSERT OR REPLACE INTO entity_style_vectors + (entity, vector, message_count, updated_at) + VALUES (?, ?, ?, ?)""", + (entity, json.dumps(new_vec), 1, now), + ) + else: + existing_vec = json.loads(row[0]) + count = row[1] or 0 + + new_count = count + 1 + merged = [ + (existing_vec[i] * count + new_vec[i]) / new_count + for i in range(len(existing_vec)) + ] + + norm = math.sqrt(sum(x * x for x in merged)) + if norm > 0: + merged = [x / norm for x in merged] + + conn.execute( + """INSERT OR REPLACE INTO entity_style_vectors + (entity, vector, message_count, updated_at) + VALUES (?, ?, ?, ?)""", + (entity, json.dumps(merged), new_count, now), + ) +''' + + +def axis(index: int) -> list[float]: + return [float(position == index) for position in range(256)] + + +def reference_mean(vectors: list[list[float]]) -> list[float]: + if not vectors: + return [0.0] * 256 + total = [0.0] * len(vectors[0]) + for vector in vectors: + for index in range(len(total)): + total[index] += vector[index] + mean = [value / len(vectors) for value in total] + norm = math.sqrt(sum(value * value for value in mean)) + return [value / norm for value in mean] if norm > 0 else mean + + +def allow_authorized_action(*_args: object) -> int: + return sqlite3.SQLITE_OK + + +class StatementFailure: + """Deny a single execution boundary, independent of SQLite's statement cache.""" + + def __init__(self, conn: sqlite3.Connection, sql: str, denied_action: tuple[int, str, str | None], + *, occurrence: int = 1) -> None: + self.conn = conn + self.sql = sql + self.denied_action = denied_action + self.occurrence = occurrence + self.matches = 0 + self.armed: list[str] = [] + + def __getattr__(self, name: str) -> object: + return getattr(self.conn, name) + + def authorize(self, action: int, first: str | None, second: str | None, *_rest: object) -> int: + return sqlite3.SQLITE_DENY if (action, first, second) == self.denied_action else sqlite3.SQLITE_OK + + def execute(self, sql: str, *args: object) -> sqlite3.Cursor: + if sql == self.sql: + self.matches += 1 + if self.matches == self.occurrence: + self.armed.append(sql) + # Install outside the pure callback so cached statements must + # reauthorize; reset before the production rollback/RELEASE. + self.conn.set_authorizer(self.authorize) + try: + return self.conn.execute(sql, *args) + finally: + self.conn.set_authorizer(allow_authorized_action) + return self.conn.execute(sql, *args) + + +class StyleAccumulatorTests(unittest.TestCase): + def connection(self, *, legacy: bool = False, full: bool = False, cached_statements: int = 100) -> sqlite3.Connection: + conn = sqlite3.connect(":memory:", timeout=0, cached_statements=cached_statements) + self.addCleanup(conn.close) + if legacy: + conn.execute(LEGACY_SCHEMA) + conn.executescript(STORAGE._SCHEMA_SQL if full else SOURCES) + if full: + conn.execute("CREATE TABLE sentinel (value TEXT)") + STORAGE._initialize_maintenance_tracking(conn) + conn.commit() + return conn + + def columns(self, conn: sqlite3.Connection) -> list[tuple]: + return [tuple(row) for row in conn.execute("PRAGMA table_info(entity_style_vectors)")] + + def row(self, conn: sqlite3.Connection, entity: str = "synthetic") -> tuple | None: + return conn.execute("SELECT * FROM entity_style_vectors WHERE entity=?", (entity,)).fetchone() + + def seed_legacy(self, conn: sqlite3.Connection) -> tuple: + conn.execute("INSERT INTO entity_style_vectors(entity,vector,message_count,updated_at) VALUES (?,?,?,?)", + ("synthetic", json.dumps(axis(0)), 9, "synthetic-old-generation")) + conn.commit() + return self.row(conn) + + def append(self, conn: sqlite3.Connection, vector: list[float], *, entity: str = "synthetic") -> None: + STYLE.update_entity_style_vector_incremental(conn, entity, "synthetic message", _pre_computed_vec=vector) + + def assert_close(self, expected: list[float], actual: list[float]) -> None: + self.assertEqual(len(expected), 256) + self.assertEqual(len(actual), 256) + self.assertTrue(all(math.isclose(a, b, rel_tol=1e-12, abs_tol=1e-12) for a, b in zip(expected, actual))) + + def test_permutations_match_batch_and_preserve_raw_sum(self) -> None: + vectors = [axis(0), axis(1), axis(1)] + for order in itertools.permutations(vectors): + conn = self.connection() + for vector in order: + self.append(conn, vector) + self.assert_close(reference_mean(vectors), STYLE.get_entity_style_vector(conn, "SYNTHETIC")) + raw, count, version = conn.execute("SELECT vector_sum,message_count,accumulator_version FROM entity_style_vectors").fetchone() + self.assertEqual((len(raw), count, version), (2048, 3, 1)) + self.assertEqual(struct.unpack("<256d", raw), (1.0, 2.0) + (0.0,) * 254) + self.assertTrue(conn.in_transaction) + + def test_seeded_orders_match_reference(self) -> None: + rng = random.Random(747) + vectors = [] + for _ in range(128): + vector = [rng.random() for _ in range(256)] + norm = math.sqrt(sum(value * value for value in vector)) + vectors.append([value / norm for value in vector]) + for count in (1, 3, 31, 128): + selected = vectors[:count] + shuffled = list(selected) + rng.shuffle(shuffled) + for order in (selected, list(reversed(selected)), shuffled): + conn = self.connection() + for vector in order: + self.append(conn, vector) + self.assert_close(reference_mean(selected), STYLE.get_entity_style_vector(conn, "synthetic")) + + def test_old_code_negative_controls(self) -> None: + namespace = dict(STYLE.__dict__) + exec(LEGACY_INCREMENTAL, namespace) + old = namespace["update_entity_style_vector_incremental"] + for vectors in ([axis(0), axis(1), axis(1)], [[0.0] * 256, axis(0), axis(1)]): + conn = self.connection() + for vector in vectors: + old(conn, "synthetic", "synthetic message", _pre_computed_vec=vector) + actual = STYLE.get_entity_style_vector(conn, "synthetic") + self.assertGreater(max(abs(a - b) for a, b in zip(actual, reference_mean(vectors))), 0.05) + conn = self.connection(legacy=True) + before = self.seed_legacy(conn) + old(conn, "synthetic", "synthetic message", _pre_computed_vec=axis(1)) + self.assertNotEqual(self.row(conn), before) + + def test_zero_and_noop_semantics(self) -> None: + for vectors in ([[0.0] * 256], [[0.0] * 256, axis(0), axis(1)], + [axis(0), [0.0] * 256, axis(1)], [axis(0), axis(1), [0.0] * 256]): + conn = self.connection() + for vector in vectors: + self.append(conn, vector) + self.assert_close(reference_mean(vectors), STYLE.get_entity_style_vector(conn, "synthetic")) + self.assertEqual(conn.execute("SELECT message_count FROM entity_style_vectors").fetchone()[0], len(vectors)) + conn = self.connection() + STYLE.update_entity_style_vector_incremental(conn, "", "message") + STYLE.update_entity_style_vector_incremental(conn, "synthetic", "", _pre_computed_vec=axis(0)) + self.assertEqual(self.columns(conn), []) + self.assertFalse(conn.in_transaction) + for text in ("a", "ab", " \t\n "): + STYLE.update_entity_style_vector_incremental(conn, "synthetic", text) + self.assertEqual(conn.execute("SELECT message_count FROM entity_style_vectors").fetchone()[0], 3) + self.assertEqual(STYLE.get_entity_style_vector(conn, "synthetic"), [0.0] * 256) + + def test_backup_restart_between_additions(self) -> None: + vectors = [axis(0), axis(1), axis(1)] + conn = self.connection() + for vector in vectors: + self.append(conn, vector) + conn.commit() + before = self.row(conn) + restored = sqlite3.connect(":memory:") + self.addCleanup(restored.close) + conn.backup(restored) + conn.close() + conn = restored + self.assertEqual(self.row(conn), before) + loaded_again = load_stdlib_source("personality_style_vec") + self.assertEqual(loaded_again.get_entity_style_vector(conn, "synthetic"), json.loads(before[1])) + self.assert_close(reference_mean(vectors), STYLE.get_entity_style_vector(conn, "synthetic")) + + def test_batch_arithmetic_and_append_after_rebuild(self) -> None: + conn = self.connection(legacy=True) + texts = ["please deploy the blue service", "I prefer quiet mornings and coffee", "another synthetic sentence"] + for index, text in enumerate(texts): + conn.execute("INSERT INTO messages VALUES (?,?,?,?,?)", (index + 1, "Synthetic", text, str(index), 0)) + conn.commit() + result = STYLE.build_entity_style_vectors(conn) + vectors = [STYLE.compute_style_vector(text) for text in texts] + self.assertEqual(result["synthetic"], reference_mean(vectors)) + self.assertEqual(result["synthetic"], STYLE.mean_pool_vectors(vectors)) + self.assertFalse(conn.in_transaction) + extra = STYLE.compute_style_vector("one more synthetic message") + self.append(conn, extra) + self.assert_close(reference_mean(vectors + [extra]), STYLE.get_entity_style_vector(conn, "synthetic")) + + def test_batch_directives_counts_and_case_folding(self) -> None: + conn = self.connection() + rows = [(1, "Synthetic", "abc", "1", 0), (2, "SYNTHETIC", "", "2", None), + (3, "Synthetic", "a", "3", 0), (4, "Synthetic", "directive text", "4", 1), + (5, "Directive-only", "directive text", "5", 1), (6, "", "no sender", "6", 0)] + conn.executemany("INSERT INTO messages VALUES (?,?,?,?,?)", rows) + conn.commit() + result = STYLE.build_entity_style_vectors(conn) + self.assertEqual(set(result), {"synthetic"}) + self.assertEqual(conn.execute("SELECT message_count FROM entity_style_vectors").fetchone()[0], 3) + self.assertEqual(result["synthetic"], reference_mean([STYLE.compute_style_vector(text) for text in ("", "abc", "a")])) + self.assertEqual(STYLE.get_entity_style_vector(conn, "SYNTHETIC"), result["synthetic"]) + + def test_batch_minimal_source_schema_and_empty_generation(self) -> None: + conn = sqlite3.connect(":memory:") + self.addCleanup(conn.close) + conn.execute("CREATE TABLE messages(sender TEXT, content TEXT, timestamp TEXT)") + conn.execute("INSERT INTO messages VALUES ('Synthetic','abc','1')") + conn.commit() + self.assertEqual(STYLE.build_entity_style_vectors(conn), {"synthetic": reference_mean([STYLE.compute_style_vector("abc")])}) + conn.execute("DELETE FROM messages") + conn.commit() + self.assertEqual(STYLE.build_entity_style_vectors(conn), {}) + self.assertEqual(conn.execute("SELECT count(*) FROM entity_style_vectors").fetchone()[0], 0) + + def test_legacy_append_preserves_row_schema_and_outer_writes(self) -> None: + for caller in (False, True): + conn = self.connection(legacy=True) + before = self.seed_legacy(conn) + columns = self.columns(conn) + if caller: + conn.execute("INSERT INTO sentinel VALUES ('pending')") + conn.execute("INSERT INTO messages VALUES (1,'Synthetic','source survives','1',0)") + with self.assertRaises(STYLE.StyleVectorRebuildRequired) as raised: + self.append(conn, axis(1)) + self.assertEqual(raised.exception.reason, "legacy") + self.assertEqual(self.row(conn), before) + self.assertEqual(self.columns(conn), columns) + self.assertEqual(conn.in_transaction, caller) + conn.commit() + self.assertEqual(conn.execute("SELECT count(*) FROM sentinel").fetchone()[0], int(caller)) + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], int(caller)) + self.assertEqual(STYLE.get_entity_style_vector(conn, "synthetic"), axis(0)) + + def test_bad_persisted_accumulators_defer_without_writes(self) -> None: + raw = struct.pack("<256d", *axis(0)) + cases = [(None, 1, 0, "legacy"), (raw, 1, 0, "legacy"), (raw, 1, 2, "unsupported_version"), + (raw, 1, -1, "invalid"), (raw, 1, "bad", "invalid"), (None, 1, 1, "invalid"), + (raw[:-1], 1, 1, "invalid"), (raw.hex(), 1, 1, "invalid"), (raw, 0, 1, "invalid"), + (raw, None, 1, "invalid"), (raw, 1.5, 1, "invalid"), (raw, "bad", 1, "invalid"), + (struct.pack("<256d", float("nan"), *([0.0] * 255)), 1, 1, "invalid"), + (struct.pack("<256d", float("inf"), *([0.0] * 255)), 1, 1, "invalid"), + (struct.pack("<256d", 1e308, *([0.0] * 255)), 1, 1, "invalid")] + for total, count, version, reason in cases: + with self.subTest(count=count, version=version, reason=reason): + conn = self.connection(full=True) + conn.execute("INSERT INTO entity_style_vectors VALUES (?,?,?,?,?,?)", + ("synthetic", json.dumps(axis(0)), count, "unchanged", total, version)) + conn.commit() + before = self.row(conn) + with self.assertRaises(STYLE.StyleVectorRebuildRequired) as raised: + self.append(conn, axis(1)) + self.assertEqual(raised.exception.reason, reason) + self.assertEqual(self.row(conn), before) + self.assertFalse(conn.in_transaction) + + def test_invalid_input_fails_before_schema_or_writer(self) -> None: + for vector in ([], [0.0] * 255, [0.0] * 257, [True] * 256, ["x"] * 256, + [float("nan")] * 256, [float("inf")] * 256, [10**1000] * 256): + conn = self.connection() + statements = [] + conn.set_trace_callback(statements.append) + with self.assertRaises(ValueError): + self.append(conn, vector) + conn.set_trace_callback(None) + self.assertEqual(statements, []) + self.assertFalse(conn.in_transaction) + self.assertEqual(self.columns(conn), []) + + def test_precomputed_vector_skips_hashing(self) -> None: + conn = self.connection() + with patch.object(STYLE, "compute_style_vector", side_effect=AssertionError("must use precomputed vector")): + self.append(conn, axis(0)) + self.assertEqual(STYLE.get_entity_style_vector(conn, "synthetic"), axis(0)) + + def test_append_success_retains_caller_commit_and_rollback(self) -> None: + for caller in (False, True): + conn = self.connection(legacy=True) + columns = self.columns(conn) + if caller: + conn.execute("INSERT INTO sentinel VALUES ('pending')") + self.append(conn, axis(0)) + self.assertTrue(conn.in_transaction) + self.assertEqual(STYLE.get_entity_style_vector(conn, "synthetic"), axis(0)) + conn.rollback() + self.assertEqual(self.columns(conn), columns) + self.assertIsNone(self.row(conn)) + self.assertEqual(conn.execute("SELECT count(*) FROM sentinel").fetchone()[0], 0) + + def test_append_write_failure_rolls_back_its_generation(self) -> None: + for caller in (False, True): + conn = self.connection() + self.append(conn, axis(0)) + conn.commit() + before = self.row(conn) + conn.execute("CREATE TRIGGER fail_style BEFORE INSERT ON entity_style_vectors BEGIN SELECT RAISE(ABORT,'synthetic failure'); END") + if caller: + conn.execute("INSERT INTO sentinel VALUES ('pending')") + with self.assertRaisesRegex(sqlite3.IntegrityError, "synthetic failure"): + self.append(conn, axis(1)) + self.assertEqual(conn.in_transaction, caller) + conn.commit() + self.assertEqual(self.row(conn), before) + self.assertEqual(conn.execute("SELECT count(*) FROM sentinel").fetchone()[0], int(caller)) + + def test_append_savepoint_and_release_failure_preserve_caller(self) -> None: + for caller, operation, cache in itertools.product((False, True), ("BEGIN", "RELEASE"), (0, 5, 100)): + conn = self.connection(cached_statements=cache) + self.append(conn, axis(0)) + conn.commit() + before = self.row(conn) + if caller: + conn.execute("INSERT INTO sentinel VALUES ('pending')") + sql = ("SAVEPOINT" if operation == "BEGIN" else "RELEASE SAVEPOINT") + " truememory_style_append" + probe = StatementFailure(conn, sql, (sqlite3.SQLITE_SAVEPOINT, operation, "truememory_style_append")) + with self.subTest(caller=caller, operation=operation, cached_statements=cache): + with self.assertRaisesRegex(sqlite3.DatabaseError, "not authorized"): + self.append(probe, axis(1)) + self.assertEqual(probe.armed, [sql]) + self.assertEqual(conn.in_transaction, caller) + conn.commit() + self.assertEqual(self.row(conn), before) + self.assertEqual(conn.execute("SELECT count(*) FROM sentinel").fetchone()[0], int(caller)) + + def test_storage_migration_preserves_legacy_and_source_counters(self) -> None: + conn = self.connection(legacy=True, full=True) + before = self.seed_legacy(conn) + token = conn.execute("SELECT * FROM maintenance_source_state").fetchone() + STORAGE._initialize_style_accumulator_schema(conn) + self.assertEqual(self.row(conn), before + (None, 0)) + self.assertEqual(conn.execute("SELECT * FROM maintenance_source_state").fetchone(), token) + self.assertFalse(conn.in_transaction) + fields = {row[1]: row for row in self.columns(conn)} + self.assertEqual(fields["vector_sum"][2:5], ("BLOB", 0, "NULL")) + self.assertEqual(fields["accumulator_version"][2:5], ("INTEGER", 1, "0")) + + def test_create_db_initializes_only_format_for_legacy_profiles(self) -> None: + conn = self.connection(legacy=True, full=True) + before = self.seed_legacy(conn) + conn.execute("INSERT INTO messages(content,sender) VALUES ('synthetic source','Synthetic')") + conn.commit() + token = conn.execute("SELECT * FROM maintenance_source_state").fetchone() + with patch.object(STORAGE.sqlite3, "connect", return_value=conn): + self.assertIs(STORAGE.create_db(":memory:"), conn) + self.assertEqual(self.row(conn), before + (None, 0)) + self.assertEqual(conn.execute("SELECT * FROM maintenance_source_state").fetchone(), token) + with self.assertRaises(STYLE.StyleVectorRebuildRequired) as raised: + self.append(conn, axis(1)) + self.assertEqual(raised.exception.reason, "legacy") + + def test_partial_column_installation_uses_named_fields(self) -> None: + for existing in ("vector_sum BLOB DEFAULT NULL", "accumulator_version INTEGER NOT NULL DEFAULT 0"): + conn = self.connection(legacy=True) + conn.execute("ALTER TABLE entity_style_vectors ADD COLUMN " + existing) + conn.commit() + STORAGE._initialize_style_accumulator_schema(conn) + self.append(conn, axis(0)) + self.append(conn, axis(1)) + self.assertEqual(conn.execute("SELECT message_count,accumulator_version,length(vector_sum) FROM entity_style_vectors").fetchone(), + (2, 1, 2048)) + self.assert_close(reference_mean([axis(0), axis(1)]), STYLE.get_entity_style_vector(conn, "synthetic")) + + def test_repeated_schema_check_does_not_read_source_or_write(self) -> None: + conn = self.connection(full=True) + statements = [] + def authorize(action: int, one: str | None, *_rest: object) -> int: + if action in (sqlite3.SQLITE_INSERT, sqlite3.SQLITE_UPDATE, sqlite3.SQLITE_DELETE, sqlite3.SQLITE_TRANSACTION): + return sqlite3.SQLITE_DENY + if action == sqlite3.SQLITE_READ and one in ("messages", "messages_fts", "entity_style_vectors"): + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + conn.set_authorizer(authorize) + conn.set_trace_callback(statements.append) + try: + STORAGE._initialize_style_accumulator_schema(conn) + finally: + conn.set_authorizer(allow_authorized_action) + conn.set_trace_callback(None) + self.assertEqual(statements, ["PRAGMA table_info(entity_style_vectors)"]) + + def test_storage_schema_migration_rolls_back_with_caller(self) -> None: + conn = self.connection(legacy=True) + before = self.seed_legacy(conn) + columns = self.columns(conn) + conn.execute("INSERT INTO sentinel VALUES ('pending')") + STORAGE._initialize_style_accumulator_schema(conn) + self.assertTrue(conn.in_transaction) + self.assertEqual(self.row(conn), before + (None, 0)) + conn.rollback() + self.assertEqual(self.row(conn), before) + self.assertEqual(self.columns(conn), columns) + self.assertEqual(conn.execute("SELECT count(*) FROM sentinel").fetchone()[0], 0) + + def test_partial_schema_migration_failure_is_atomic(self) -> None: + for caller, cache in itertools.product((False, True), (0, 5, 100)): + conn = self.connection(legacy=True, cached_statements=cache) + before = self.seed_legacy(conn) + columns = self.columns(conn) + if caller: + conn.execute("INSERT INTO sentinel VALUES ('pending')") + sql = "ALTER TABLE entity_style_vectors ADD COLUMN accumulator_version INTEGER NOT NULL DEFAULT 0" + probe = StatementFailure(conn, sql, (sqlite3.SQLITE_ALTER_TABLE, "main", "entity_style_vectors")) + statements: list[str] = [] + conn.set_trace_callback(statements.append) + try: + with self.subTest(caller=caller, cached_statements=cache): + with self.assertRaisesRegex(sqlite3.DatabaseError, "not authorized"): + STORAGE._initialize_style_accumulator_schema(probe) + self.assertEqual(probe.armed, [sql]) + finally: + conn.set_trace_callback(None) + self.assertIn("ALTER TABLE entity_style_vectors ADD COLUMN vector_sum BLOB DEFAULT NULL", statements) + self.assertEqual(conn.in_transaction, caller) + self.assertEqual(self.columns(conn), columns) + conn.commit() + self.assertEqual(self.row(conn), before) + self.assertEqual(conn.execute("SELECT count(*) FROM sentinel").fetchone()[0], int(caller)) + + def test_schema_commit_or_release_failure_restores_legacy_format(self) -> None: + for caller, cache in itertools.product((False, True), (0, 5, 100)): + conn = self.connection(legacy=True, cached_statements=cache) + before = self.seed_legacy(conn) + columns = self.columns(conn) + if caller: + conn.execute("INSERT INTO sentinel VALUES ('pending')") + sql = "RELEASE SAVEPOINT truememory_style_schema" if caller else "COMMIT" + denied_action = ((sqlite3.SQLITE_SAVEPOINT, "RELEASE", "truememory_style_schema") if caller else + (sqlite3.SQLITE_TRANSACTION, "COMMIT", None)) + probe = StatementFailure(conn, sql, denied_action) + with self.subTest(caller=caller, cached_statements=cache): + with self.assertRaisesRegex(sqlite3.DatabaseError, "not authorized"): + STORAGE._initialize_style_accumulator_schema(probe) + self.assertEqual(probe.armed, [sql]) + self.assertEqual(conn.in_transaction, caller) + self.assertEqual(self.columns(conn), columns) + conn.commit() + self.assertEqual(self.row(conn), before) + self.assertEqual(conn.execute("SELECT count(*) FROM sentinel").fetchone()[0], int(caller)) + + def test_batch_schema_creation_occurs_only_after_computation(self) -> None: + conn = self.connection() + conn.execute("INSERT INTO messages VALUES (1,'Synthetic','abc','1',0)") + conn.commit() + compute = STYLE.compute_style_vector + observed = [] + def outside_writer(text: str) -> list[float]: + observed.append((conn.in_transaction, self.columns(conn))) + return compute(text) + with patch.object(STYLE, "compute_style_vector", outside_writer): + STYLE.build_entity_style_vectors(conn) + self.assertEqual(observed, [(False, [])]) + self.assertEqual(conn.execute("SELECT accumulator_version,length(vector_sum) FROM entity_style_vectors").fetchone(), (1, 2048)) + + def test_batch_partial_publication_rolls_back_schema_and_rows(self) -> None: + for caller in (False, True): + conn = self.connection(legacy=True) + before = self.seed_legacy(conn) + columns = self.columns(conn) + conn.executemany("INSERT INTO messages VALUES (?,?,?,?,?)", + [(1, "a", "first content", "1", 0), (2, "b", "second content", "2", 0)]) + conn.commit() + conn.execute("CREATE TRIGGER fail_style BEFORE INSERT ON entity_style_vectors WHEN new.entity='b' BEGIN SELECT RAISE(ABORT,'synthetic failure'); END") + if caller: + conn.execute("INSERT INTO sentinel VALUES ('pending')") + with self.assertRaisesRegex(sqlite3.IntegrityError, "synthetic failure"): + STYLE.build_entity_style_vectors(conn) + self.assertEqual(conn.in_transaction, caller) + self.assertEqual(self.columns(conn), columns) + conn.commit() + self.assertEqual(self.row(conn), before) + self.assertEqual(conn.execute("SELECT count(*) FROM entity_style_vectors").fetchone()[0], 1) + self.assertEqual(conn.execute("SELECT count(*) FROM sentinel").fetchone()[0], int(caller)) + + def test_batch_late_cancellation_retains_prior_accumulator_generation(self) -> None: + conn = self.connection() + conn.executemany("INSERT INTO messages VALUES (?,?,?,?,?)", + [(1, "a", "first", "1", 0), (2, "b", "second", "2", 0)]) + conn.commit() + STYLE.build_entity_style_vectors(conn) + before = conn.execute("SELECT * FROM entity_style_vectors ORDER BY entity").fetchall() + conn.execute("INSERT INTO sentinel VALUES ('pending')") + compute = STYLE.compute_style_vector + calls = 0 + def cancelled(text: str) -> list[float]: + nonlocal calls + calls += 1 + if calls == 2: + raise KeyboardInterrupt("synthetic cancellation") + return compute(text) + with patch.object(STYLE, "compute_style_vector", cancelled): + with self.assertRaises(KeyboardInterrupt): + STYLE.build_entity_style_vectors(conn) + conn.commit() + self.assertEqual(conn.execute("SELECT * FROM entity_style_vectors ORDER BY entity").fetchall(), before) + self.assertEqual(conn.execute("SELECT count(*) FROM sentinel").fetchone()[0], 1) + + def test_batch_source_change_rejects_before_schema_upgrade(self) -> None: + conn = self.connection(legacy=True) + before = self.seed_legacy(conn) + columns = self.columns(conn) + conn.execute("INSERT INTO messages VALUES (1,'Synthetic','original','1',0)") + conn.commit() + compute = STYLE.compute_style_vector + def changed(text: str) -> list[float]: + self.assertFalse(conn.in_transaction) + conn.execute("UPDATE messages SET content='corrected' WHERE id=1") + conn.commit() + return compute(text) + with patch.object(STYLE, "compute_style_vector", changed): + with self.assertRaisesRegex(sqlite3.OperationalError, "L0 source changed"): + STYLE.build_entity_style_vectors(conn) + self.assertFalse(conn.in_transaction) + self.assertEqual(self.columns(conn), columns) + self.assertEqual(self.row(conn), before) + + def test_batch_commit_and_release_failure_restore_accumulator(self) -> None: + for caller, cache in itertools.product((False, True), (0, 5, 100)): + conn = self.connection(legacy=True, cached_statements=cache) + before = self.seed_legacy(conn) + columns = self.columns(conn) + conn.execute("INSERT INTO messages VALUES (1,'Synthetic','replacement','1',0)") + conn.commit() + if caller: + conn.execute("INSERT INTO sentinel VALUES ('pending')") + sql = "RELEASE SAVEPOINT truememory_l0" if caller else "COMMIT" + denied_action = ((sqlite3.SQLITE_SAVEPOINT, "RELEASE", "truememory_l0") if caller else + (sqlite3.SQLITE_TRANSACTION, "COMMIT", None)) + probe = StatementFailure(conn, sql, denied_action, occurrence=2) + with self.subTest(caller=caller, cached_statements=cache): + with self.assertRaisesRegex(sqlite3.DatabaseError, "not authorized"): + STYLE.build_entity_style_vectors(probe) + self.assertEqual(probe.armed, [sql]) + self.assertEqual(probe.matches, 3 if caller else 2) + self.assertEqual(conn.in_transaction, caller) + self.assertEqual(self.columns(conn), columns) + conn.commit() + self.assertEqual(self.row(conn), before) + self.assertEqual(conn.execute("SELECT count(*) FROM sentinel").fetchone()[0], int(caller)) + + def test_batch_and_schema_do_not_advance_source_tracking(self) -> None: + conn = self.connection(legacy=True, full=True) + self.seed_legacy(conn) + conn.execute("INSERT INTO messages(content,sender) VALUES ('ordinary content','Synthetic')") + conn.commit() + token = conn.execute("SELECT * FROM maintenance_source_state").fetchone() + layers = conn.execute("SELECT * FROM maintenance_layers ORDER BY layer").fetchall() + STORAGE._initialize_style_accumulator_schema(conn) + STYLE.build_entity_style_vectors(conn) + self.assertEqual(conn.execute("SELECT * FROM maintenance_source_state").fetchone(), token) + self.assertEqual(conn.execute("SELECT * FROM maintenance_layers ORDER BY layer").fetchall(), layers) + + def test_two_connections_cannot_read_then_overwrite_one_another(self) -> None: + uri = "file:synthetic-style-" + uuid.uuid4().hex + "?mode=memory&cache=shared" + first = sqlite3.connect(uri, uri=True, timeout=0, cached_statements=100) + second = sqlite3.connect(uri, uri=True, timeout=0, cached_statements=100) + self.addCleanup(first.close) + self.addCleanup(second.close) + self.append(first, axis(0)) + first.commit() + self.append(first, axis(1)) + statements = [] + second.set_trace_callback(statements.append) + with self.assertRaises(sqlite3.OperationalError): + self.append(second, axis(1)) + second.set_trace_callback(None) + self.assertFalse(second.in_transaction) + self.assertFalse(any("SELECT vector_sum" in sql for sql in statements)) + first.commit() + self.append(second, axis(1)) + second.commit() + self.assertEqual(second.execute("SELECT message_count FROM entity_style_vectors").fetchone()[0], 3) + self.assert_close(reference_mean([axis(0), axis(1), axis(1)]), STYLE.get_entity_style_vector(first, "synthetic")) + + def test_getter_still_reads_legacy_json_and_missing_rows(self) -> None: + conn = self.connection(legacy=True) + self.seed_legacy(conn) + self.assertEqual(STYLE.get_entity_style_vector(conn, "SYNTHETIC"), axis(0)) + self.assertIsNone(STYLE.get_entity_style_vector(conn, "unknown")) + self.assertEqual(len(self.columns(conn)), 4) + + def test_actual_engine_add_catches_style_deferral_and_keeps_source(self) -> None: + rebuild = types.ModuleType("synthetic_rebuild") + rebuild.__dict__.update(sqlite3=sqlite3, Iterator=Iterator, AbstractContextManager=AbstractContextManager, + contextmanager=contextmanager, nullcontext=nullcontext) + tree = ast.parse((ROOT / "truememory/rebuild_source.py").read_text(encoding="utf-8")) + selected = [node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name == "rebuild_transaction"] + exec(compile(ast.Module(body=selected, type_ignores=[]), "rebuild_source.py", "exec"), rebuild.__dict__) + + maintenance = types.ModuleType("synthetic_append_maintenance") + modules = {"truememory.personality_style_vec": STYLE, "truememory.rebuild_source": rebuild, + "truememory.maintenance": maintenance, "truememory.storage": STORAGE, + "truememory._platform": load_stdlib_source("_platform"), + "truememory.tier_switch.writer": load_stdlib_source("tier_switch/writer")} + + def safe_import(name: str, globals: dict | None = None, locals: dict | None = None, + fromlist: tuple[str, ...] = (), level: int = 0) -> object: + if name in modules: + return modules[name] + if name.split(".", 1)[0] not in sys.stdlib_module_names: + raise AssertionError("Unexpected import in engine.add: " + name) + return builtins.__import__(name, globals, locals, fromlist, level) + + maintenance.__dict__["__builtins__"] = dict(vars(builtins), __import__=safe_import) + path = ROOT / "truememory/maintenance.py" + exec(compile(path.read_text(encoding="utf-8"), str(path), "exec"), maintenance.__dict__) + tree = ast.parse((ROOT / "truememory/engine.py").read_text(encoding="utf-8")) + engine_class = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == "TrueMemoryEngine") + add = next(node for node in engine_class.body if isinstance(node, ast.FunctionDef) and node.name == "add") + namespace = {"engine_operation": lambda function: function, "__builtins__": dict(vars(builtins), __import__=safe_import), "MAX_CONTENT_LENGTH": 50000, + "insert_message": STORAGE.insert_message, "_update_style_vec": STYLE.update_entity_style_vector_incremental, + "logger": logging.getLogger("synthetic_style_engine")} + validator = next(node for node in tree.body + if isinstance(node, ast.FunctionDef) and node.name == "_validate_add_content") + exec(compile(ast.Module(body=[validator, add], type_ignores=[]), "engine.py", "exec"), namespace) + for caller, directive in itertools.product((False, True), (False, True)): + conn = self.connection(legacy=True, full=True) + before = self.seed_legacy(conn) + engine = types.SimpleNamespace(conn=conn, _has_vectors=False, _has_personality=False, _has_style_vec=True, + _write_lock=threading.Lock(), _ensure_connection=lambda: None, + _maybe_auto_consolidate=lambda: None) + compute = STYLE.compute_style_vector + locks = [] + def outside_writer(text: str) -> list[float]: + locks.append(engine._write_lock.locked()) + return compute(text) + if caller: + conn.execute("INSERT INTO sentinel VALUES ('pending')") + with patch.object(STYLE, "compute_style_vector", outside_writer): + result = namespace["add"](engine, "source survives legacy deferral", sender="Synthetic", directive=directive) + self.assertEqual(locks, [False]) + self.assertEqual(self.row(conn), before) + self.assertEqual(conn.in_transaction, caller) + self.assertEqual(conn.execute("SELECT content,directive FROM messages WHERE id=?", (result["id"],)).fetchone(), + ("source survives legacy deferral", int(directive))) + if caller: + conn.rollback() + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-l0-style-incremental-747.py b/tests/test-l0-style-incremental-747.py new file mode 100644 index 00000000..404e969d --- /dev/null +++ b/tests/test-l0-style-incremental-747.py @@ -0,0 +1,960 @@ +"""Atomic incremental style coverage with actual source and in-memory SQLite.""" + +import ast +import builtins +import json +import logging +import runpy +import sqlite3 +import sys +import threading +import types +import unittest +from collections.abc import Iterator +from contextlib import AbstractContextManager, contextmanager, nullcontext +from pathlib import Path +from unittest.mock import Mock, patch + + +ROOT = Path(__file__).resolve().parents[1] +BASE = runpy.run_path(str(Path(__file__).with_name("test-l0-style-maintenance-747.py"))) +STORAGE, STYLE, MAINTENANCE = BASE["STORAGE"], BASE["STYLE"], BASE["MAINTENANCE"] +ACCUMULATOR = BASE["ACCUMULATOR"] + + +def load_add() -> types.FunctionType: + rebuild = types.ModuleType("synthetic_incremental_rebuild") + rebuild.__dict__.update(sqlite3=sqlite3, Iterator=Iterator, AbstractContextManager=AbstractContextManager, + contextmanager=contextmanager, nullcontext=nullcontext) + source = ast.parse((ROOT / "truememory/rebuild_source.py").read_text(encoding="utf-8")) + selected = [node for node in source.body if isinstance(node, ast.FunctionDef) and node.name == "rebuild_transaction"] + exec(compile(ast.Module(body=selected, type_ignores=[]), "synthetic-rebuild", "exec"), rebuild.__dict__) + modules = {"truememory.personality_style_vec": STYLE, "truememory.rebuild_source": rebuild, + "truememory.maintenance": MAINTENANCE, + "truememory.tier_switch.runtime": MAINTENANCE._fixture_modules["truememory.tier_switch.runtime"], + "truememory.tier_switch.writer": ACCUMULATOR["load_stdlib_source"]("tier_switch/writer")} + + def safe_import(name: str, globals: dict | None = None, locals: dict | None = None, + fromlist: tuple = (), level: int = 0) -> object: + if name in modules: + return modules[name] + if name.split(".", 1)[0] not in sys.stdlib_module_names: + raise AssertionError("Non-stdlib import forbidden: " + name) + return builtins.__import__(name, globals, locals, fromlist, level) + + tree = ast.parse((ROOT / "truememory/engine.py").read_text(encoding="utf-8")) + engine = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == "TrueMemoryEngine") + add = next(node for node in engine.body if isinstance(node, ast.FunctionDef) and node.name == "add") + namespace = {"engine_operation": lambda function: function, "__builtins__": dict(vars(builtins), __import__=safe_import), "MAX_CONTENT_LENGTH": 50000, + "insert_message": STORAGE.insert_message, "_update_style_vec": STYLE.update_entity_style_vector_incremental, + "logger": logging.getLogger("synthetic-style-incremental")} + validator = next(node for node in tree.body + if isinstance(node, ast.FunctionDef) and node.name == "_validate_add_content") + exec(compile(ast.Module(body=[validator, add], type_ignores=[]), "actual-engine-add", "exec"), namespace) + return namespace["add"] + + +ADD = load_add() + + +class BoundaryFailure: + def __init__(self, conn: sqlite3.Connection, boundary: str) -> None: + self.conn, self.boundary = conn, boundary + self.denied = 0 + + def __getattr__(self, name: str) -> object: + return getattr(self.conn, name) + + def commit(self) -> None: + self.execute("COMMIT") + + def execute(self, sql: str, *args: object) -> sqlite3.Cursor: + match = (sql == "COMMIT" if self.boundary == "commit" else + sql == "RELEASE SAVEPOINT rebuild_source" if self.boundary == "caller_release" else + sql.startswith("RELEASE truememory_style_coverage_")) + if match and not self.denied: + self.denied += 1 + action = ((sqlite3.SQLITE_TRANSACTION, "COMMIT", None) if sql == "COMMIT" else + (sqlite3.SQLITE_SAVEPOINT, "RELEASE", sql.split()[-1])) + fault = ACCUMULATOR["StatementFailure"](self.conn, sql, action) + return fault.execute(sql, *args) + return self.conn.execute(sql, *args) + + +class StyleIncrementalTests(unittest.TestCase): + def connection(self, *, bootstrap: bool = True, cache: int = 100) -> sqlite3.Connection: + conn = sqlite3.connect(":memory:", cached_statements=cache) + self.addCleanup(conn.close) + conn.executescript(STORAGE._SCHEMA_SQL) + STORAGE._initialize_maintenance_tracking(conn) + conn.execute("CREATE TABLE sentinel(value TEXT)") + conn.commit() + if bootstrap: + self.assertEqual(MAINTENANCE.run_style_maintenance(conn).outcome, "success_empty") + return conn + + def engine(self, conn: sqlite3.Connection) -> types.SimpleNamespace: + engine = types.SimpleNamespace(conn=conn, _has_vectors=False, _has_personality=False, _has_style_vec=True, + _write_lock=threading.Lock(), _ensure_connection=lambda: None, + _maybe_auto_consolidate=lambda: None) + engine.add = types.MethodType(ADD, engine) + return engine + + def checkpoint(self, conn: sqlite3.Connection) -> tuple | None: + return MAINTENANCE._read_layer_row(conn, "style_vectors") + + def rows(self, conn: sqlite3.Connection) -> list[tuple]: + return conn.execute("SELECT * FROM entity_style_vectors ORDER BY entity").fetchall() + + def state(self, conn: sqlite3.Connection) -> object: + spec = MAINTENANCE.style_layer_spec(conn) + return MAINTENANCE.read_layer_states(conn, (spec,))[spec.layer] + + def append(self, conn: sqlite3.Connection, previous: object, *, content: str = "synthetic ordinary", + sender: str = "Synthetic", directive: bool = False, message_id: int | None = None, + publish: object = None) -> object: + if message_id is None: + message_id = STORAGE.insert_message(conn, {"content": content, "sender": sender, "directive": directive}) + vector = STYLE.compute_style_vector(content) + return MAINTENANCE._publish_style_append( + conn, previous, message_id, sender=sender, content=content, directive=directive, precomputed=True, + publish=publish or (lambda: STYLE.update_entity_style_vector_incremental( + conn, sender, content, _pre_computed_vec=vector)), + ) + + def test_75_actual_adds_advance_raw_sums_and_attempts_without_any_full_build(self) -> None: + conn = self.connection() + engine = self.engine(conn) + expected = {} + original = STYLE._compute_entity_style_vectors + with patch.object(STYLE, "_compute_entity_style_vectors", wraps=original) as builds: + for index in range(75): + sender = "" if index % 11 == 0 else ("Synthetic" if index % 2 else "SYNTHETIC") + directive = index % 13 == 0 + text = "a" if index % 7 == 0 else "synthetic ordinary " + str(index) + result = engine.add(text, sender=sender, directive=directive) + self.assertEqual(result["id"], index + 1) + if sender and not directive: + total, count = expected.setdefault(sender.lower(), [[0.0] * STYLE.DIM, 0]) + vector = STYLE.compute_style_vector(text) + expected[sender.lower()][0] = [one + two for one, two in zip(total, vector)] + expected[sender.lower()][1] = count + 1 + state = self.state(conn) + self.assertEqual((state.successful_revision, state.attempted_revision, state.attempted_insert_count), + (index + 1, index + 1, index + 1)) + self.assertEqual((state.successful_coverage, state.full_rebuild_required), ("complete", False)) + self.assertEqual(MAINTENANCE.run_style_maintenance(conn, threshold=25).outcome, "current") + self.assertEqual(builds.call_count, 0) + self.assertEqual(self.state(conn).output_count, len(expected)) + for entity, (total, count) in expected.items(): + row = conn.execute("SELECT vector_sum,message_count,accumulator_version,vector FROM entity_style_vectors WHERE entity=?", + (entity,)).fetchone() + self.assertEqual(row[:3], (STYLE._STYLE_SUM.pack(*total), count, 1)) + self.assertEqual(json.loads(row[3]), STYLE._style_profile_from_sum(total, count)) + self.assertFalse(conn.in_transaction) + + def test_new_entities_case_merging_duplicates_and_zero_vectors_count_once(self) -> None: + conn = self.connection() + engine = self.engine(conn) + for sender, text in (("One", "a"), ("ONE", "ab"), ("One", "ab"), ("Two", "synthetic second")): + engine.add(text, sender=sender) + self.assertEqual(conn.execute("SELECT entity,message_count FROM entity_style_vectors ORDER BY entity").fetchall(), + [("one", 3), ("two", 1)]) + self.assertEqual(STYLE.get_entity_style_vector(conn, "one"), [0.0] * STYLE.DIM) + self.assertEqual(self.state(conn).output_count, 2) + + def test_excluded_rows_advance_successful_empty_without_calling_updater(self) -> None: + conn = self.connection() + with patch.dict(ADD.__globals__, _update_style_vec=lambda *args, **kwargs: self.fail("Excluded profile write")): + self.engine(conn).add("synthetic directive", sender="Synthetic", directive=True) + self.engine(conn).add("synthetic anonymous", sender="") + self.assertEqual(self.rows(conn), []) + self.assertEqual((self.state(conn).outcome, self.state(conn).attempted_insert_count), ("success_empty", 2)) + + def test_no_enrollment_and_no_rebuild_when_coverage_is_missing_or_dirty(self) -> None: + for mode in ("missing", "dirty", "legacy", "running", "failed", "future", "hash", "triggers"): + with self.subTest(mode=mode): + conn = self.connection(bootstrap=mode != "missing") + if mode == "dirty": + conn.execute("UPDATE maintenance_layers SET full_rebuild_required=1 WHERE layer='style_vectors'") + elif mode == "legacy": + conn.execute("INSERT INTO entity_style_vectors(entity,vector) VALUES ('synthetic','[]')") + elif mode in {"running", "failed"}: + conn.execute("UPDATE maintenance_layers SET outcome=? WHERE layer='style_vectors'", (mode,)) + elif mode == "future": + conn.execute("ALTER TABLE entity_style_vectors RENAME TO old_style") + conn.execute("CREATE TABLE entity_style_vectors(entity TEXT PRIMARY KEY, vector TEXT, message_count INTEGER, " + "updated_at TEXT, vector_sum BLOB, accumulator_version INTEGER DEFAULT 9)") + for name, definition in STORAGE._STYLE_OUTPUT_TRIGGERS.items(): + conn.execute("DROP TRIGGER " + name) + conn.execute(definition) + self.assertEqual(self.state(conn).successful_coverage, "complete") + elif mode == "hash": + conn.execute("UPDATE metadata SET value='1' WHERE key='style_vec_hash_version'") + elif mode == "triggers": + conn.execute("DROP TRIGGER truememory_style_output_insert") + conn.commit() + before, checkpoint = self.rows(conn), self.checkpoint(conn) + updater = Mock(side_effect=AssertionError("Unsafe profile write")) + with patch.dict(ADD.__globals__, _update_style_vec=updater), \ + patch.object(STYLE, "_compute_entity_style_vectors", side_effect=AssertionError("No fallback rebuild")) as builds: + self.engine(conn).add("synthetic pending", sender="Synthetic") + updater.assert_not_called() + builds.assert_not_called() + self.assertEqual(self.rows(conn), before) + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], 1) + if mode == "missing": + self.assertIsNone(self.checkpoint(conn)) + self.assertEqual(conn.execute("SELECT count(*) FROM maintenance_layers").fetchone()[0], 8) + if mode == "failed": + self.assertEqual(self.checkpoint(conn), checkpoint) + + def test_incomplete_attempt_or_source_proof_cannot_advance_coverage(self) -> None: + mutations = ( + "UPDATE maintenance_layers SET attempted_insert_count=NULL WHERE layer='style_vectors'", + "UPDATE maintenance_layers SET successful_coverage='unverified' WHERE layer='style_vectors'", + "UPDATE maintenance_layers SET output_count=-1 WHERE layer='style_vectors'", + "UPDATE maintenance_layers SET builder_version=9 WHERE layer='style_vectors'", + "UPDATE maintenance_source_state SET revision=1", + ) + for sql in mutations: + with self.subTest(sql=sql): + conn = self.connection() + conn.execute(sql) + conn.commit() + updater = Mock(side_effect=AssertionError("Incomplete proof")) + with patch.dict(ADD.__globals__, _update_style_vec=updater): + self.engine(conn).add("synthetic pending", sender="Synthetic") + updater.assert_not_called() + self.assertEqual(self.rows(conn), []) + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], 1) + + def test_precompute_failure_preserves_source_and_invalidates_current_attempt(self) -> None: + conn = self.connection() + with patch.object(STYLE, "compute_style_vector", side_effect=ValueError("synthetic failure")): + self.engine(conn).add("synthetic retained source", sender="Synthetic") + state = self.state(conn) + self.assertEqual((state.outcome, state.error_category, state.attempted_revision), + ("pending", "StylePrecomputeFailed", None)) + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], 1) + self.assertEqual(self.rows(conn), []) + self.assertEqual(MAINTENANCE.run_style_maintenance(conn).outcome, "success") + self.assertEqual(conn.execute("SELECT message_count FROM entity_style_vectors").fetchone()[0], 1) + + def test_source_insert_hash_trigger_cannot_certify_an_old_hash(self) -> None: + conn = self.connection() + engine = self.engine(conn) + engine.add("synthetic baseline", sender="Synthetic") + before = self.rows(conn) + conn.execute("CREATE TRIGGER synthetic_source_hash AFTER INSERT ON messages BEGIN " + "UPDATE metadata SET value='1' WHERE key='style_vec_hash_version'; END") + conn.commit() + engine.add("synthetic retained", sender="Synthetic") + self.assertEqual(self.rows(conn), before) + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], 2) + self.assertEqual(conn.execute("SELECT value FROM metadata WHERE key='style_vec_hash_version'").fetchone()[0], "1") + self.assertEqual((self.state(conn).outcome, self.state(conn).error_category), ("pending", "StyleHashPending")) + + def test_extra_main_and_temp_output_triggers_defer_without_deleting_other_profiles(self) -> None: + for temporary, name in ((False, "synthetic_extra"), (True, "synthetic_extra"), + (True, "truememory_style_output_insert"), (False, "truememory_style_output_extra")): + with self.subTest(temporary=temporary, name=name): + conn = self.connection() + engine = self.engine(conn) + engine.add("synthetic other profile", sender="Other") + before, checkpoint = self.rows(conn), self.checkpoint(conn) + conn.execute("CREATE " + ("TEMP " if temporary else "") + "TRIGGER " + name + + " AFTER INSERT ON main.entity_style_vectors WHEN new.entity='synthetic' BEGIN " + "DELETE FROM entity_style_vectors WHERE entity='other'; END") + conn.commit() + updater = Mock(wraps=STYLE.update_entity_style_vector_incremental) + with patch.dict(ADD.__globals__, _update_style_vec=updater): + engine.add("synthetic retained", sender="Synthetic") + updater.assert_not_called() + self.assertEqual(self.rows(conn), before) + self.assertEqual(self.checkpoint(conn), checkpoint) + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], 2) + changes = conn.total_changes + result = MAINTENANCE.run_style_maintenance(conn, force=True) + self.assertEqual((result.outcome, result.error_category, result.attempted), + ("deferred", "StyleTriggersUnsupported", False)) + spec = MAINTENANCE.style_layer_spec(conn) + self.assertTrue(spec.resolve_dependency().deferred) + self.assertEqual(MAINTENANCE.run_layers(conn, (spec,), force=True)[0].error_category, + "StyleTriggersUnsupported") + self.assertEqual(conn.total_changes, changes) + + def test_extra_checkpoint_triggers_cannot_run_while_reporting_pending(self) -> None: + for temporary in (False, True): + for action in ("failure", "delete_profile"): + with self.subTest(temporary=temporary, action=action): + conn = self.connection() + engine = self.engine(conn) + engine.add("synthetic other profile", sender="Other") + before, checkpoint = self.rows(conn), self.checkpoint(conn) + change = ("UPDATE maintenance_layers SET outcome='failed',error_category='NewerFailure'," + "run_generation='newer-owner',attempted_insert_count=999 WHERE layer='style_vectors';" + if action == "failure" else "DELETE FROM entity_style_vectors WHERE entity='other';") + conn.execute("CREATE " + ("TEMP " if temporary else "") + + "TRIGGER synthetic_checkpoint AFTER UPDATE OF outcome ON main.maintenance_layers " + "WHEN new.layer='style_vectors' AND new.outcome='pending' BEGIN " + change + " END") + conn.commit() + with patch.object(STYLE, "compute_style_vector", side_effect=ValueError("synthetic precompute")): + engine.add("synthetic retained", sender="Synthetic") + self.assertEqual(self.rows(conn), before) + self.assertEqual(self.checkpoint(conn), checkpoint) + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], 2) + self.assertEqual(self.state(conn).dependency.error_category, "StyleTriggersUnsupported") + + def test_metadata_triggers_defer_before_preparation_or_rebuild_writes(self) -> None: + for temporary in (False, True): + for readiness in ("ready", "unenrolled", "repair"): + for borrowed in (False, True): + with self.subTest(temporary=temporary, readiness=readiness, borrowed=borrowed): + conn = self.connection(bootstrap=False) + STORAGE.insert_message(conn, {"content": "synthetic source", "sender": "Synthetic", "directive": False}) + conn.commit() + if readiness != "unenrolled": + self.assertEqual(MAINTENANCE.run_style_maintenance(conn).outcome, "success") + if readiness == "repair": + conn.execute("DROP TRIGGER truememory_style_output_update") + conn.execute("CREATE " + ("TEMP " if temporary else "") + + "TRIGGER synthetic_metadata AFTER UPDATE OF value ON main.metadata " + "WHEN new.key='style_vec_hash_version' BEGIN DELETE FROM entity_style_vectors; END") + conn.commit() + before, checkpoint = self.rows(conn), self.checkpoint(conn) + if borrowed: + conn.execute("INSERT INTO sentinel VALUES ('synthetic caller')") + changes = conn.total_changes + sql = [] + conn.set_trace_callback(sql.append) + result = MAINTENANCE.run_style_maintenance(conn, force=True, allow_caller_transaction=borrowed) + conn.set_trace_callback(None) + self.assertEqual((result.outcome, result.error_category, result.attempted), + ("deferred", "StyleTriggersUnsupported", False)) + self.assertFalse(any(statement.lstrip().upper().startswith( + ("INSERT", "UPDATE", "DELETE", "REPLACE", "BEGIN", "SAVEPOINT")) for statement in sql)) + self.assertEqual(conn.total_changes, changes) + self.assertEqual(self.rows(conn), before) + self.assertEqual(self.checkpoint(conn), checkpoint) + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], 1) + self.assertEqual(conn.in_transaction, borrowed) + self.assertEqual(conn.execute("SELECT * FROM sentinel").fetchall(), + [("synthetic caller",)] if borrowed else []) + conn.rollback() + self.assertEqual(conn.execute("SELECT * FROM sentinel").fetchall(), []) + self.assertEqual(self.rows(conn), before) + self.assertEqual(self.checkpoint(conn), checkpoint) + + def test_metadata_triggers_skip_incremental_publication_and_pending_diagnostics(self) -> None: + for temporary in (False, True): + with self.subTest(temporary=temporary): + conn = self.connection() + engine = self.engine(conn) + engine.add("synthetic baseline", sender="Synthetic") + before, checkpoint = self.rows(conn), self.checkpoint(conn) + conn.execute("CREATE " + ("TEMP " if temporary else "") + + "TRIGGER synthetic_metadata AFTER UPDATE OF value ON main.metadata " + "WHEN new.key='style_vec_hash_version' BEGIN DELETE FROM entity_style_vectors; END") + conn.commit() + updater = Mock(wraps=STYLE.update_entity_style_vector_incremental) + with patch.dict(ADD.__globals__, _update_style_vec=updater): + engine.add("synthetic retained", sender="Synthetic") + updater.assert_not_called() + self.assertEqual(self.rows(conn), before) + self.assertEqual(self.checkpoint(conn), checkpoint) + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], 2) + self.assertEqual(self.state(conn).dependency.error_category, "StyleTriggersUnsupported") + + def test_metadata_trigger_installed_during_compute_is_rejected_before_publication(self) -> None: + for temporary in (False, True): + with self.subTest(temporary=temporary): + conn = self.connection() + self.engine(conn).add("synthetic baseline", sender="Synthetic") + before, checkpoint = self.rows(conn), self.checkpoint(conn) + compute = STYLE._compute_entity_style_vectors + + def installed_after_compute(*args: object, **kwargs: object) -> object: + value = compute(*args, **kwargs) + conn.execute("CREATE " + ("TEMP " if temporary else "") + + "TRIGGER synthetic_late_metadata AFTER UPDATE OF value ON main.metadata " + "WHEN new.key='style_vec_hash_version' BEGIN DELETE FROM entity_style_vectors; END") + return value + + with patch.object(STYLE, "_compute_entity_style_vectors", installed_after_compute), \ + patch.object(STYLE, "_publish_style_vectors", wraps=STYLE._publish_style_vectors) as publish: + result = MAINTENANCE.run_style_maintenance(conn, force=True) + publish.assert_not_called() + self.assertEqual((result.outcome, result.error_category), ("deferred", "StyleTriggersUnsupported")) + self.assertEqual(self.rows(conn), before) + self.assertEqual(self.checkpoint(conn), checkpoint) + self.assertFalse(MAINTENANCE._style_extra_triggers(conn)) + self.assertFalse(conn.in_transaction) + + def test_source_trigger_newer_failure_is_not_overwritten_by_append_reporting(self) -> None: + conn = self.connection() + conn.executescript("""CREATE TRIGGER synthetic_checkpoint AFTER UPDATE OF outcome ON maintenance_layers + WHEN new.layer='style_vectors' AND new.outcome='pending' BEGIN + UPDATE maintenance_layers SET outcome='failed',error_category='NewerFailure', + run_generation='newer-owner',attempted_insert_count=999 WHERE layer='style_vectors'; END; + CREATE TRIGGER synthetic_source AFTER INSERT ON messages BEGIN + UPDATE maintenance_layers SET outcome='pending' WHERE layer='style_vectors'; END;""") + self.engine(conn).add("synthetic retained", sender="Synthetic") + state = self.state(conn) + self.assertEqual((state.outcome, state.error_category, state.run_generation, state.attempted_insert_count), + ("failed", "NewerFailure", "newer-owner", 999)) + self.assertEqual(self.rows(conn), []) + + def test_preparation_rejects_unsupported_triggers_before_enrollment_or_repair(self) -> None: + for bootstrap in (False, True): + for temporary in (False, True): + with self.subTest(bootstrap=bootstrap, temporary=temporary): + conn = self.connection(bootstrap=bootstrap) + if bootstrap: + conn.execute("DROP TRIGGER truememory_style_output_update") + conn.execute("CREATE " + ("TEMP " if temporary else "") + + "TRIGGER synthetic_preparation AFTER UPDATE ON main.maintenance_layers BEGIN " + "INSERT INTO sentinel VALUES ('unexpected checkpoint mutation'); END") + conn.commit() + before = self.checkpoint(conn) + changes = conn.total_changes + result = MAINTENANCE.run_style_maintenance(conn) + self.assertEqual((result.outcome, result.error_category), ("deferred", "StyleTriggersUnsupported")) + self.assertEqual(conn.total_changes, changes) + self.assertEqual(self.checkpoint(conn), before) + self.assertFalse(conn.in_transaction) + + def test_preparation_rechecks_trigger_admission_inside_its_original_writer_boundary(self) -> None: + for borrowed in (False, True): + conn = self.connection(bootstrap=False) + if borrowed: + conn.execute("INSERT INTO sentinel VALUES ('synthetic caller')") + installed = [] + + class InstallBeforeAdmission: + def __getattr__(self, name: str) -> object: + return getattr(conn, name) + + def execute(self, sql: str, *args: object) -> sqlite3.Cursor: + if sql in {"BEGIN IMMEDIATE", "SAVEPOINT truememory_style_tracking"} and not installed: + installed.append(True) + conn.execute("CREATE TEMP TRIGGER synthetic_preparation AFTER UPDATE ON main.maintenance_layers BEGIN " + "INSERT INTO sentinel VALUES ('unexpected checkpoint mutation'); END") + return conn.execute(sql, *args) + + result = MAINTENANCE.run_style_maintenance(InstallBeforeAdmission(), allow_caller_transaction=borrowed) + self.assertEqual(result.error_category, "StyleTriggersUnsupported") + self.assertEqual(installed, [True]) + self.assertEqual(conn.in_transaction, borrowed) + self.assertIsNone(self.checkpoint(conn)) + self.assertEqual(conn.execute("SELECT value FROM sentinel").fetchall(), + [("synthetic caller",)] if borrowed else []) + conn.rollback() + + def test_trigger_installed_after_baseline_cannot_fire_on_running_diagnostic(self) -> None: + conn = self.connection() + before = self.checkpoint(conn) + read = MAINTENANCE.read_layer_states + reads = [] + + def installed_after_read(*args: object, **kwargs: object) -> object: + state = read(*args, **kwargs) + reads.append(True) + if len(reads) == 2: + conn.execute("CREATE TEMP TRIGGER synthetic_late_checkpoint AFTER UPDATE ON main.maintenance_layers " + "BEGIN INSERT INTO sentinel VALUES ('unexpected diagnostic write'); END") + return state + + with patch.object(MAINTENANCE, "read_layer_states", installed_after_read), \ + patch.object(STYLE, "_compute_entity_style_vectors", wraps=STYLE._compute_entity_style_vectors) as builds: + result = MAINTENANCE.run_style_maintenance(conn, force=True) + builds.assert_not_called() + self.assertEqual((result.outcome, result.error_category), ("deferred", "StyleTriggersUnsupported")) + self.assertEqual(self.checkpoint(conn), before) + self.assertEqual(conn.execute("SELECT * FROM sentinel").fetchall(), []) + self.assertFalse(conn.in_transaction) + + def test_trigger_installed_after_rollback_cannot_fire_on_diagnostic_restoration(self) -> None: + conn = self.connection() + installed = [] + + class InstallAfterRollback: + def __getattr__(self, name: str) -> object: + return getattr(conn, name) + + def rollback(self) -> None: + conn.rollback() + if not installed: + installed.append(True) + conn.execute("CREATE TEMP TRIGGER synthetic_late_restore AFTER UPDATE ON main.maintenance_layers " + "BEGIN INSERT INTO sentinel VALUES ('unexpected restoration'); END") + + with patch.object(STYLE, "_compute_entity_style_vectors", + side_effect=MAINTENANCE.LayerDeferredError("synthetic busy", category="StyleWriterBusy")): + result = MAINTENANCE.run_style_maintenance(InstallAfterRollback(), force=True) + self.assertEqual((result.outcome, result.error_category), ("deferred", "StyleTriggersUnsupported")) + self.assertEqual(self.state(conn).outcome, "running") + self.assertEqual(conn.execute("SELECT * FROM sentinel").fetchall(), []) + self.assertFalse(conn.in_transaction) + conn.execute("DROP TRIGGER synthetic_late_restore") + self.assertEqual(MAINTENANCE.run_style_maintenance(conn).outcome, "success_empty") + + def test_tracked_output_admission_checks_triggers_before_any_profile_write(self) -> None: + conn = self.connection() + before = self.checkpoint(conn) + compute = STYLE._compute_entity_style_vectors + + def installed_after_compute(*args: object, **kwargs: object) -> object: + value = compute(*args, **kwargs) + conn.execute("CREATE TEMP TRIGGER synthetic_late_output AFTER DELETE ON main.entity_style_vectors " + "BEGIN INSERT INTO sentinel VALUES ('unexpected output publication'); END") + return value + + with patch.object(STYLE, "_compute_entity_style_vectors", installed_after_compute), \ + patch.object(STYLE, "_publish_style_vectors", wraps=STYLE._publish_style_vectors) as publication: + result = MAINTENANCE.run_style_maintenance(conn, force=True) + publication.assert_not_called() + self.assertEqual((result.outcome, result.error_category), ("deferred", "StyleTriggersUnsupported")) + self.assertEqual(self.checkpoint(conn), before) + self.assertEqual(conn.execute("SELECT * FROM sentinel").fetchall(), []) + self.assertFalse(conn.in_transaction) + + def test_unsupported_attempt_diagnostic_reports_deferral_without_writing(self) -> None: + for unavailable in (False, True): + with self.subTest(unavailable=unavailable): + conn = self.connection() + before = self.checkpoint(conn) + installed = [] + + def install() -> None: + if not installed: + installed.append(True) + conn.execute("CREATE TEMP TRIGGER synthetic_late_attempt AFTER UPDATE ON main.maintenance_layers " + "BEGIN INSERT INTO sentinel VALUES ('unexpected attempt write'); END") + + class InstallAfterFailure: + def __getattr__(self, name: str) -> object: + return getattr(conn, name) + + def rollback(self) -> None: + conn.rollback() + install() + + if unavailable: + conn.execute("DROP TRIGGER truememory_style_output_update") + spec = MAINTENANCE.style_layer_spec(conn) + read = MAINTENANCE.read_layer_states + reads = [] + + def installed_after_read(*args: object, **kwargs: object) -> object: + states = read(*args, **kwargs) + reads.append(True) + if len(reads) == 2: + install() + return states + + with patch.object(MAINTENANCE, "read_layer_states", installed_after_read): + result, = MAINTENANCE.run_layers(conn, (spec,), force=True) + self.assertEqual(self.checkpoint(conn), before) + self.assertFalse(result.attempted) + else: + with patch.object(STYLE, "_compute_entity_style_vectors", side_effect=ValueError("synthetic failure")): + result = MAINTENANCE.run_style_maintenance(InstallAfterFailure(), force=True) + self.assertEqual(self.state(conn).outcome, "running") + self.assertEqual(installed, [True]) + self.assertEqual((result.outcome, result.error_category), ("deferred", "StyleTriggersUnsupported")) + self.assertEqual(conn.execute("SELECT * FROM sentinel").fetchall(), []) + self.assertFalse(conn.in_transaction) + + def test_updater_metadata_and_checkpoint_mutations_rollback_only_style_work(self) -> None: + for change in ("hash", "ready_trigger", "extra_main", "extra_temp", "checkpoint_trigger", "checkpoint", "format", + "metadata_main", "metadata_temp"): + with self.subTest(change=change): + conn = self.connection() + engine = self.engine(conn) + engine.add("synthetic baseline", sender="Synthetic") + before = self.rows(conn) + publish = STYLE.update_entity_style_vector_incremental + + def changed(*args: object, **kwargs: object) -> None: + publish(*args, **kwargs) + if change == "hash": + conn.execute("UPDATE metadata SET value='1' WHERE key='style_vec_hash_version'") + elif change == "ready_trigger": + conn.execute("DROP TRIGGER truememory_style_output_insert") + elif change == "checkpoint": + conn.execute("UPDATE maintenance_layers SET outcome='failed',error_category='NewerFailure'," + "run_generation='newer-owner',attempted_insert_count=999 WHERE layer='style_vectors'") + elif change == "format": + conn.execute("ALTER TABLE entity_style_vectors ADD COLUMN synthetic_format TEXT") + else: + table = ("metadata" if change.startswith("metadata_") else + "maintenance_layers" if change == "checkpoint_trigger" else "entity_style_vectors") + conn.execute("CREATE " + ("TEMP " if change in {"extra_temp", "metadata_temp"} else "") + + "TRIGGER synthetic_during_append AFTER UPDATE ON main." + table + + " BEGIN INSERT INTO sentinel VALUES ('synthetic trigger'); END") + + with patch.dict(ADD.__globals__, _update_style_vec=changed): + engine.add("synthetic retained", sender="Synthetic") + self.assertEqual(self.rows(conn), before) + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], 2) + self.assertEqual(self.state(conn).outcome, "pending") + self.assertIsNone(self.state(conn).attempted_revision) + self.assertEqual(conn.execute("SELECT value FROM metadata WHERE key='style_vec_hash_version'").fetchone()[0], "2") + self.assertFalse(MAINTENANCE._style_extra_triggers(conn)) + self.assertTrue(STORAGE._style_output_tracking_ready(conn)) + + def test_post_success_metadata_and_provenance_changes_cannot_certify_publication(self) -> None: + for change in ("hash", "checkpoint", "extra_trigger"): + with self.subTest(change=change): + conn = self.connection() + engine = self.engine(conn) + engine.add("synthetic baseline", sender="Synthetic") + before = self.rows(conn) + record = MAINTENANCE.record_layer_success_in_transaction + + def changed(*args: object, **kwargs: object) -> None: + record(*args, **kwargs) + if change == "hash": + conn.execute("UPDATE metadata SET value='1' WHERE key='style_vec_hash_version'") + elif change == "checkpoint": + conn.execute("UPDATE maintenance_layers SET outcome='failed',error_category='NewerFailure'," + "run_generation='newer-owner',attempted_insert_count=999 WHERE layer='style_vectors'") + else: + conn.execute("CREATE TEMP TRIGGER synthetic_after_success AFTER INSERT ON main.entity_style_vectors " + "BEGIN DELETE FROM entity_style_vectors WHERE entity='synthetic'; END") + + with patch.object(MAINTENANCE, "record_layer_success_in_transaction", changed): + engine.add("synthetic retained", sender="Synthetic") + self.assertEqual(self.rows(conn), before) + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], 2) + self.assertEqual(self.state(conn).outcome, "pending") + self.assertIsNone(self.state(conn).attempted_revision) + + def test_pending_category_compare_and_swap_preserves_a_newer_whole_row(self) -> None: + conn = self.connection() + changed = [] + + class ChangeBeforeCategoryWrite: + def __getattr__(self, name: str) -> object: + return getattr(conn, name) + + def execute(self, sql: str, *args: object) -> sqlite3.Cursor: + if sql.startswith("UPDATE maintenance_layers SET error_category=") and not changed: + changed.append(True) + conn.execute("UPDATE maintenance_layers SET outcome='failed',error_category='NewerFailure'," + "run_generation='newer-owner',attempted_insert_count=999 WHERE layer='style_vectors'") + return conn.execute(sql, *args) + + with patch.object(STYLE, "compute_style_vector", side_effect=ValueError("synthetic precompute")): + self.engine(ChangeBeforeCategoryWrite()).add("synthetic retained", sender="Synthetic") + self.assertEqual(changed, [True]) + state = self.state(conn) + self.assertEqual((state.outcome, state.error_category, state.run_generation, state.attempted_insert_count), + ("failed", "NewerFailure", "newer-owner", 999)) + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], 1) + + def test_replayed_proof_never_counts_one_source_row_twice(self) -> None: + conn = self.connection() + conn.execute("BEGIN IMMEDIATE") + previous = MAINTENANCE._capture_style_append(conn) + self.assertEqual(self.append(conn, previous).outcome, "success") + before, checkpoint = self.rows(conn), self.checkpoint(conn) + result = self.append(conn, previous, message_id=1) + self.assertEqual((result.outcome, result.error_category), ("deferred", "StyleCoverageChanged")) + self.assertEqual(self.rows(conn), before) + self.assertEqual(self.checkpoint(conn), checkpoint) + conn.commit() + + def test_exact_append_rejects_correction_extra_insert_epoch_and_low_id_reuse(self) -> None: + for mode in ("correction", "extra", "epoch", "reuse", "wrong_id", "wrong_content", "counters", "nonappend"): + with self.subTest(mode=mode): + conn = self.connection() + self.engine(conn).add("synthetic baseline", sender="Synthetic") + before = self.rows(conn) + conn.execute("BEGIN IMMEDIATE") + previous = MAINTENANCE._capture_style_append(conn) + new_id = STORAGE.insert_message(conn, {"content": "synthetic ordinary", "sender": "Synthetic"}) + if mode == "correction": + conn.execute("UPDATE messages SET content='synthetic correction' WHERE id=1") + elif mode == "extra": + STORAGE.insert_message(conn, {"content": "synthetic extra", "sender": "Synthetic"}) + elif mode == "epoch": + conn.execute("UPDATE maintenance_source_state SET epoch='synthetic-new-epoch'") + elif mode == "reuse": + conn.execute("DELETE FROM messages WHERE id=1") + conn.execute("INSERT INTO messages(id,content,sender) VALUES (1,'synthetic replacement','Synthetic')") + elif mode == "counters": + conn.execute("UPDATE maintenance_source_state SET insert_count=insert_count+1") + elif mode == "nonappend": + conn.execute("UPDATE maintenance_source_state SET nonappend_revision=revision") + result = self.append(conn, previous, message_id=1 if mode == "wrong_id" else new_id, + content="synthetic different" if mode == "wrong_content" else "synthetic ordinary", + publish=lambda: self.fail("Unproven append reached updater")) + self.assertEqual(result.outcome, "deferred") + self.assertEqual(self.rows(conn), before) + conn.rollback() + + def test_unique_schema_and_insert_below_high_watermark_are_not_exact_appends(self) -> None: + for unique in (False, True): + conn = self.connection() + if unique: + conn.execute("CREATE UNIQUE INDEX synthetic_unique ON messages(content)") + conn.execute("BEGIN IMMEDIATE") + previous = MAINTENANCE._capture_style_append(conn) + conn.execute("INSERT INTO messages(id,content,sender) VALUES (?, 'synthetic ordinary','Synthetic')", + (1 if unique else 0,)) + result = self.append(conn, previous, message_id=1 if unique else 0, + publish=lambda: self.fail("Nonappend reached updater")) + self.assertEqual(result.error_category, "StyleAppendUnproven") + self.assertEqual(self.rows(conn), []) + conn.rollback() + + def test_newer_output_invalidation_survives_old_append_proof(self) -> None: + conn = self.connection() + conn.execute("BEGIN IMMEDIATE") + previous = MAINTENANCE._capture_style_append(conn) + new_id = STORAGE.insert_message(conn, {"content": "synthetic ordinary", "sender": "Synthetic"}) + conn.execute("INSERT INTO entity_style_vectors(entity,vector) VALUES ('other','[]')") + checkpoint = self.checkpoint(conn) + result = self.append(conn, previous, message_id=new_id, publish=lambda: self.fail("Stale proof")) + self.assertEqual(result.error_category, "StyleCoverageChanged") + self.assertEqual(self.checkpoint(conn), checkpoint) + conn.rollback() + + def test_running_worker_cannot_restore_over_newly_missed_foreground_append(self) -> None: + conn = self.connection() + before = self.checkpoint(conn) + with MAINTENANCE.maintenance_owner(None) as owner: + conn.execute("UPDATE maintenance_layers SET outcome='running',run_generation=? WHERE layer='style_vectors'", + (owner.generation,)) + conn.commit() + running = self.checkpoint(conn) + self.engine(conn).add("synthetic foreground append", sender="Synthetic") + invalidated = self.checkpoint(conn) + self.assertEqual(self.state(conn).outcome, "pending") + with self.assertRaises(MAINTENANCE.MaintenanceBusyError): + MAINTENANCE._restore_deferred_attempt(conn, "style_vectors", before, running, owner, + protect_rollback=True) + self.assertEqual(self.checkpoint(conn), invalidated) + self.assertEqual(MAINTENANCE.run_style_maintenance(conn).outcome, "success") + self.assertEqual(conn.execute("SELECT message_count FROM entity_style_vectors").fetchone()[0], 1) + + def test_publication_error_rolls_back_profile_but_keeps_source_and_pending(self) -> None: + for error in (ValueError("synthetic failure"), sqlite3.OperationalError("synthetic SQL failure"), + sqlite3.OperationalError("database is locked")): + with self.subTest(error=type(error).__name__, locked=str(error) == "database is locked"): + conn = self.connection() + engine = self.engine(conn) + engine.add("synthetic baseline", sender="Synthetic") + before = self.rows(conn) + publish = STYLE.update_entity_style_vector_incremental + + def failed(*args: object, **kwargs: object) -> None: + publish(*args, **kwargs) + raise error + + with patch.dict(ADD.__globals__, _update_style_vec=failed): + engine.add("synthetic retained", sender="Synthetic") + self.assertEqual(self.rows(conn), before) + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], 2) + state = self.state(conn) + self.assertEqual((state.outcome, state.successful_coverage, state.attempted_revision), + ("pending", "unverified", None)) + self.assertEqual(state.error_category, "StyleWriterBusy" if str(error) == "database is locked" else type(error).__name__) + + def test_source_mutation_inside_updater_is_rolled_back_without_erasing_append(self) -> None: + conn = self.connection() + publish = STYLE.update_entity_style_vector_incremental + + def changed(*args: object, **kwargs: object) -> None: + publish(*args, **kwargs) + conn.execute("UPDATE messages SET content='synthetic unapproved correction' WHERE id=1") + + with patch.dict(ADD.__globals__, _update_style_vec=changed): + self.engine(conn).add("synthetic retained", sender="Synthetic") + self.assertEqual(conn.execute("SELECT content FROM messages").fetchone()[0], "synthetic retained") + self.assertEqual(self.rows(conn), []) + self.assertEqual(self.state(conn).error_category, "StyleSourceChanged") + + def test_provenance_failure_rolls_back_the_successful_raw_sum_append(self) -> None: + conn = self.connection() + engine = self.engine(conn) + engine.add("synthetic baseline", sender="Synthetic") + before = self.rows(conn) + record = MAINTENANCE.record_layer_success_in_transaction + + def failed(*args: object, **kwargs: object) -> None: + record(*args, **kwargs) + raise sqlite3.OperationalError("synthetic provenance failure") + + with patch.object(MAINTENANCE, "record_layer_success_in_transaction", failed): + engine.add("synthetic retained", sender="Synthetic") + self.assertEqual(self.rows(conn), before) + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], 2) + self.assertEqual((self.state(conn).outcome, self.state(conn).attempted_revision), ("pending", None)) + + def test_failed_pending_write_reports_failure_without_claiming_durable_invalidation(self) -> None: + conn = self.connection() + conn.execute("BEGIN IMMEDIATE") + previous = MAINTENANCE._capture_style_append(conn) + checkpoint = self.checkpoint(conn) + new_id = STORAGE.insert_message(conn, {"content": "synthetic ordinary", "sender": "Synthetic"}) + with patch.object(MAINTENANCE, "_STYLE_OUTPUT_INVALIDATE_SQL", "synthetic invalid SQL"): + result = MAINTENANCE._publish_style_append( + conn, previous, new_id, sender="Synthetic", content="synthetic ordinary", directive=False, + precomputed=False, publish=lambda: self.fail("No precomputed vector"), + ) + self.assertEqual((result.outcome, result.error_category, result.coverage, result.pending_caller_commit), + ("failed", "OperationalError", "unverified", True)) + self.assertEqual(self.checkpoint(conn), checkpoint) + state = self.state(conn) + self.assertEqual(MAINTENANCE.layer_freshness(state.source, state, state.dependency), "append_pending") + conn.commit() + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], 1) + + def test_read_capture_failure_preserves_source_and_does_not_claim_success(self) -> None: + unusual_error = type("Synthetic Invalid Category", (sqlite3.OperationalError,), {}) + for error in (sqlite3.OperationalError("database table is locked: maintenance_layers"), + sqlite3.OperationalError("synthetic capture failure"), unusual_error("synthetic detail")): + conn = self.connection() + read = MAINTENANCE.read_layer_states + failures = [] + + def unavailable(*args: object, **kwargs: object) -> object: + if not failures: + failures.append(True) + raise error + return read(*args, **kwargs) + + with patch.object(MAINTENANCE, "read_layer_states", unavailable): + self.engine(conn).add("synthetic retained", sender="Synthetic") + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], 1) + self.assertEqual(self.rows(conn), []) + self.assertEqual(self.state(conn).successful_coverage, "unverified") + self.assertRegex(self.state(conn).error_category, r"\A[A-Za-z][A-Za-z0-9_]{0,63}\Z") + + def test_borrowed_append_success_and_failure_remain_in_caller_transaction(self) -> None: + for failed in (False, True): + conn = self.connection() + before = self.checkpoint(conn) + conn.execute("INSERT INTO sentinel VALUES ('synthetic pending')") + updater = STYLE.update_entity_style_vector_incremental + with patch.dict(ADD.__globals__, _update_style_vec=(lambda *args, **kwargs: (_ for _ in ()).throw(ValueError("synthetic"))) + if failed else updater): + self.engine(conn).add("synthetic caller source", sender="Synthetic") + self.assertTrue(conn.in_transaction) + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], 1) + conn.rollback() + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], 0) + self.assertEqual(conn.execute("SELECT count(*) FROM sentinel").fetchone()[0], 0) + self.assertEqual(self.rows(conn), []) + self.assertEqual(self.checkpoint(conn), before) + + def test_cached_source_commit_and_caller_release_faults_restore_whole_add(self) -> None: + for cache in (0, 5, 100): + for borrowed in (False, True): + with self.subTest(cache=cache, borrowed=borrowed): + conn = self.connection(cache=cache) + if borrowed: + conn.execute("BEGIN") + self.engine(conn).add("synthetic warm release", sender="Synthetic") + conn.rollback() + before = self.checkpoint(conn) + source = MAINTENANCE.read_source_revision(conn) + if borrowed: + conn.execute("INSERT INTO sentinel VALUES ('synthetic caller')") + fault = BoundaryFailure(conn, "caller_release" if borrowed else "commit") + with self.assertRaises(sqlite3.DatabaseError): + self.engine(fault).add("synthetic rejected publication", sender="Synthetic") + self.assertEqual(fault.denied, 1) + self.assertEqual(conn.in_transaction, borrowed) + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], 0) + self.assertEqual(conn.execute("SELECT count(*) FROM sentinel").fetchone()[0], int(borrowed)) + self.assertEqual(self.rows(conn), []) + self.assertEqual(self.checkpoint(conn), before) + self.assertEqual(MAINTENANCE.read_source_revision(conn), source) + + def test_style_savepoint_release_fault_preserves_add_and_invalidates_coverage(self) -> None: + for cache in (0, 5, 100): + conn = self.connection(cache=cache) + fault = BoundaryFailure(conn, "style_release") + self.engine(fault).add("synthetic retained", sender="Synthetic") + self.assertEqual(fault.denied, 1) + self.assertFalse(conn.in_transaction) + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], 1) + self.assertEqual(self.rows(conn), []) + self.assertEqual((self.state(conn).outcome, self.state(conn).attempted_revision), ("pending", None)) + + def test_rejected_rollback_is_not_swallowed_as_successful_source_add(self) -> None: + conn = self.connection() + + class RefuseRollback: + def __getattr__(self, name: str) -> object: + return getattr(conn, name) + + def execute(self, sql: str, *args: object) -> sqlite3.Cursor: + if sql.startswith("ROLLBACK TO truememory_style_coverage_"): + raise sqlite3.OperationalError("synthetic rollback refused") + return conn.execute(sql, *args) + + with patch.dict(ADD.__globals__, _update_style_vec=lambda *args, **kwargs: (_ for _ in ()).throw(ValueError("synthetic"))): + with self.assertRaises(MAINTENANCE._LayerRollbackFailed): + self.engine(RefuseRollback()).add("synthetic failed source", sender="Synthetic") + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], 0) + self.assertEqual(self.rows(conn), []) + + def test_cleanup_release_failure_preserves_borrowed_caller_and_rejects_add(self) -> None: + conn = self.connection() + before = self.checkpoint(conn) + conn.execute("INSERT INTO sentinel VALUES ('synthetic caller')") + + class RefuseRelease: + def __getattr__(self, name: str) -> object: + return getattr(conn, name) + + def execute(self, sql: str, *args: object) -> sqlite3.Cursor: + if sql.startswith("RELEASE truememory_style_coverage_"): + raise sqlite3.OperationalError("synthetic release refused") + return conn.execute(sql, *args) + + with self.assertRaises(MAINTENANCE._LayerRollbackFailed): + self.engine(RefuseRelease()).add("synthetic rejected", sender="Synthetic") + self.assertTrue(conn.in_transaction) + self.assertEqual(conn.execute("SELECT count(*) FROM sentinel").fetchone()[0], 1) + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], 0) + self.assertEqual(self.rows(conn), []) + self.assertEqual(self.checkpoint(conn), before) + conn.rollback() + + def test_ended_transaction_cannot_be_reported_as_a_safe_style_failure(self) -> None: + conn = self.connection() + + def ended(*args: object, **kwargs: object) -> None: + conn.rollback() + raise sqlite3.OperationalError("database is locked") + + with patch.dict(ADD.__globals__, _update_style_vec=ended): + with self.assertRaises(MAINTENANCE._LayerRollbackFailed): + self.engine(conn).add("synthetic rejected", sender="Synthetic") + self.assertFalse(conn.in_transaction) + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], 0) + + def test_add_queries_use_only_indexed_source_and_profile_point_reads(self) -> None: + conn = self.connection() + statements = [] + conn.set_trace_callback(statements.append) + self.engine(conn).add("synthetic bounded", sender="Synthetic") + conn.set_trace_callback(None) + reads = [sql for sql in statements if sql.startswith("SELECT") and + ("FROM messages " in sql or "FROM entity_style_vectors " in sql)] + self.assertEqual(len(reads), 3) + for sql in reads: + self.assertIn(" WHERE ", sql) + plans = conn.execute("EXPLAIN QUERY PLAN " + sql).fetchall() + self.assertTrue(all("SEARCH" in row[3] for row in plans), plans) + self.assertFalse(any("COUNT(" in sql.upper() or "MAX(" in sql.upper() for sql in statements if sql.startswith("SELECT"))) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-l0-style-maintenance-747.py b/tests/test-l0-style-maintenance-747.py new file mode 100644 index 00000000..5849674e --- /dev/null +++ b/tests/test-l0-style-maintenance-747.py @@ -0,0 +1,965 @@ +"""Private style maintenance with stdlib source and synthetic in-memory SQLite.""" + +import ast +import builtins +import json +import runpy +import sqlite3 +import sys +import threading +import types +import unittest +import uuid +from pathlib import Path +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] +ACCUMULATOR = runpy.run_path(str(Path(__file__).with_name("test-l0-style-accumulator-747.py"))) +STYLE, STORAGE = ACCUMULATOR["STYLE"], ACCUMULATOR["STORAGE"] + + +def load_maintenance() -> types.ModuleType: + modules = {"truememory.storage": STORAGE, + "truememory._platform": ACCUMULATOR["load_stdlib_source"]("_platform")} + + def safe_import(name: str, globals: dict | None = None, locals: dict | None = None, + fromlist: tuple = (), level: int = 0) -> object: + if name in modules: + return modules[name] + if name.split(".", 1)[0] not in sys.stdlib_module_names: + raise AssertionError("Non-stdlib import forbidden: " + name) + return builtins.__import__(name, globals, locals, fromlist, level) + + def load_source(name: str) -> types.ModuleType: + loaded = types.ModuleType("synthetic_style_boundary_" + name.replace("/", "_")) + loaded.__dict__["__builtins__"] = dict(vars(builtins), __import__=safe_import) + source = ROOT / "truememory" / (name + ".py") + missing = object() + previous = sys.modules.get(loaded.__name__, missing) + sys.modules[loaded.__name__] = loaded + try: + exec(compile(source.read_text(encoding="utf-8"), str(source), "exec"), loaded.__dict__) + finally: + if previous is missing: + sys.modules.pop(loaded.__name__, None) + else: + sys.modules[loaded.__name__] = previous + modules["truememory." + name.replace("/", ".")] = loaded + return loaded + + module = load_source("maintenance") + modules["psutil"] = types.SimpleNamespace() + for name in ("rebuild_source", "embedding_target", "tier_config", "tier_switch/cache", + "tier_switch/job", "tier_switch/source", "tier_switch/activation", "tier_switch/serving", + "tier_switch/writer"): + load_source(name) + modules["truememory.tier_switch"] = types.SimpleNamespace(serving=modules["truememory.tier_switch.serving"]) + vector = types.ModuleType("synthetic_style_vector_identity") + vector.__dict__.update(__builtins__=dict(vars(builtins), __import__=safe_import), sqlite3=sqlite3, + EMBEDDING_MODEL="model2vec", _embedding_dim=256, _model=None, _lock=threading.Lock(), + _frozen_embedding_target=None, _runtime_policy_tier=None, resolve_tier=lambda: "edge", + _cfg_get_model_group=modules["truememory.tier_config"].get_model_group) + tree = ast.parse((ROOT / "truememory/vector_search.py").read_text(encoding="utf-8")) + resolvers = [node for node in tree.body if isinstance(node, ast.FunctionDef) + and node.name in {"_active_tier_group", "_active_vec_table", "_active_sep_table"}] + exec(compile(ast.Module(body=resolvers, type_ignores=[]), "actual-style-vector-resolvers", "exec"), vector.__dict__) + modules["truememory.vector_search"] = vector + modules["truememory"] = types.SimpleNamespace(vector_search=vector, + reranker=types.SimpleNamespace(_model=None, get_current_reranker_name=lambda: "synthetic/reranker")) + runtime = load_source("tier_switch/runtime") + runtime.sys = types.SimpleNamespace(modules=modules) + module._fixture_modules = modules + + def import_module(name: str) -> types.ModuleType: + if name != "truememory.personality_style_vec": + raise AssertionError("Unexpected style dependency: " + name) + return STYLE + + module.importlib = types.SimpleNamespace(import_module=import_module) + return module + + +MAINTENANCE = load_maintenance() + + +class PublicationFailure: + """Invalidate cached statements before denying one real COMMIT or RELEASE.""" + + def __init__(self, conn: sqlite3.Connection, *, borrowed: bool) -> None: + self.conn, self.borrowed = conn, borrowed + self.matches = 0 + self.denied = 0 + + def __getattr__(self, name: str) -> object: + return getattr(self.conn, name) + + def execute(self, sql: str, *args: object) -> sqlite3.Cursor: + match = sql.startswith("RELEASE truememory_layer_") if self.borrowed else sql == "COMMIT" + if match: + self.matches += 1 + if self.matches == (1 if self.borrowed else 2): + self.denied += 1 + denied = ((sqlite3.SQLITE_SAVEPOINT, "RELEASE", sql.split()[1]) if self.borrowed + else (sqlite3.SQLITE_TRANSACTION, "COMMIT", None)) + + def authorize(action: int, one: str | None, two: str | None, *_rest: object) -> int: + return sqlite3.SQLITE_DENY if (action, one, two) == denied else sqlite3.SQLITE_OK + + self.conn.set_authorizer(authorize) + try: + return self.conn.execute(sql, *args) + finally: + self.conn.set_authorizer(ACCUMULATOR["allow_authorized_action"]) + return self.conn.execute(sql, *args) + + +class LegacySqliteModule: + """Python 3.10 lacks these primary result-code exports.""" + + def __getattr__(self, name: str) -> object: + if name in {"SQLITE_BUSY", "SQLITE_LOCKED", "SQLITE_INTERRUPT"}: + raise AttributeError(name) + return getattr(sqlite3, name) + + +class StyleMaintenanceTests(unittest.TestCase): + def connection(self, cache: int = 100, *, uri: str | None = None) -> sqlite3.Connection: + conn = sqlite3.connect(uri or ":memory:", uri=uri is not None, timeout=0, cached_statements=cache) + self.addCleanup(conn.close) + conn.executescript(STORAGE._SCHEMA_SQL) + STORAGE._initialize_maintenance_tracking(conn) + conn.execute("CREATE TABLE IF NOT EXISTS sentinel(value TEXT)") + conn.commit() + return conn + + def add(self, conn: sqlite3.Connection, text: str = "synthetic source", *, sender: str = "Synthetic", + directive: int = 0) -> None: + conn.execute("INSERT INTO messages(sender,content,timestamp,directive) VALUES (?,?,'2026-01-01',?)", + (sender, text, directive)) + conn.commit() + + def state(self, conn: sqlite3.Connection) -> object: + spec = MAINTENANCE.style_layer_spec(conn) + parameters = json.loads(spec.resolve_dependency().key)["parameters"] + self.assertEqual(parameters, {"accumulator_version": STYLE._STYLE_ACCUMULATOR_VERSION, + "hash_version": 2, "dimension": STYLE.DIM, "ngrams": list(STYLE.NGRAM_SIZES), + "sum_format": STYLE._STYLE_SUM.format}) + return MAINTENANCE.read_layer_states(conn, (spec,))["style_vectors"] + + def output(self, conn: sqlite3.Connection) -> list[tuple]: + return conn.execute("SELECT * FROM entity_style_vectors ORDER BY entity").fetchall() + + def checkpoint(self, conn: sqlite3.Connection) -> tuple | None: + return MAINTENANCE._read_layer_row(conn, "style_vectors") + + def test_spec_and_ordinary_schema_do_not_activate_or_scan(self) -> None: + conn = self.connection() + source = MAINTENANCE.read_source_revision(conn) + before = conn.total_changes + + def authorize(action: int, table: str | None, *_rest: object) -> int: + if action == sqlite3.SQLITE_READ and table in {"messages", "entity_style_vectors"}: + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + + conn.set_authorizer(authorize) + spec = MAINTENANCE.style_layer_spec(conn) + state = MAINTENANCE.read_layer_states(conn, (spec,))[spec.layer] + self.assertEqual(state.successful_coverage, "unverified") + self.assertEqual(conn.total_changes, before) + self.assertEqual(len(STORAGE._MAINTENANCE_LAYERS), 8) + self.assertEqual(len(MAINTENANCE.all_layer_specs(conn)), 8) + self.assertEqual(len(MAINTENANCE.maintenance_busy_result()), 9) + self.assertEqual(conn.execute("SELECT count(*) FROM maintenance_layers").fetchone()[0], 8) + MAINTENANCE.prepare_style_maintenance(conn) + self.assertEqual(MAINTENANCE.read_source_revision(conn), source) + self.assertEqual(conn.execute("SELECT count(*) FROM maintenance_layers").fetchone()[0], 9) + self.assertIsNone(self.state(conn).successful_revision) + before = conn.total_changes + MAINTENANCE.prepare_style_maintenance(conn) + self.assertEqual(conn.total_changes, before) + conn.set_authorizer(ACCUMULATOR["allow_authorized_action"]) + + def test_preparation_is_owned_or_part_of_caller_transaction(self) -> None: + for borrowed in (False, True): + conn = self.connection() + if borrowed: + conn.execute("INSERT INTO sentinel VALUES ('synthetic pending')") + MAINTENANCE.prepare_style_maintenance(conn) + self.assertEqual(conn.in_transaction, borrowed) + if borrowed: + conn.rollback() + self.assertIsNone(self.checkpoint(conn)) + self.assertEqual(conn.execute("SELECT count(*) FROM sqlite_master WHERE name LIKE 'truememory_style_output_%'").fetchone()[0], 0) + else: + self.assertIsNotNone(self.checkpoint(conn)) + + def test_cached_preparation_commit_and_release_faults_do_not_enroll(self) -> None: + for cache in (0, 5, 100): + for borrowed in (False, True): + with self.subTest(cache=cache, borrowed=borrowed): + conn = self.connection(cache) + if borrowed: + conn.execute("INSERT INTO sentinel VALUES ('synthetic pending')") + sql = "RELEASE SAVEPOINT truememory_style_tracking" if borrowed else "COMMIT" + action = ((sqlite3.SQLITE_SAVEPOINT, "RELEASE", "truememory_style_tracking") if borrowed + else (sqlite3.SQLITE_TRANSACTION, "COMMIT", None)) + probe = ACCUMULATOR["StatementFailure"](conn, sql, action) + with self.assertRaises(sqlite3.DatabaseError): + MAINTENANCE.prepare_style_maintenance(probe) + self.assertEqual(probe.armed, [sql]) + self.assertIsNone(self.checkpoint(conn)) + self.assertEqual(conn.execute("SELECT count(*) FROM sqlite_master WHERE name LIKE 'truememory_style_output_%'").fetchone()[0], 0) + self.assertEqual(conn.in_transaction, borrowed) + self.assertEqual(conn.execute("SELECT count(*) FROM sentinel").fetchone()[0], int(borrowed)) + conn.rollback() + + def test_batch_arithmetic_case_zero_directive_and_hash_are_unchanged(self) -> None: + conn = self.connection() + for sender, text, directive in (("Synthetic", "abc", 0), ("synthetic", "a", 0), + ("Synthetic", "", 0), ("Other", "ignored", 1), ("", "ignored", 0)): + self.add(conn, text, sender=sender, directive=directive) + expected = STYLE.build_entity_style_vectors(conn) + baseline = conn.execute("SELECT entity,vector,message_count,vector_sum,accumulator_version FROM entity_style_vectors").fetchall() + token = MAINTENANCE.read_source_revision(conn) + public_rows = conn.execute("SELECT * FROM maintenance_layers ORDER BY layer").fetchall() + result = MAINTENANCE.run_style_maintenance(conn) + self.assertEqual((result.outcome, result.output_count, result.coverage), ("success", 1, "complete")) + self.assertEqual(conn.execute("SELECT entity,vector,message_count,vector_sum,accumulator_version FROM entity_style_vectors").fetchall(), baseline) + self.assertEqual(STYLE.get_entity_style_vector(conn, "SYNTHETIC"), expected["synthetic"]) + self.assertEqual(conn.execute("SELECT value FROM metadata WHERE key='style_vec_hash_version'").fetchone(), ("2",)) + self.assertEqual(MAINTENANCE.read_source_revision(conn), token) + self.assertEqual(conn.execute("SELECT * FROM maintenance_layers WHERE layer!='style_vectors' ORDER BY layer").fetchall(), public_rows) + self.assertFalse(MAINTENANCE.run_style_maintenance(conn).attempted) + self.assertEqual(self.state(conn).attempted_insert_count, 5) + + def test_empty_success_retirement_and_source_corrections(self) -> None: + for statement in ("UPDATE messages SET content='synthetic changed'", + "UPDATE messages SET sender='Other'", "UPDATE messages SET directive=1", "DELETE FROM messages"): + with self.subTest(statement=statement): + conn = self.connection() + self.add(conn) + MAINTENANCE.run_style_maintenance(conn) + conn.execute(statement) + conn.commit() + self.assertEqual(len(MAINTENANCE.plan_layers(conn, (MAINTENANCE.style_layer_spec(conn),))), 1) + result = MAINTENANCE.run_style_maintenance(conn) + self.assertIn(result.outcome, {"success", "success_empty"}) + self.assertEqual(self.state(conn).successful_revision, MAINTENANCE.read_source_revision(conn).revision) + empty = self.connection() + result = MAINTENANCE.run_style_maintenance(empty) + self.assertEqual((result.outcome, result.output_count), ("success_empty", 0)) + self.assertEqual(self.state(empty).attempted_insert_count, 0) + + def test_output_changes_invalidate_failed_baseline_and_rollback_atomically(self) -> None: + for statement in ("UPDATE entity_style_vectors SET message_count=999", + "DELETE FROM entity_style_vectors", + "INSERT INTO entity_style_vectors(entity,vector) VALUES ('other','[]')"): + with self.subTest(statement=statement): + conn = self.connection() + self.add(conn) + MAINTENANCE.run_style_maintenance(conn) + with patch.object(STYLE, "compute_style_vector", side_effect=ValueError("synthetic")): + self.assertEqual(MAINTENANCE.run_style_maintenance(conn, force=True).outcome, "failed") + self.assertEqual(MAINTENANCE.plan_layers(conn, (MAINTENANCE.style_layer_spec(conn),)), ()) + old, token = self.checkpoint(conn), MAINTENANCE.read_source_revision(conn) + conn.execute(statement) + self.assertIsNone(self.state(conn).attempted_revision) + self.assertTrue(self.state(conn).full_rebuild_required) + self.assertEqual(MAINTENANCE.read_source_revision(conn), token) + conn.rollback() + self.assertEqual(self.checkpoint(conn), old) + conn.execute(statement) + conn.commit() + self.assertEqual(len(MAINTENANCE.plan_layers(conn, (MAINTENANCE.style_layer_spec(conn),))), 1) + self.assertEqual(MAINTENANCE.run_style_maintenance(conn).outcome, "success") + + def test_trigger_repair_is_metadata_only_and_does_not_certify_v1_rows(self) -> None: + conn = self.connection() + self.add(conn) + MAINTENANCE.run_style_maintenance(conn) + conn.execute("DROP TRIGGER truememory_style_output_update") + conn.execute("CREATE TRIGGER truememory_style_output_update AFTER UPDATE ON entity_style_vectors BEGIN SELECT 1; END") + conn.commit() + output, source = self.output(conn), MAINTENANCE.read_source_revision(conn) + + def authorize(action: int, table: str | None, *_rest: object) -> int: + return sqlite3.SQLITE_DENY if action == sqlite3.SQLITE_READ and table in {"messages", "entity_style_vectors"} else sqlite3.SQLITE_OK + + conn.set_authorizer(authorize) + MAINTENANCE.prepare_style_maintenance(conn) + self.assertIsNone(self.state(conn).attempted_revision) + self.assertTrue(self.state(conn).full_rebuild_required) + conn.set_authorizer(ACCUMULATOR["allow_authorized_action"]) + self.assertEqual(self.output(conn), output) + self.assertEqual(MAINTENANCE.read_source_revision(conn), source) + + def test_future_custom_format_is_retained_and_low_level_still_has_its_old_contract(self) -> None: + conn = self.connection() + self.add(conn) + conn.execute("INSERT INTO entity_style_vectors VALUES ('synthetic','[1]',9,'synthetic old',X'1234',9)") + conn.execute("INSERT INTO metadata(key,value) VALUES ('style_vec_hash_version','1')") + conn.commit() + before = self.output(conn) + result = MAINTENANCE.run_style_maintenance(conn) + self.assertEqual((result.outcome, result.error_category), ("unavailable", "StyleAccumulatorUnsupported")) + self.assertEqual(self.output(conn), before) + self.assertEqual(conn.execute("SELECT value FROM metadata WHERE key='style_vec_hash_version'").fetchone(), ("1",)) + self.assertIsNone(self.state(conn).successful_revision) + self.assertFalse(MAINTENANCE.run_style_maintenance(conn).attempted) + STYLE.build_entity_style_vectors(conn) + self.assertEqual(conn.execute("SELECT accumulator_version FROM entity_style_vectors").fetchone(), (1,)) + self.assertIsNone(self.state(conn).attempted_revision) + + def test_custom_future_default_is_guarded_even_without_rows(self) -> None: + for with_source in (False, True): + with self.subTest(with_source=with_source): + conn = self.connection() + conn.execute("DROP TABLE entity_style_vectors") + conn.execute("CREATE TABLE entity_style_vectors(entity TEXT PRIMARY KEY,vector TEXT,message_count INTEGER," + "updated_at TEXT,vector_sum BLOB,accumulator_version INTEGER NOT NULL DEFAULT 9)") + conn.commit() + if with_source: + self.add(conn) + schema = conn.execute("PRAGMA table_info(entity_style_vectors)").fetchall() + result = MAINTENANCE.run_style_maintenance(conn) + self.assertEqual((result.outcome, result.error_category), ("unavailable", "StyleAccumulatorUnsupported")) + self.assertEqual(conn.execute("PRAGMA table_info(entity_style_vectors)").fetchall(), schema) + self.assertEqual(self.output(conn), []) + self.assertIsNone(conn.execute("SELECT value FROM metadata WHERE key='style_vec_hash_version'").fetchone()) + + def test_missing_output_trigger_cannot_certify_through_an_unprepared_spec(self) -> None: + conn = self.connection() + self.add(conn) + MAINTENANCE.run_style_maintenance(conn) + before = self.output(conn) + conn.execute("DROP TRIGGER truememory_style_output_update") + conn.commit() + spec = MAINTENANCE.style_layer_spec(conn) + self.assertFalse(spec.resolve_dependency().available) + result = MAINTENANCE.run_layers(conn, (spec,), force=True)[0] + self.assertEqual((result.outcome, result.error_category), ("unavailable", "StyleTrackingUnprepared")) + self.assertEqual(self.output(conn), before) + self.assertEqual(MAINTENANCE.run_style_maintenance(conn).outcome, "success") + + def test_borrowed_success_remains_pending_and_outer_rollback_restores_everything(self) -> None: + conn = self.connection() + self.add(conn) + MAINTENANCE.run_style_maintenance(conn) + before, checkpoint = self.output(conn), self.checkpoint(conn) + conn.execute("INSERT INTO sentinel VALUES ('synthetic caller')") + conn.execute("UPDATE messages SET content='synthetic changed'") + result = MAINTENANCE.run_style_maintenance(conn, allow_caller_transaction=True) + self.assertTrue(result.pending_caller_commit) + self.assertEqual(result.outcome, "success") + self.assertTrue(conn.in_transaction) + conn.rollback() + self.assertEqual(self.output(conn), before) + self.assertEqual(self.checkpoint(conn), checkpoint) + self.assertEqual(conn.execute("SELECT count(*) FROM sentinel").fetchone()[0], 0) + + def test_legacy_and_invalid_sums_rebuild_from_source_without_double_counting(self) -> None: + for version, total in ((0, None), (1, b"synthetic invalid sum")): + with self.subTest(version=version): + conn = self.connection() + self.add(conn, "synthetic first") + self.add(conn, "synthetic second") + conn.execute("INSERT INTO entity_style_vectors VALUES ('synthetic','[1]',99,'synthetic old',?,?)", + (total, version)) + conn.commit() + self.assertEqual(STYLE.get_entity_style_vector(conn, "Synthetic"), [1]) + self.assertEqual(MAINTENANCE.run_style_maintenance(conn).outcome, "success") + self.assertEqual(conn.execute("SELECT message_count,accumulator_version,length(vector_sum) FROM entity_style_vectors").fetchone(), (2, 1, 2048)) + self.assertEqual(self.state(conn).attempted_insert_count, 2) + + def test_cached_commit_and_release_faults_retain_prior_output(self) -> None: + for cache in (0, 5, 100): + for borrowed in (False, True): + with self.subTest(cache=cache, borrowed=borrowed): + conn = self.connection(cache) + self.add(conn) + MAINTENANCE.run_style_maintenance(conn) + before, prior = self.output(conn), self.state(conn) + conn.execute("UPDATE metadata SET value='1' WHERE key='style_vec_hash_version'") + conn.commit() + if borrowed: + conn.execute("INSERT INTO sentinel VALUES ('synthetic caller')") + probe = PublicationFailure(conn, borrowed=borrowed) + result = MAINTENANCE.run_style_maintenance(probe, force=True, allow_caller_transaction=borrowed) + self.assertEqual((result.outcome, probe.denied), ("failed", 1)) + self.assertEqual(self.output(conn), before) + self.assertEqual(self.state(conn).successful_revision, prior.successful_revision) + self.assertEqual(conn.execute("SELECT value FROM metadata WHERE key='style_vec_hash_version'").fetchone(), ("1",)) + self.assertEqual(conn.in_transaction, borrowed) + if borrowed: + self.assertEqual(conn.execute("SELECT count(*) FROM sentinel").fetchone()[0], 1) + conn.rollback() + + def test_verified_source_change_defers_without_consuming_attempt(self) -> None: + conn = self.connection() + self.add(conn) + MAINTENANCE.run_style_maintenance(conn) + before, checkpoint = self.output(conn), self.checkpoint(conn) + compute = STYLE.compute_style_vector + + def changed(text: str) -> list[float]: + conn.execute("UPDATE messages SET recipient='synthetic changed'") + return compute(text) + + with patch.object(STYLE, "compute_style_vector", changed): + result = MAINTENANCE.run_style_maintenance(conn, force=True) + self.assertEqual((result.outcome, result.error_category), ("deferred", "StyleSourceChanged")) + self.assertEqual(self.output(conn), before) + self.assertEqual(self.checkpoint(conn), checkpoint) + self.assertFalse(conn.in_transaction) + + def test_cancellation_during_compute_and_after_writer_retains_generation(self) -> None: + for phase in ("before", "compute", "writer"): + with self.subTest(phase=phase): + conn = self.connection() + self.add(conn) + MAINTENANCE.run_style_maintenance(conn) + before, checkpoint = self.output(conn), self.checkpoint(conn) + cancel = threading.Event() + compute = STYLE.compute_style_vector + + def cancelled(text: str) -> list[float]: + if phase == "compute": + cancel.set() + return compute(text) + + def trace(sql: str) -> None: + if phase == "writer" and sql == "DELETE FROM entity_style_vectors": + cancel.set() + + if phase == "before": + cancel.set() + conn.set_trace_callback(trace) + with patch.object(STYLE, "compute_style_vector", cancelled): + result = MAINTENANCE.run_style_maintenance(conn, force=True, cancel=cancel, connection_owned=True) + conn.set_trace_callback(None) + self.assertEqual((result.outcome, result.error_category), ("deferred", "StyleCancelled")) + self.assertEqual(self.output(conn), before) + self.assertEqual(self.checkpoint(conn), checkpoint) + self.assertFalse(conn.in_transaction) + + def test_sql_capture_cancellation_disables_handler_before_rollback(self) -> None: + conn = self.connection() + self.add(conn) + MAINTENANCE.run_style_maintenance(conn) + before, checkpoint = self.output(conn), self.checkpoint(conn) + cancel = threading.Event() + + class ProgressBoundary: + def __getattr__(boundary, name: str) -> object: + return getattr(conn, name) + + def set_progress_handler(boundary, handler: object, steps: int) -> None: + conn.set_progress_handler(handler, 1 if handler is not None else 0) + + def execute(boundary, sql: str, *args: object) -> sqlite3.Cursor: + if sql.startswith("SELECT sender, content, timestamp"): + cancel.set() + return conn.execute(sql, *args) + + result = MAINTENANCE.run_style_maintenance(ProgressBoundary(), force=True, cancel=cancel, connection_owned=True) + self.assertEqual((result.outcome, result.error_category), ("deferred", "StyleCancelled")) + self.assertFalse(conn.in_transaction) + self.assertEqual(self.output(conn), before) + self.assertEqual(self.checkpoint(conn), checkpoint) + + def test_borrowed_progress_handler_is_never_replaced(self) -> None: + conn = self.connection() + self.add(conn) + MAINTENANCE.prepare_style_maintenance(conn) + conn.execute("INSERT INTO sentinel VALUES ('synthetic caller')") + calls = [] + conn.set_progress_handler(lambda: calls.append(1) or 0, 1) + + class BorrowedBoundary: + def __getattr__(boundary, name: str) -> object: + return getattr(conn, name) + + def set_progress_handler(boundary, *_args: object) -> None: + raise AssertionError("Borrowed handler must remain untouched") + + result = MAINTENANCE.run_style_maintenance(BorrowedBoundary(), cancel=threading.Event(), + allow_caller_transaction=True, connection_owned=True) + self.assertEqual(result.outcome, "success") + self.assertTrue(calls) + before = len(calls) + conn.execute("SELECT count(*) FROM messages").fetchone() + self.assertGreater(len(calls), before) + conn.set_progress_handler(None, 0) + conn.rollback() + + def test_clean_caller_connection_keeps_its_unknown_progress_handler_by_default(self) -> None: + conn = self.connection() + self.add(conn) + calls = [] + conn.set_progress_handler(lambda: calls.append(1) or 0, 1) + + class CallerBoundary: + def __getattr__(boundary, name: str) -> object: + return getattr(conn, name) + + def set_progress_handler(boundary, *_args: object) -> None: + raise AssertionError("Clean transaction does not prove connection ownership") + + result = MAINTENANCE.run_style_maintenance(CallerBoundary(), cancel=threading.Event()) + self.assertEqual(result.outcome, "success") + before = len(calls) + conn.execute("SELECT count(*) FROM messages").fetchone() + self.assertGreater(len(calls), before) + conn.set_progress_handler(None, 0) + + def test_refused_rollback_never_restores_a_deferred_checkpoint(self) -> None: + conn = self.connection() + self.add(conn) + MAINTENANCE.run_style_maintenance(conn) + cancel = threading.Event() + compute = STYLE.compute_style_vector + + def cancelled(text: str) -> list[float]: + cancel.set() + return compute(text) + + class RefusedRollback: + def __getattr__(boundary, name: str) -> object: + return getattr(conn, name) + + def rollback(boundary) -> None: + raise sqlite3.OperationalError("synthetic rollback refusal") + + with patch.object(STYLE, "compute_style_vector", cancelled): + with self.assertRaises(MAINTENANCE._LayerRollbackFailed): + MAINTENANCE.run_style_maintenance(RefusedRollback(), force=True, cancel=cancel) + self.assertTrue(conn.in_transaction) + self.assertEqual(self.state(conn).outcome, "running") + conn.rollback() + self.assertEqual(self.state(conn).outcome, "running") + + def test_stale_deferred_restore_cannot_overwrite_new_invalidation(self) -> None: + conn = self.connection() + self.add(conn) + MAINTENANCE.run_style_maintenance(conn) + with MAINTENANCE.maintenance_owner(None) as owner: + previous = self.checkpoint(conn) + conn.execute("UPDATE maintenance_layers SET outcome='running',run_generation=? WHERE layer='style_vectors'", (owner.generation,)) + conn.commit() + running = self.checkpoint(conn) + conn.execute("UPDATE entity_style_vectors SET message_count=111") + conn.commit() + invalidated = self.checkpoint(conn) + with self.assertRaises(MAINTENANCE.MaintenanceBusyError): + MAINTENANCE._restore_deferred_attempt(conn, "style_vectors", previous, running, owner) + self.assertEqual(self.checkpoint(conn), invalidated) + self.assertIsNone(self.state(conn).attempted_revision) + + def test_two_connections_observe_only_committed_style_output(self) -> None: + uri = "file:synthetic-style-" + uuid.uuid4().hex + "?mode=memory&cache=shared" + first = self.connection(uri=uri) + second = self.connection(uri=uri) + self.add(first) + MAINTENANCE.run_style_maintenance(first) + self.assertEqual(self.output(first), self.output(second)) + self.assertEqual(self.checkpoint(first), self.checkpoint(second)) + reopened = load_maintenance() + self.assertFalse(reopened.run_style_maintenance(second).attempted) + + def test_writer_contention_defers_only_after_confirmed_output_rollback(self) -> None: + uri = "file:synthetic-style-" + uuid.uuid4().hex + "?mode=memory&cache=shared" + first = self.connection(uri=uri) + second = self.connection(uri=uri) + self.add(first) + MAINTENANCE.run_style_maintenance(first) + before, checkpoint = self.output(first), self.checkpoint(first) + compute = STYLE.compute_style_vector + rollbacks = [] + + def contend(text: str) -> list[float]: + second.execute("BEGIN IMMEDIATE") + second.execute("INSERT INTO sentinel VALUES ('synthetic competing writer')") + return compute(text) + + class WriterBoundary: + def __getattr__(boundary, name: str) -> object: + return getattr(first, name) + + def rollback(boundary) -> None: + first.rollback() + rollbacks.append(not first.in_transaction) + second.rollback() + + with patch.object(STYLE, "compute_style_vector", contend): + result = MAINTENANCE.run_style_maintenance(WriterBoundary(), force=True) + self.assertEqual((result.outcome, result.error_category), ("deferred", "StyleWriterBusy")) + self.assertEqual(rollbacks, [True]) + self.assertEqual(self.output(first), before) + self.assertEqual(self.checkpoint(first), checkpoint) + + def test_unexpected_sql_failure_stays_failed_and_modelbusy_default_stays_unchanged(self) -> None: + conn = self.connection() + self.add(conn) + with patch.object(STYLE, "compute_style_vector", side_effect=sqlite3.OperationalError("synthetic unexpected")): + result = MAINTENANCE.run_style_maintenance(conn) + self.assertEqual((result.outcome, result.error_category), ("failed", "OperationalError")) + self.assertEqual(MAINTENANCE.LayerDeferredError("synthetic").category, "ModelBusy") + with self.assertRaises(ValueError): + MAINTENANCE.LayerDeferredError("synthetic", category="Unreviewed") + + def test_existing_adapter_modelbusy_result_and_checkpoint_are_preserved(self) -> None: + conn = self.connection() + dependency = MAINTENANCE.make_layer_dependency(1) + + def busy(_conn: sqlite3.Connection) -> None: + raise MAINTENANCE.LayerDeferredError("synthetic model owner") + + spec = MAINTENANCE.LayerSpec("summaries", "build_summaries", lambda: dependency, busy, lambda _conn: 0) + before = MAINTENANCE._read_layer_row(conn, "summaries") + restore = MAINTENANCE._restore_deferred_attempt + + def positional_restore(*args: object) -> None: + self.assertEqual(len(args), 5) + restore(*args) + + with patch.object(MAINTENANCE, "_restore_deferred_attempt", positional_restore): + result = MAINTENANCE.run_layers(conn, (spec,))[0] + self.assertEqual((result.outcome, result.error_category), ("deferred", "ModelBusy")) + self.assertEqual(MAINTENANCE._read_layer_row(conn, "summaries"), before) + + def test_python310_errors_and_extended_codes_retain_specific_categories(self) -> None: + cases = ( + ("synthetic unexpected", None, False, "failed", "OperationalError"), + ("database is locked", None, False, "deferred", "StyleWriterBusy"), + ("database table is locked: messages", None, False, "deferred", "StyleWriterBusy"), + ("database table is locked: maintenance_layers", None, False, "deferred", "StyleWriterBusy"), + ("interrupted", None, True, "deferred", "StyleCancelled"), + ("synthetic busy snapshot", 517, False, "deferred", "StyleWriterBusy"), + ("synthetic shared cache", 262, False, "deferred", "StyleWriterBusy"), + ("database is locked", 10, False, "failed", "OperationalError"), + ) + for message, code, cancelled, outcome, category in cases: + with self.subTest(code=code, category=category, message=message): + conn = self.connection() + self.add(conn) + MAINTENANCE.run_style_maintenance(conn) + before = self.output(conn) + cancel = threading.Event() + error = sqlite3.OperationalError(message) + self.assertFalse(hasattr(error, "sqlite_errorcode")) + if code is not None: + error.sqlite_errorcode = code + + def fail(_text: str) -> list[float]: + if cancelled: + cancel.set() + raise error + + with patch.object(MAINTENANCE, "sqlite3", LegacySqliteModule()), \ + patch.object(STYLE, "compute_style_vector", fail): + result = MAINTENANCE.run_style_maintenance(conn, force=True, cancel=cancel) + self.assertEqual((result.outcome, result.error_category), (outcome, category)) + self.assertEqual(self.output(conn), before) + self.assertFalse(conn.in_transaction) + + def test_running_diagnostic_contention_defers_without_recording_an_attempt(self) -> None: + uri = "file:synthetic-style-" + uuid.uuid4().hex + "?mode=memory&cache=shared" + first = self.connection(uri=uri) + second = self.connection(uri=uri) + self.add(first) + MAINTENANCE.run_style_maintenance(first) + before, checkpoint = self.output(first), self.checkpoint(first) + second.execute("BEGIN IMMEDIATE") + second.execute("INSERT INTO sentinel VALUES ('synthetic writer stays active')") + try: + result = MAINTENANCE.run_style_maintenance(first, force=True) + self.assertEqual((result.outcome, result.error_category, result.attempted), + ("deferred", "StyleWriterBusy", False)) + self.assertTrue(second.in_transaction) + self.assertFalse(first.in_transaction) + self.assertEqual(self.output(first), before) + self.assertEqual(self.checkpoint(first), checkpoint) + finally: + second.rollback() + self.assertEqual(MAINTENANCE.run_style_maintenance(first, force=True).outcome, "success") + + def test_restoration_contention_leaves_running_diagnostic_for_later_recovery(self) -> None: + uri = "file:synthetic-style-" + uuid.uuid4().hex + "?mode=memory&cache=shared" + first = self.connection(uri=uri) + second = self.connection(uri=uri) + self.add(first) + MAINTENANCE.run_style_maintenance(first) + before, checkpoint = self.output(first), self.checkpoint(first) + compute = STYLE.compute_style_vector + + def contend(text: str) -> list[float]: + second.execute("BEGIN IMMEDIATE") + second.execute("INSERT INTO sentinel VALUES ('synthetic writer stays active')") + return compute(text) + + try: + with patch.object(STYLE, "compute_style_vector", contend): + result = MAINTENANCE.run_style_maintenance(first, force=True) + self.assertEqual((result.outcome, result.error_category), ("deferred", "StyleWriterBusy")) + self.assertTrue(second.in_transaction) + self.assertFalse(first.in_transaction) + self.assertEqual(self.output(first), before) + self.assertEqual(self.state(first).outcome, "running") + self.assertNotEqual(self.checkpoint(first), checkpoint) + finally: + second.rollback() + self.assertEqual(MAINTENANCE.run_style_maintenance(first).outcome, "success") + self.assertEqual(self.state(first).outcome, "success") + + def test_actual_delete_interrupt_confirms_owned_automatic_rollback(self) -> None: + for legacy in (False, True): + with self.subTest(legacy=legacy): + conn = self.connection() + self.add(conn) + MAINTENANCE.run_style_maintenance(conn) + before, checkpoint = self.output(conn), self.checkpoint(conn) + cancel = threading.Event() + observations = [] + + class InterruptBoundary: + handler_active = False + + def __getattr__(boundary, name: str) -> object: + return getattr(conn, name) + + def set_progress_handler(boundary, handler: object, steps: int) -> None: + boundary.handler_active = handler is not None + conn.set_progress_handler(handler, 1 if handler is not None else 0) + + def execute(boundary, sql: str, *args: object) -> sqlite3.Cursor: + if sql == "DELETE FROM entity_style_vectors": + cancel.set() + elif cancel.is_set(): + self.assertFalse(boundary.handler_active) + try: + return conn.execute(sql, *args) + except sqlite3.OperationalError as error: + if sql == "DELETE FROM entity_style_vectors": + observations.append((str(error), conn.in_transaction)) + if legacy: + old_error = sqlite3.OperationalError(str(error)) + self.assertFalse(hasattr(old_error, "sqlite_errorcode")) + raise old_error from error + raise + + with patch.object(MAINTENANCE, "sqlite3", LegacySqliteModule() if legacy else sqlite3): + result = MAINTENANCE.run_style_maintenance(InterruptBoundary(), force=True, + cancel=cancel, connection_owned=True) + self.assertEqual(observations, [("interrupted", False)]) + self.assertEqual((result.outcome, result.error_category), ("deferred", "StyleCancelled")) + self.assertFalse(conn.in_transaction) + self.assertEqual(self.output(conn), before) + self.assertEqual(self.checkpoint(conn), checkpoint) + + def test_unwitnessed_interrupt_cannot_claim_an_ended_transaction_rolled_back(self) -> None: + conn = self.connection() + self.add(conn) + MAINTENANCE.run_style_maintenance(conn) + cancel = threading.Event() + + def ended(_text: str) -> list[float]: + conn.commit() + cancel.set() + error = sqlite3.OperationalError("interrupted") + error.sqlite_errorcode = 9 + raise error + + with patch.object(STYLE, "compute_style_vector", ended): + with self.assertRaises(MAINTENANCE._LayerRollbackFailed): + MAINTENANCE.run_style_maintenance(conn, force=True, cancel=cancel, connection_owned=True) + self.assertEqual(self.state(conn).outcome, "running") + + def test_first_enrollment_and_trigger_repair_contention_preserve_caller(self) -> None: + for repair in (False, True): + for borrowed in (False, True): + with self.subTest(repair=repair, borrowed=borrowed): + uri = "file:synthetic-style-" + uuid.uuid4().hex + "?mode=memory&cache=shared" + first = self.connection(uri=uri) + second = self.connection(uri=uri) + self.add(first) + if repair: + MAINTENANCE.run_style_maintenance(first) + first.execute("DROP TRIGGER truememory_style_output_update") + first.commit() + before, checkpoint = self.output(first), self.checkpoint(first) + first.execute("CREATE TEMP TABLE caller_pending(value TEXT)") + if borrowed: + first.execute("BEGIN") + first.execute("INSERT INTO caller_pending VALUES ('synthetic pending')") + second.execute("BEGIN IMMEDIATE") + second.execute("INSERT INTO sentinel VALUES ('synthetic persistent writer')") + try: + result = MAINTENANCE.run_style_maintenance(first, allow_caller_transaction=borrowed) + self.assertEqual((result.outcome, result.error_category, result.attempted), + ("deferred", "StyleWriterBusy", False)) + self.assertEqual(result.pending_caller_commit, borrowed) + self.assertTrue(second.in_transaction) + self.assertEqual(first.in_transaction, borrowed) + self.assertEqual(first.execute("SELECT count(*) FROM caller_pending").fetchone()[0], int(borrowed)) + self.assertEqual(self.output(first), before) + self.assertEqual(self.checkpoint(first), checkpoint) + self.assertFalse(STORAGE._style_output_tracking_ready(first)) + finally: + first.rollback() + second.rollback() + self.assertEqual(MAINTENANCE.run_style_maintenance(first).outcome, "success") + + def test_preparation_unknown_errors_or_unconfirmed_transaction_end_do_not_defer(self) -> None: + for ended in (False, True): + with self.subTest(ended=ended): + conn = self.connection() + self.add(conn) + + class UnsafePreparation: + def __getattr__(boundary, name: str) -> object: + return getattr(conn, name) + + def execute(boundary, sql: str, *args: object) -> sqlite3.Cursor: + if (ended and sql == "COMMIT") or (not ended and sql == "BEGIN IMMEDIATE"): + if ended: + conn.commit() + raise sqlite3.OperationalError("database is locked" if ended else "synthetic unknown") + return conn.execute(sql, *args) + + with self.assertRaises(sqlite3.OperationalError): + MAINTENANCE.run_style_maintenance(UnsafePreparation()) + self.assertFalse(conn.in_transaction) + self.assertEqual(self.checkpoint(conn) is not None, ended) + + def test_source_metadata_read_contention_before_enrollment_defers(self) -> None: + uri = "file:synthetic-style-" + uuid.uuid4().hex + "?mode=memory&cache=shared" + first = self.connection(uri=uri) + second = self.connection(uri=uri) + self.add(first) + second.execute("BEGIN IMMEDIATE") + second.execute("INSERT INTO messages(sender,content) VALUES ('Synthetic','synthetic pending writer')") + try: + result = MAINTENANCE.run_style_maintenance(first) + self.assertEqual((result.outcome, result.error_category, result.attempted), + ("deferred", "StyleWriterBusy", False)) + self.assertTrue(second.in_transaction) + self.assertFalse(first.in_transaction) + self.assertIsNone(self.checkpoint(first)) + finally: + second.rollback() + self.assertEqual(MAINTENANCE.run_style_maintenance(first).outcome, "success") + + def test_failed_preparation_savepoint_rollback_is_not_deferred(self) -> None: + uri = "file:synthetic-style-" + uuid.uuid4().hex + "?mode=memory&cache=shared" + first = self.connection(uri=uri) + second = self.connection(uri=uri) + self.add(first) + first.execute("BEGIN") + second.execute("BEGIN IMMEDIATE") + second.execute("INSERT INTO sentinel VALUES ('synthetic writer')") + + class RefusedSavepointRollback: + def __getattr__(boundary, name: str) -> object: + return getattr(first, name) + + def execute(boundary, sql: str, *args: object) -> sqlite3.Cursor: + if sql == "ROLLBACK TO SAVEPOINT truememory_style_tracking": + raise sqlite3.OperationalError("synthetic rollback refused") + return first.execute(sql, *args) + + try: + with self.assertRaisesRegex(sqlite3.OperationalError, "synthetic rollback refused"): + MAINTENANCE.run_style_maintenance(RefusedSavepointRollback(), allow_caller_transaction=True) + self.assertTrue(first.in_transaction) + self.assertTrue(second.in_transaction) + self.assertIsNone(self.checkpoint(first)) + finally: + first.rollback() + second.rollback() + + def test_read_baseline_races_after_preparation_initial_read_and_cancellation_defer(self) -> None: + for phase in ("initial", "layer", "cancelled"): + with self.subTest(phase=phase): + uri = "file:synthetic-style-" + uuid.uuid4().hex + "?mode=memory&cache=shared" + first = self.connection(uri=uri) + second = self.connection(uri=uri) + self.add(first) + MAINTENANCE.run_style_maintenance(first) + before, checkpoint = self.output(first), self.checkpoint(first) + prepare = MAINTENANCE.prepare_style_maintenance + read = MAINTENANCE.read_layer_states + reads = [] + cancel = threading.Event() + if phase == "cancelled": + cancel.set() + + def contend() -> None: + second.execute("BEGIN IMMEDIATE") + second.execute("INSERT INTO messages(sender,content) VALUES ('Synthetic','synthetic pending writer')") + + def prepared(conn: sqlite3.Connection, **kwargs: object) -> None: + prepare(conn, **kwargs) + if phase == "initial": + contend() + + def baseline(conn: sqlite3.Connection, specs: tuple) -> dict: + reads.append(1) + result = read(conn, specs) + if len(reads) == 1 and phase != "initial": + contend() + return result + + try: + with patch.object(MAINTENANCE, "prepare_style_maintenance", prepared), \ + patch.object(MAINTENANCE, "read_layer_states", baseline): + result = MAINTENANCE.run_style_maintenance(first, force=True, cancel=cancel) + self.assertEqual((result.outcome, result.error_category, result.attempted), + ("deferred", "StyleWriterBusy", False)) + self.assertEqual(len(reads), 1 if phase == "initial" else 2) + self.assertTrue(second.in_transaction) + self.assertFalse(first.in_transaction) + self.assertEqual(self.output(first), before) + self.assertEqual(self.checkpoint(first), checkpoint) + finally: + second.rollback() + self.assertEqual(MAINTENANCE.run_style_maintenance(first, force=True).outcome, "success") + + def test_read_deferral_requires_known_lock_and_unchanged_caller_boundary(self) -> None: + for borrowed in (False, True): + for changed_boundary in (False, True): + for locked in (False, True): + with self.subTest(borrowed=borrowed, changed_boundary=changed_boundary, locked=locked): + conn = self.connection() + self.add(conn) + MAINTENANCE.run_style_maintenance(conn) + conn.execute("CREATE TEMP TABLE caller_pending(value TEXT)") + if borrowed: + conn.execute("BEGIN") + conn.execute("INSERT INTO caller_pending VALUES ('synthetic pending')") + + def failed_read(_conn: sqlite3.Connection, _specs: tuple) -> dict: + if changed_boundary: + if borrowed: + conn.commit() + else: + conn.execute("BEGIN") + raise sqlite3.OperationalError("database table is locked: maintenance_source_state" + if locked else "synthetic unexpected read") + + with patch.object(MAINTENANCE, "read_layer_states", failed_read): + if locked and not changed_boundary: + result = MAINTENANCE.run_style_maintenance(conn, force=True, allow_caller_transaction=borrowed) + self.assertEqual((result.outcome, result.error_category, result.pending_caller_commit), + ("deferred", "StyleWriterBusy", borrowed)) + self.assertEqual(conn.in_transaction, borrowed) + self.assertEqual(conn.execute("SELECT count(*) FROM caller_pending").fetchone()[0], int(borrowed)) + else: + with self.assertRaises(sqlite3.OperationalError): + MAINTENANCE.run_style_maintenance(conn, force=True, allow_caller_transaction=borrowed) + conn.rollback() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-l0-style-routing-747.py b/tests/test-l0-style-routing-747.py new file mode 100644 index 00000000..1250ff45 --- /dev/null +++ b/tests/test-l0-style-routing-747.py @@ -0,0 +1,1437 @@ +"""Actual private style routing with synthetic SQLite and stdlib-only loaders. + +Run the InMemoryRouting and CoordinatorRouting classes for local checks. +FileOpenerRouting requires the isolated full-suite host and synthetic files. +""" + +import os +import runpy +import sqlite3 +import tempfile +import threading +import types +import unittest +import uuid +from pathlib import Path +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] +BASE = runpy.run_path(str(Path(__file__).with_name("test-l0-style-incremental-747.py"))) +ROUTING = runpy.run_path(str(Path(__file__).with_name("test-maintenance-engine-routing-753.py"))) +STORAGE, STYLE, MAINTENANCE = BASE["STORAGE"], BASE["STYLE"], BASE["MAINTENANCE"] + + +def public_specs(conn: sqlite3.Connection, *_args: object, **_kwargs: object) -> tuple: + layers = ("clusters", "summaries", "contradictions", "structured_facts", "surprise", "episodes", "landmarks", "dunbar") + return tuple(MAINTENANCE.LayerSpec(layer, key, lambda: MAINTENANCE.make_layer_dependency(1), + lambda _conn: None, lambda _conn: 0, connection=conn) + for layer, key in zip(layers, MAINTENANCE._MAINTENANCE_RESULT_KEYS)) + + +class InMemoryRouting(unittest.TestCase): + def connection(self, *, bootstrap: bool = False) -> sqlite3.Connection: + conn = sqlite3.connect(":memory:") + self.addCleanup(conn.close) + conn.executescript(STORAGE._SCHEMA_SQL) + STORAGE._initialize_maintenance_tracking(conn) + conn.execute("CREATE TABLE sentinel(value TEXT)") + conn.commit() + if bootstrap: + self.assertEqual(MAINTENANCE.run_style_maintenance(conn).outcome, "success_empty") + return conn + + def engine(self, conn: sqlite3.Connection) -> object: + modules = MAINTENANCE._fixture_modules + modules["truememory.personality_style_vec"] = STYLE + modules["truememory.rebuild_source"] = ROUTING["stdlib_module"]("rebuild_source", modules) + module = ROUTING["load_engine"](modules) + module._HAS_STYLE_VEC = True + module._update_style_vec = STYLE.update_entity_style_vector_incremental + engine = module.TrueMemoryEngine(":memory:") + engine.conn, engine.ready = conn, True + engine._has_consolidation = False + engine._has_style_vec = True + engine._maintenance_coordinator = MAINTENANCE.MaintenanceCoordinator(None) + engine._maintenance_handle = conn + engine._engine_test_module = module + return engine + + def add(self, conn: sqlite3.Connection, *, sender: str = "Synthetic") -> None: + STORAGE.insert_message(conn, {"content": "synthetic source", "sender": sender, "directive": False}) + conn.commit() + + def checkpoint(self, conn: sqlite3.Connection) -> tuple | None: + return MAINTENANCE._read_layer_row(conn, "style_vectors") + + def rows(self, conn: sqlite3.Connection) -> list: + return conn.execute("SELECT * FROM entity_style_vectors ORDER BY entity").fetchall() + + def no_native(self) -> object: + original = MAINTENANCE.importlib + return patch.object(MAINTENANCE, "importlib", types.SimpleNamespace(import_module=lambda name: + original.import_module(name) if name == "truememory.personality_style_vec" else + self.fail("Unexpected dependency preparation"))) + + def test_style_only_dispatch_precedes_and_excludes_public_preparation(self) -> None: + conn = self.connection() + self.add(conn) + coordinator = MAINTENANCE.MaintenanceCoordinator(None) + with self.no_native(), patch.object(MAINTENANCE, "_prepare_worker_extensions", side_effect=AssertionError), \ + patch.object(MAINTENANCE, "engine_layer_specs", side_effect=AssertionError): + report = MAINTENANCE.run_engine_maintenance(conn, coordinator, include_layers=False, include_style=True) + self.assertEqual(report.results, ()) + self.assertTrue(report.preferences.startswith("SKIPPED")) + self.assertEqual(report.style_result.outcome, "success") + self.assertEqual(MAINTENANCE.maintenance_report_status(report), ("success", None)) + self.assertEqual(MAINTENANCE.observe_style(conn).health["freshness"], "current") + + def test_combined_style_publishes_before_unrelated_native_failure(self) -> None: + conn = self.connection() + self.add(conn) + with patch.object(MAINTENANCE, "_prepare_worker_extensions", side_effect=RuntimeError("synthetic extension")): + with self.assertRaises(RuntimeError): + MAINTENANCE.run_engine_maintenance(conn, MAINTENANCE.MaintenanceCoordinator(None), include_style=True) + self.assertEqual(MAINTENANCE.observe_style(conn).health["freshness"], "current") + self.assertEqual(len(self.rows(conn)), 1) + + def test_bounded_readiness_and_health_do_not_read_source_or_profiles(self) -> None: + conn = self.connection() + def authorize(action: int, table: str | None, *_args: object) -> int: + return sqlite3.SQLITE_DENY if action == sqlite3.SQLITE_READ and table in {"messages", "entity_style_vectors"} else sqlite3.SQLITE_OK + conn.set_authorizer(authorize) + before = conn.total_changes + with self.no_native(): + observation = MAINTENANCE.observe_style(conn) + health = self.engine(conn).get_style_health() + self.assertTrue(observation.eligible) + self.assertEqual(health["coverage"], "unverified") + self.assertEqual(conn.total_changes, before) + self.assertFalse(conn.in_transaction) + conn.set_authorizer(BASE["ACCUMULATOR"]["allow_authorized_action"]) + + def test_style_plans_with_consolidation_disabled_and_submits_outside_lock(self) -> None: + engine = self.engine(self.connection()) + calls = [] + def requested(**kwargs: object) -> bool: + self.assertFalse(engine._write_lock.locked()) + self.assertFalse(engine.conn.in_transaction) + calls.append(kwargs) + return False + with patch.object(engine._maintenance_coordinator, "request_layers", requested), \ + patch.object(MAINTENANCE, "engine_layer_specs", side_effect=AssertionError): + engine._maybe_auto_consolidate() + self.assertEqual(calls, [{"threshold": 25, "include_layers": False, "include_style": True}]) + + def test_75_actual_adds_make_no_style_request_or_full_rebuild(self) -> None: + conn = self.connection(bootstrap=True) + engine = self.engine(conn) + with patch.object(engine._maintenance_coordinator, "request_layers") as requested, \ + patch.object(STYLE, "_compute_entity_style_vectors", wraps=STYLE._compute_entity_style_vectors) as builds: + for index in range(75): + engine.add("synthetic append " + str(index), sender="Synthetic") + requested.assert_not_called() + builds.assert_not_called() + self.assertEqual(conn.execute("SELECT message_count FROM entity_style_vectors").fetchone(), (75,)) + self.assertEqual(MAINTENANCE.observe_style(conn).state.attempted_insert_count, 75) + + def test_uncovered_append_threshold_is_24_then_25_and_corrections_are_immediate(self) -> None: + conn = self.connection(bootstrap=True) + for _ in range(24): + self.add(conn) + self.assertFalse(MAINTENANCE.observe_style(conn).eligible) + self.assertEqual(MAINTENANCE.observe_style(conn).health["pending_reason"], "awaiting_threshold") + self.add(conn) + self.assertTrue(MAINTENANCE.observe_style(conn).eligible) + self.assertEqual(MAINTENANCE.run_routed_style(conn).outcome, "success") + conn.execute("UPDATE messages SET content='synthetic correction' WHERE id=1") + conn.commit() + self.assertTrue(MAINTENANCE.observe_style(conn).eligible) + + def test_hash_migration_and_owned_trigger_repair_make_current_rows_eligible(self) -> None: + for change in ("hash", "trigger", "coverage"): + with self.subTest(change=change): + conn = self.connection(bootstrap=True) + if change == "hash": + conn.execute("UPDATE metadata SET value='1' WHERE key='style_vec_hash_version'") + elif change == "trigger": + conn.execute("DROP TRIGGER truememory_style_output_update") + else: + conn.execute("UPDATE maintenance_layers SET successful_coverage='unverified' WHERE layer='style_vectors'") + conn.commit() + self.assertTrue(MAINTENANCE.observe_style(conn).eligible) + self.assertEqual(MAINTENANCE.run_routed_style(conn).outcome, "success_empty") + self.assertFalse(MAINTENANCE.observe_style(conn).eligible) + self.assertEqual(MAINTENANCE.observe_style(conn).health["coverage"], "complete") + + def test_failed_hash_migration_keeps_attempt_baseline_and_suppresses_reopen_retry(self) -> None: + conn = self.connection(bootstrap=True) + conn.execute("UPDATE metadata SET value='1' WHERE key='style_vec_hash_version'") + conn.commit() + with patch.object(STYLE, "_compute_entity_style_vectors", side_effect=ValueError("synthetic failure")): + result = MAINTENANCE.run_routed_style(conn) + self.assertEqual(result.outcome, "failed") + before = self.checkpoint(conn) + self.assertFalse(MAINTENANCE.observe_style(conn).eligible) + self.assertEqual(MAINTENANCE.run_routed_style(conn).outcome, "untrusted") + self.assertEqual(self.checkpoint(conn), before) + + def test_failed_attempts_retry_at_25_appends_without_repeated_worker_opens(self) -> None: + for mode in ("initial", "current", "future"): + conn = self.connection(bootstrap=mode == "current") + self.add(conn) + if mode == "current": + MAINTENANCE.run_routed_style(conn, force=True) + conn.execute("UPDATE metadata SET value='1' WHERE key='style_vec_hash_version'") + elif mode == "future": + conn.execute("DROP TABLE entity_style_vectors") + conn.execute("CREATE TABLE entity_style_vectors(entity TEXT PRIMARY KEY,vector TEXT,message_count INTEGER," + "updated_at TEXT,vector_sum BLOB,accumulator_version INTEGER NOT NULL DEFAULT 9)") + conn.commit() + if mode == "future": + failed = MAINTENANCE.run_routed_style(conn) + else: + with patch.object(STYLE, "_compute_entity_style_vectors", side_effect=ValueError("synthetic build failure")): + failed = MAINTENANCE.run_routed_style(conn) + self.assertEqual(failed.outcome, "unavailable" if mode == "future" else "failed") + engine = self.engine(conn) + coordinator = MAINTENANCE.MaintenanceCoordinator(ROOT / "synthetic-worker-not-created.sqlite") + engine._maintenance_coordinator = coordinator + events = [] + class Opened: + row_factory = None + def __getattr__(self, name: str) -> object: + return getattr(conn, name) + def execute(self, sql: str, *args: object) -> sqlite3.Cursor: + if sql == "PRAGMA quick_check(1)": + events.append("check") + return conn.execute(sql, *args) + def close(self) -> None: + events.append("close") + class ImmediateThread: + def __init__(self, *, target: object, **_kwargs: object) -> None: + self.target = target + def start(self) -> None: + self.target() + def connect(*_args: object, **_kwargs: object) -> object: + events.append("open") + return Opened() + previous = 0 + with patch.object(MAINTENANCE.sqlite3, "connect", connect), \ + patch.object(MAINTENANCE.threading, "Thread", ImmediateThread), \ + patch.object(MAINTENANCE, "_acquire_owner", return_value=123), \ + patch.object(MAINTENANCE, "_release_owner"): + for count in (0, 1, 24, 25): + for _ in range(count - previous): + self.add(conn) + previous = count + with self.subTest(mode=mode, appends=count): + observed = MAINTENANCE.observe_style(conn) + if count < 25: + baseline = self.checkpoint(conn) + self.assertFalse(MAINTENANCE.run_routed_style(conn).attempted) + self.assertEqual(self.checkpoint(conn), baseline) + before_wakes = len(events) + for _ in range(3): + engine._maybe_auto_consolidate() + self.assertEqual(events[before_wakes:], ["open", "check", "close"] if count == 25 else []) + self.assertEqual(observed.eligible, count == 25) + self.assertEqual(observed.eligible, MAINTENANCE._eligible(observed.state, 25, False)) + if count == 25: + result = coordinator._last_report.style_result + self.assertTrue(result.attempted) + self.assertEqual(result.outcome, "unavailable" if mode == "future" else "success") + self.assertFalse(MAINTENANCE.observe_style(conn).eligible) + + def test_failed_attempt_correction_or_dependency_repair_retries_before_25(self) -> None: + for change in ("correction", "dependency"): + with self.subTest(change=change): + conn = self.connection() + self.add(conn) + with patch.object(STYLE, "_compute_entity_style_vectors", side_effect=ValueError("synthetic failure")): + self.assertEqual(MAINTENANCE.run_routed_style(conn).outcome, "failed") + if change == "correction": + conn.execute("UPDATE messages SET content='synthetic correction' WHERE id=1") + else: + conn.execute("DROP TRIGGER truememory_style_output_update") + conn.commit() + self.assertTrue(MAINTENANCE.observe_style(conn).eligible) + self.assertEqual(MAINTENANCE.run_routed_style(conn).outcome, "success") + self.assertFalse(MAINTENANCE.observe_style(conn).eligible) + + def test_future_default_remains_unverified_and_guarded_without_reopen_loop(self) -> None: + conn = self.connection(bootstrap=True) + conn.execute("DROP TABLE entity_style_vectors") + conn.execute("CREATE TABLE entity_style_vectors(entity TEXT PRIMARY KEY,vector TEXT,message_count INTEGER," + "updated_at TEXT,vector_sum BLOB,accumulator_version INTEGER NOT NULL DEFAULT 9)") + conn.commit() + MAINTENANCE.prepare_style_maintenance(conn) + observation = MAINTENANCE.observe_style(conn) + self.assertTrue(observation.eligible) + self.assertFalse(observation.health["format_ready"]) + self.assertEqual(observation.health["coverage"], "unverified") + schema = conn.execute("PRAGMA table_info(entity_style_vectors)").fetchall() + result = MAINTENANCE.run_routed_style(conn) + self.assertEqual((result.outcome, result.error_category), ("unavailable", "StyleAccumulatorUnsupported")) + self.assertEqual(conn.execute("PRAGMA table_info(entity_style_vectors)").fetchall(), schema) + self.assertEqual(self.rows(conn), []) + self.assertFalse(MAINTENANCE.observe_style(conn).eligible) + + def test_hash_invalidation_writer_contention_defers_after_confirmed_rollback(self) -> None: + for borrowed in (False, True): + with self.subTest(borrowed=borrowed): + original = self.connection(bootstrap=True) + original.execute("UPDATE metadata SET value='1' WHERE key='style_vec_hash_version'") + original.commit() + uri = "file:synthetic-style-routing-" + uuid.uuid4().hex + "?mode=memory&cache=shared" + conn = sqlite3.connect(uri, uri=True, timeout=0) + contender = sqlite3.connect(uri, uri=True, timeout=0) + self.addCleanup(conn.close) + self.addCleanup(contender.close) + original.backup(conn) + before = self.checkpoint(conn) + if borrowed: + conn.execute("BEGIN") + observe = MAINTENANCE.observe_style + def contend(*args: object, **kwargs: object) -> object: + result = observe(*args, **kwargs) + contender.execute("BEGIN IMMEDIATE") + contender.execute("INSERT INTO sentinel VALUES ('synthetic writer')") + return result + with patch.object(MAINTENANCE, "observe_style", contend): + result = MAINTENANCE.run_routed_style(conn, allow_caller_transaction=borrowed) + self.assertEqual((result.outcome, result.error_category), ("deferred", "StyleWriterBusy")) + self.assertEqual(conn.in_transaction, borrowed) + self.assertEqual(self.checkpoint(conn), before) + contender.rollback() + result = MAINTENANCE.run_routed_style(conn, allow_caller_transaction=borrowed) + self.assertEqual(result.outcome, "success_empty") + if borrowed: + conn.rollback() + self.assertEqual(self.checkpoint(conn), before) + + def test_hash_invalidation_preserves_newer_failure_instead_of_forced_overwrite(self) -> None: + for borrowed in (False, True): + with self.subTest(borrowed=borrowed): + conn = self.connection(bootstrap=True) + conn.execute("UPDATE metadata SET value='1' WHERE key='style_vec_hash_version'") + conn.commit() + if borrowed: + conn.execute("BEGIN") + expected = [] + observe = MAINTENANCE.observe_style + def replace(*args: object, **kwargs: object) -> object: + result = observe(*args, **kwargs) + conn.execute("UPDATE maintenance_layers SET outcome='failed',error_category='NewerFailure'," + "run_generation='newer-owner',attempted_insert_count=999 WHERE layer='style_vectors'") + if not borrowed: + conn.commit() + expected.append(self.checkpoint(conn)) + return result + with patch.object(MAINTENANCE, "observe_style", replace), \ + patch.object(STYLE, "_compute_entity_style_vectors", side_effect=AssertionError): + result = MAINTENANCE.run_routed_style(conn, force=True, allow_caller_transaction=borrowed) + self.assertEqual((result.outcome, result.error_category), ("deferred", "StyleSourceChanged")) + self.assertEqual(self.checkpoint(conn), expected[0]) + self.assertEqual(conn.in_transaction, borrowed) + + def test_hash_invalidation_does_not_hide_unknown_errors_or_lost_transactions(self) -> None: + for boundary in ("unknown", "ended", "rollback"): + with self.subTest(boundary=boundary): + conn = self.connection(bootstrap=True) + conn.execute("UPDATE metadata SET value='1' WHERE key='style_vec_hash_version'") + conn.commit() + before = self.checkpoint(conn) + class Fault: + def __getattr__(self, name: str) -> object: + return getattr(conn, name) + + def execute(self, sql: str, *args: object) -> sqlite3.Cursor: + if sql == "UPDATE maintenance_layers SET layer=layer WHERE 0": + if boundary == "ended": + conn.rollback() + raise sqlite3.OperationalError("synthetic unknown" if boundary == "unknown" else "database is locked") + return conn.execute(sql, *args) + + def rollback(self) -> None: + if boundary == "rollback": + raise sqlite3.OperationalError("synthetic rollback failure") + conn.rollback() + + expected = sqlite3.OperationalError if boundary == "unknown" else MAINTENANCE._LayerRollbackFailed + # The observation's read-only rollback must remain ordinary; + # inject only at the subsequent writer boundary. + with patch.object(MAINTENANCE, "observe_style", return_value=MAINTENANCE.observe_style(conn)): + with self.assertRaises(expected): + MAINTENANCE.run_routed_style(Fault()) + conn.rollback() + self.assertEqual(self.checkpoint(conn), before) + + def test_unsupported_triggers_do_not_schedule_or_mutate_on_any_guarded_table(self) -> None: + for table in ("entity_style_vectors", "maintenance_layers", "metadata"): + for temporary in (False, True): + with self.subTest(table=table, temporary=temporary): + conn = self.connection() + conn.execute("CREATE " + ("TEMP " if temporary else "") + "TRIGGER synthetic_unsupported AFTER INSERT ON main." + + table + " BEGIN INSERT INTO sentinel VALUES ('unexpected mutation'); END") + conn.commit() + engine = self.engine(conn) + before = conn.total_changes + with patch.object(engine._maintenance_coordinator, "request_layers") as requested: + engine._maybe_auto_consolidate() + requested.assert_not_called() + report = MAINTENANCE.run_engine_maintenance(conn, engine._maintenance_coordinator, + include_layers=False, include_style=True) + self.assertEqual(report.style_result.error_category, "StyleTriggersUnsupported") + self.assertEqual(report.style_result.outcome, "deferred") + self.assertEqual(conn.total_changes, before) + self.assertIsNone(self.checkpoint(conn)) + + def test_source_readiness_rejects_missing_changed_shadowed_and_unready_tracking(self) -> None: + for change in ("missing", "changed", "shadow", "unready", "token", "schema"): + with self.subTest(change=change): + conn = self.connection() + if change == "missing": + conn.execute("DROP TRIGGER messages_maintenance_ai") + elif change == "changed": + conn.execute("DROP TRIGGER messages_maintenance_ai") + conn.execute("CREATE TRIGGER messages_maintenance_ai AFTER INSERT ON messages BEGIN SELECT 1; END") + elif change == "shadow": + conn.execute("CREATE TEMP TRIGGER messages_maintenance_ai AFTER INSERT ON main.messages BEGIN SELECT 1; END") + elif change == "unready": + conn.execute("UPDATE maintenance_source_state SET tracking_ready=0") + elif change == "token": + conn.execute("DELETE FROM maintenance_source_state") + else: + conn.execute("ALTER TABLE messages RENAME COLUMN id TO synthetic_id") + conn.commit() + before = conn.total_changes + observation = MAINTENANCE.observe_style(conn) + self.assertFalse(observation.eligible) + self.assertEqual(observation.health["error_category"], "StyleSourceUnavailable") + self.assertEqual(MAINTENANCE.run_routed_style(conn).outcome, "unavailable") + self.assertEqual(conn.total_changes, before) + + def test_canonical_source_rechecked_under_preparation_and_diagnostic_writer(self) -> None: + for enrolled in (False, True): + for borrowed in (False, True): + with self.subTest(enrolled=enrolled, borrowed=borrowed): + conn = self.connection(bootstrap=enrolled) + self.add(conn) + if enrolled: + MAINTENANCE.run_routed_style(conn, force=True) + baseline, output = self.checkpoint(conn), self.rows(conn) + if borrowed: + conn.execute("INSERT INTO sentinel VALUES ('caller pending')") + observe = MAINTENANCE.observe_style + def invalidate(*args: object, **kwargs: object) -> object: + result = observe(*args, **kwargs) + conn.execute("DROP TRIGGER messages_maintenance_ai") + if not borrowed: + conn.commit() + return result + before = conn.total_changes + with patch.object(MAINTENANCE, "observe_style", invalidate): + result = MAINTENANCE.run_routed_style(conn, force=True, allow_caller_transaction=borrowed) + self.assertEqual((result.outcome, result.error_category), ("deferred", "StyleSourceChanged")) + self.assertEqual(conn.total_changes, before) + self.assertEqual(self.checkpoint(conn), baseline) + self.assertEqual(self.rows(conn), output) + self.assertEqual(conn.in_transaction, borrowed) + self.assertEqual(MAINTENANCE.observe_style(conn).health["error_category"], "StyleSourceUnavailable") + if borrowed: + self.assertEqual(conn.execute("SELECT * FROM sentinel").fetchall(), [("caller pending",)]) + conn.rollback() + self.assertTrue(MAINTENANCE.observe_style(conn).health["available"] if enrolled else + MAINTENANCE.observe_style(conn).eligible) + + def test_canonical_source_rechecked_before_output_and_rollback_remains_required(self) -> None: + for boundary in ("publication", "rollback_failure"): + with self.subTest(boundary=boundary): + conn = self.connection(bootstrap=True) + self.add(conn) + MAINTENANCE.run_routed_style(conn, force=True) + baseline, output = self.checkpoint(conn), self.rows(conn) + compute = STYLE._compute_entity_style_vectors + def invalidate(*args: object, **kwargs: object) -> object: + result = compute(*args, **kwargs) + conn.execute("DROP TRIGGER messages_maintenance_ai") + return result + class Connection: + def __getattr__(self, name: str) -> object: + return getattr(conn, name) + def rollback(self) -> None: + missing = conn.execute("SELECT 1 FROM sqlite_master WHERE name='messages_maintenance_ai'").fetchone() is None + if boundary == "rollback_failure" and missing: + raise sqlite3.OperationalError("synthetic rollback failure") + conn.rollback() + with patch.object(STYLE, "_compute_entity_style_vectors", invalidate): + if boundary == "rollback_failure": + with self.assertRaises(MAINTENANCE._LayerRollbackFailed): + MAINTENANCE.run_routed_style(Connection(), force=True) + conn.rollback() + else: + result = MAINTENANCE.run_routed_style(Connection(), force=True) + self.assertEqual((result.outcome, result.error_category), ("deferred", "StyleSourceChanged")) + self.assertEqual(self.checkpoint(conn), baseline) + self.assertEqual(self.rows(conn), output) + self.assertFalse(conn.in_transaction) + + def test_current_style_rechecks_source_in_every_baseline_snapshot(self) -> None: + for boundary in ("observation", "first_baseline", "cancelled_baseline"): + for borrowed in (False, True): + with self.subTest(boundary=boundary, borrowed=borrowed): + conn = self.connection(bootstrap=True) + self.add(conn) + MAINTENANCE.run_routed_style(conn, force=True) + baseline, output = self.checkpoint(conn), self.rows(conn) + self.assertEqual(MAINTENANCE.observe_style(conn).health["freshness"], "current") + if borrowed: + conn.execute("INSERT INTO sentinel VALUES ('caller pending')") + cancel = threading.Event() + original = (MAINTENANCE.observe_style if boundary == "observation" else + MAINTENANCE._read_layer_run_baseline) + changed = False + def invalidate(*args: object, **kwargs: object) -> object: + nonlocal changed + result = original(*args, **kwargs) + if not changed: + changed = True + conn.execute("DROP TRIGGER messages_maintenance_ai") + if not borrowed: + conn.commit() + if boundary == "cancelled_baseline": + cancel.set() + return result + def read_only(action: int, table: str | None, *_args: object) -> int: + if action in {sqlite3.SQLITE_INSERT, sqlite3.SQLITE_UPDATE, sqlite3.SQLITE_DELETE} and table in { + "messages", "entity_style_vectors", "maintenance_layers", "metadata", + }: + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + attribute = "observe_style" if boundary == "observation" else "_read_layer_run_baseline" + before = conn.total_changes + conn.set_authorizer(read_only) + try: + with patch.object(MAINTENANCE, attribute, invalidate), \ + patch.object(STYLE, "_compute_entity_style_vectors", side_effect=AssertionError): + result = MAINTENANCE.run_routed_style(conn, force=False, cancel=cancel, + allow_caller_transaction=borrowed) + finally: + conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.assertTrue(changed) + self.assertEqual((result.outcome, result.error_category, result.coverage, result.attempted), + ("deferred", "StyleSourceChanged", "unverified", False)) + self.assertEqual(conn.total_changes, before) + self.assertEqual(self.checkpoint(conn), baseline) + self.assertEqual(self.rows(conn), output) + self.assertEqual(conn.in_transaction, borrowed) + self.assertEqual(MAINTENANCE.observe_style(conn).health["error_category"], "StyleSourceUnavailable") + if borrowed: + self.assertEqual(conn.execute("SELECT * FROM sentinel").fetchall(), [("caller pending",)]) + conn.rollback() + self.assertEqual(MAINTENANCE.observe_style(conn).health["freshness"], "current") + + def test_current_style_baseline_is_read_only_and_preserves_caller_snapshot(self) -> None: + for borrowed in (False, True): + with self.subTest(borrowed=borrowed): + conn = self.connection(bootstrap=True) + baseline = self.checkpoint(conn) + if borrowed: + conn.execute("INSERT INTO sentinel VALUES ('caller pending')") + observed = [] + validate = MAINTENANCE._validate_routed_style_source + def in_snapshot(connection: sqlite3.Connection) -> None: + observed.append(connection.in_transaction) + validate(connection) + def read_only(action: int, table: str | None, *_args: object) -> int: + if action in {sqlite3.SQLITE_INSERT, sqlite3.SQLITE_UPDATE, sqlite3.SQLITE_DELETE}: + return sqlite3.SQLITE_DENY + if action == sqlite3.SQLITE_READ and table in {"messages", "entity_style_vectors"}: + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + before = conn.total_changes + conn.set_authorizer(read_only) + try: + with patch.object(MAINTENANCE, "_validate_routed_style_source", in_snapshot), \ + patch.object(STYLE, "_compute_entity_style_vectors", side_effect=AssertionError): + result = MAINTENANCE.run_routed_style(conn, allow_caller_transaction=borrowed) + finally: + conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.assertEqual((result.outcome, result.coverage, result.attempted), ("current", "complete", False)) + self.assertTrue(observed) + self.assertTrue(all(observed)) + self.assertEqual(conn.total_changes, before) + self.assertEqual(self.checkpoint(conn), baseline) + self.assertEqual(conn.in_transaction, borrowed) + if borrowed: + self.assertEqual(conn.execute("SELECT * FROM sentinel").fetchall(), [("caller pending",)]) + conn.rollback() + + def test_late_complete_readiness_loss_defers_without_an_attempt_and_can_recover(self) -> None: + changes = {"hash": "StyleHashPending", "format": "StyleAccumulatorUnsupported", + "output_trigger": "StyleTrackingUnprepared", "checkpoint": "StyleTrackingUnprepared"} + changes.update({f"{schema}_{table}": "StyleTriggersUnsupported" for schema in ("main", "temp") + for table in ("entity_style_vectors", "maintenance_layers", "metadata")}) + for change, category in changes.items(): + boundaries = ("observation", "preparation", "first_baseline", "cancelled_baseline") if change in { + "hash", "format", + } else ("preparation", "first_baseline", "cancelled_baseline") + for boundary in boundaries: + for borrowed in (False, True): + with self.subTest(change=change, boundary=boundary, borrowed=borrowed): + conn = self.connection(bootstrap=True) + self.add(conn) + self.assertEqual(MAINTENANCE.run_routed_style(conn, force=True).outcome, "success") + baseline, output = self.checkpoint(conn), self.rows(conn) + if borrowed: + conn.execute("INSERT INTO sentinel VALUES ('caller pending')") + cancel = threading.Event() + attribute = {"observation": "observe_style", "preparation": "_prepare_routed_style_maintenance"}.get( + boundary, "_read_layer_run_baseline") + original = getattr(MAINTENANCE, attribute) + changed = [] + def read_only(action: int, table: str | None, *_args: object) -> int: + if action in {sqlite3.SQLITE_INSERT, sqlite3.SQLITE_UPDATE, sqlite3.SQLITE_DELETE} and table in { + "messages", "entity_style_vectors", "maintenance_layers", "metadata", + }: + return sqlite3.SQLITE_DENY + if action == sqlite3.SQLITE_READ and table in {"messages", "entity_style_vectors"}: + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + def invalidate(*args: object, **kwargs: object) -> object: + result = original(*args, **kwargs) + if not changed: + if change == "hash": + conn.execute("UPDATE metadata SET value='1' WHERE key='style_vec_hash_version'") + elif change == "format": + conn.execute("ALTER TABLE entity_style_vectors DROP COLUMN accumulator_version") + conn.execute("ALTER TABLE entity_style_vectors ADD COLUMN accumulator_version INTEGER DEFAULT 9") + elif change == "output_trigger": + conn.execute("DROP TRIGGER truememory_style_output_update") + elif change == "checkpoint": + conn.execute("DELETE FROM maintenance_layers WHERE layer='style_vectors'") + else: + schema, table = change.split("_", 1) + conn.execute(f"CREATE {'TEMP ' if schema == 'temp' else ''}TRIGGER extra_proof " + f"AFTER UPDATE ON main.{table} BEGIN SELECT 1; END") + if not borrowed: + conn.commit() + changed.append((self.checkpoint(conn), self.rows(conn), conn.total_changes)) + if boundary == "cancelled_baseline": + cancel.set() + conn.set_authorizer(read_only) + return result + try: + with patch.object(MAINTENANCE, attribute, invalidate), self.no_native(), \ + patch.object(STYLE, "_compute_entity_style_vectors", side_effect=AssertionError): + result = MAINTENANCE.run_routed_style(conn, cancel=cancel, + allow_caller_transaction=borrowed) + finally: + conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.assertEqual(len(changed), 1) + self.assertEqual((result.outcome, result.error_category, result.coverage, result.attempted), + ("deferred", category, "unverified", False)) + self.assertEqual((self.checkpoint(conn), self.rows(conn), conn.total_changes), changed[0]) + self.assertEqual(conn.in_transaction, borrowed) + if change in {"hash", "output_trigger", "checkpoint"}: + if not borrowed: + self.assertTrue(MAINTENANCE.observe_style(conn).eligible) + repaired = MAINTENANCE.run_routed_style(conn, allow_caller_transaction=borrowed) + self.assertEqual((repaired.outcome, repaired.coverage), ("success", "complete")) + self.assertTrue(repaired.attempted) + elif change == "format": + failed = MAINTENANCE.run_routed_style(conn, allow_caller_transaction=borrowed) + self.assertEqual((failed.outcome, failed.error_category, failed.attempted), + ("unavailable", "StyleAccumulatorUnsupported", True)) + attempt = self.checkpoint(conn) + self.assertFalse(MAINTENANCE.run_routed_style(conn, allow_caller_transaction=borrowed).attempted) + self.assertEqual(self.checkpoint(conn), attempt) + if borrowed: + self.assertEqual(conn.execute("SELECT * FROM sentinel").fetchall(), [("caller pending",)]) + conn.rollback() + self.assertEqual((self.checkpoint(conn), self.rows(conn)), (baseline, output)) + + def test_failed_builder_with_old_complete_coverage_keeps_its_retry_budget(self) -> None: + for change, failure in ((change, failure) for change in ("hash", "format") + for failure in (ValueError, MAINTENANCE.LayerUnavailableError)): + for borrowed in (False, True): + with self.subTest(change=change, failure=failure.__name__, borrowed=borrowed): + conn = self.connection(bootstrap=True) + self.add(conn) + self.assertEqual(MAINTENANCE.run_routed_style(conn, force=True).outcome, "success") + with patch.object(STYLE, "_compute_entity_style_vectors", side_effect=failure("synthetic failure")): + self.assertEqual(MAINTENANCE.run_routed_style(conn, force=True).outcome, + "failed" if failure is ValueError else "unavailable") + baseline = self.checkpoint(conn) + self.assertEqual(baseline[MAINTENANCE._LAYER_ROW_COLUMNS.index("successful_coverage")], "complete") + if change == "hash": + conn.execute("UPDATE metadata SET value='1' WHERE key='style_vec_hash_version'") + else: + conn.execute("ALTER TABLE entity_style_vectors DROP COLUMN accumulator_version") + conn.execute("ALTER TABLE entity_style_vectors ADD COLUMN accumulator_version INTEGER DEFAULT 9") + conn.commit() + if borrowed: + conn.execute("INSERT INTO sentinel VALUES ('caller pending')") + previous = 0 + for count in (0, 1, 24, 25): + for _ in range(count - previous): + STORAGE.insert_message(conn, {"content": "synthetic append", "sender": "Synthetic"}) + if not borrowed: + conn.commit() + previous = count + result = MAINTENANCE.run_routed_style(conn, allow_caller_transaction=borrowed) + self.assertEqual(result.attempted, count == 25) + if count < 25: + self.assertEqual(result.coverage, "unverified") + self.assertEqual(result.error_category, failure.__name__) + self.assertEqual(self.checkpoint(conn), baseline) + else: + self.assertEqual(result.outcome, "success" if change == "hash" else "unavailable") + self.assertEqual(conn.in_transaction, borrowed) + if borrowed: + self.assertEqual(conn.execute("SELECT * FROM sentinel").fetchall(), [("caller pending",)]) + conn.rollback() + self.assertEqual(self.checkpoint(conn), baseline) + + def test_retry_state_reload_preserves_known_unverified_failure_coverage(self) -> None: + coverage_index = MAINTENANCE._LAYER_ROW_COLUMNS.index("successful_coverage") + for change in ("intact", "hash", "format"): + for failure in (ValueError, MAINTENANCE.LayerUnavailableError): + for borrowed in (False, True): + for force in (False, True): + with self.subTest(change=change, failure=failure.__name__, borrowed=borrowed, force=force): + conn = self.connection(bootstrap=True) + self.add(conn) + self.assertEqual(MAINTENANCE.run_routed_style(conn, force=True).outcome, "success") + with patch.object(STYLE, "_compute_entity_style_vectors", side_effect=failure("synthetic failure")): + prior = MAINTENANCE.run_routed_style(conn, force=True) + self.assertEqual(prior.coverage, "complete") + baseline = self.checkpoint(conn) + if change == "hash": + conn.execute("UPDATE metadata SET value='1' WHERE key='style_vec_hash_version'") + elif change == "format": + conn.execute("ALTER TABLE entity_style_vectors DROP COLUMN accumulator_version") + conn.execute("ALTER TABLE entity_style_vectors ADD COLUMN accumulator_version INTEGER DEFAULT 9") + conn.commit() + output = self.rows(conn) + if borrowed: + conn.execute("INSERT INTO sentinel VALUES ('caller pending')") + if not force: + for _ in range(25): + STORAGE.insert_message(conn, {"content": "synthetic append", "sender": "Synthetic"}) + if not borrowed: + conn.commit() + compute = STYLE._compute_entity_style_vectors + with patch.object(STYLE, "_compute_entity_style_vectors", wraps=compute, + side_effect=failure("synthetic failure") if change == "intact" else None): + result = MAINTENANCE.run_routed_style(conn, force=force, allow_caller_transaction=borrowed) + expected = ("success" if change == "hash" else "unavailable" if change == "format" + or failure is MAINTENANCE.LayerUnavailableError else "failed") + self.assertEqual((result.outcome, result.coverage, result.attempted), + (expected, "unverified" if change == "format" else "complete", True)) + self.assertEqual(self.checkpoint(conn)[coverage_index], "complete") + self.assertEqual(conn.in_transaction, borrowed) + if change != "hash": + self.assertEqual(self.rows(conn), output) + if borrowed: + self.assertEqual(conn.execute("SELECT * FROM sentinel").fetchall(), [("caller pending",)]) + conn.rollback() + self.assertEqual(self.checkpoint(conn), baseline) + self.assertEqual(self.rows(conn), output) + + def test_canonical_style_rejects_preexisting_and_late_temp_table_shadows(self) -> None: + for table in ("messages", "maintenance_source_state", "maintenance_layers", "metadata", "entity_style_vectors"): + for kind in ("TABLE", "VIEW"): + for late in (False, True): + for borrowed in (False, True): + with self.subTest(table=table, kind=kind, late=late, borrowed=borrowed): + conn = self.connection(bootstrap=True) + baseline = self.checkpoint(conn) + if borrowed: + conn.execute("INSERT INTO sentinel VALUES ('caller pending')") + def shadow() -> None: + conn.execute(f"CREATE TEMP {kind} {table.upper()} AS SELECT * FROM main.{table}") + if not borrowed: + conn.commit() + prepare = MAINTENANCE._prepare_routed_style_maintenance + def after_prepare(*args: object, **kwargs: object) -> None: + prepare(*args, **kwargs) + shadow() + if not late: + shadow() + before = conn.total_changes + with patch.object(STYLE, "_compute_entity_style_vectors", side_effect=AssertionError), self.no_native(): + if late: + with patch.object(MAINTENANCE, "_prepare_routed_style_maintenance", after_prepare): + result = MAINTENANCE.run_routed_style(conn, allow_caller_transaction=borrowed) + else: + result = MAINTENANCE.run_routed_style(conn, allow_caller_transaction=borrowed) + self.assertEqual((result.error_category, result.coverage, result.attempted), + ("StyleSourceChanged" if late else "StyleSourceUnavailable", "unverified", False)) + self.assertEqual(conn.total_changes, before) + self.assertEqual(conn.execute("SELECT " + ",".join(MAINTENANCE._LAYER_ROW_COLUMNS) + + " FROM main.maintenance_layers WHERE layer='style_vectors'").fetchone(), baseline) + self.assertEqual(conn.in_transaction, borrowed) + if borrowed: + self.assertEqual(conn.execute("SELECT * FROM sentinel").fetchall(), [("caller pending",)]) + conn.rollback() + + def test_canonical_style_rejects_case_variant_source_trigger_shadows(self) -> None: + for late in (False, True): + for borrowed in (False, True): + with self.subTest(late=late, borrowed=borrowed): + conn = self.connection(bootstrap=True) + baseline = self.checkpoint(conn) + if borrowed: + conn.execute("INSERT INTO sentinel VALUES ('caller pending')") + def shadow() -> None: + conn.execute("CREATE TEMP TRIGGER MESSAGES_MAINTENANCE_AI AFTER INSERT ON main.messages BEGIN SELECT 1; END") + if not borrowed: + conn.commit() + prepare = MAINTENANCE._prepare_routed_style_maintenance + def after_prepare(*args: object, **kwargs: object) -> None: + prepare(*args, **kwargs) + shadow() + if not late: + shadow() + before = conn.total_changes + with patch.object(STYLE, "_compute_entity_style_vectors", side_effect=AssertionError): + if late: + with patch.object(MAINTENANCE, "_prepare_routed_style_maintenance", after_prepare): + result = MAINTENANCE.run_routed_style(conn, allow_caller_transaction=borrowed) + else: + result = MAINTENANCE.run_routed_style(conn, allow_caller_transaction=borrowed) + self.assertEqual((result.error_category, result.coverage, result.attempted), + ("StyleSourceChanged" if late else "StyleSourceUnavailable", "unverified", False)) + self.assertEqual(conn.total_changes, before) + self.assertEqual(self.checkpoint(conn), baseline) + self.assertEqual(conn.in_transaction, borrowed) + if borrowed: + self.assertEqual(conn.execute("SELECT * FROM sentinel").fetchall(), [("caller pending",)]) + conn.rollback() + + def test_current_style_read_rollback_failure_is_not_a_safe_deferral(self) -> None: + for failure in ("raises", "no_op"): + with self.subTest(failure=failure): + conn = self.connection(bootstrap=True) + baseline = self.checkpoint(conn) + armed = False + observe = MAINTENANCE.observe_style + def observed(*args: object, **kwargs: object) -> object: + nonlocal armed + result = observe(*args, **kwargs) + armed = True + return result + class Connection: + def __getattr__(self, name: str) -> object: + return getattr(conn, name) + def rollback(self) -> None: + if armed: + if failure == "raises": + raise sqlite3.OperationalError("synthetic read rollback failure") + return + conn.rollback() + with patch.object(MAINTENANCE, "observe_style", observed): + with self.assertRaises(MAINTENANCE._LayerRollbackFailed): + MAINTENANCE.run_routed_style(Connection()) + self.assertTrue(conn.in_transaction) + conn.rollback() + self.assertEqual(self.checkpoint(conn), baseline) + + def test_in_memory_pending_and_caller_transaction_do_not_start_workers(self) -> None: + engine = self.engine(self.connection()) + with patch.object(MAINTENANCE.threading, "Thread", side_effect=AssertionError): + engine._maybe_auto_consolidate() + self.assertEqual(engine._maintenance_coordinator.status[0], "pending_in_memory") + engine.conn.execute("INSERT INTO sentinel VALUES ('caller')") + with patch.object(engine._maintenance_coordinator, "request_layers") as requested: + engine._maybe_auto_consolidate() + requested.assert_not_called() + self.assertEqual(engine.get_style_health()["pending_reason"], "pending_caller_commit") + engine.conn.rollback() + + def test_borrowed_success_rollback_preserves_failure_and_original_handle(self) -> None: + conn = self.connection(bootstrap=True) + coordinator = MAINTENANCE.MaintenanceCoordinator(None) + failure = MAINTENANCE.LayerResult("style_vectors", "style_vectors", "failed", 0, 0.0, "SyntheticFailure", True, "unverified") + token = coordinator._begin_observation() + old_report = MAINTENANCE.MaintenanceReport((), "SKIPPED", failure) + coordinator._finish_observation(token, old_report, "failed", "SyntheticFailure") + before = self.checkpoint(conn) + conn.execute("INSERT INTO sentinel VALUES ('caller')") + with MAINTENANCE.maintenance_observation(coordinator, borrowed=True) as reports: + report = MAINTENANCE.run_engine_maintenance(conn, coordinator, force=True, include_layers=False, + include_style=True, allow_caller_transaction=True) + reports.append(report) + self.assertEqual(MAINTENANCE.maintenance_report_status(report)[0], "pending") + self.assertIs(coordinator._last_report, old_report) + self.assertEqual(MAINTENANCE.style_health(conn, coordinator)["last_error"], "SyntheticFailure") + self.assertTrue(conn.in_transaction) + conn.rollback() + self.assertEqual(self.checkpoint(conn), before) + self.assertEqual(coordinator.status, ("failed", "SyntheticFailure")) + self.assertEqual(conn.execute("SELECT count(*) FROM sentinel").fetchone(), (0,)) + + def test_historical_success_cannot_hide_new_invalidation_and_committed_success_clears_health(self) -> None: + conn = self.connection(bootstrap=True) + self.add(conn) + coordinator = MAINTENANCE.MaintenanceCoordinator(None) + with MAINTENANCE.maintenance_observation(coordinator) as reports: + reports.append(MAINTENANCE.run_engine_maintenance(conn, coordinator, force=True, include_layers=False, include_style=True)) + conn.execute("DELETE FROM entity_style_vectors") + conn.commit() + self.assertNotEqual(MAINTENANCE.style_health(conn, coordinator)["freshness"], "current") + token = coordinator._begin_observation() + coordinator._finish_observation(token, None, "failed", "SyntheticFailure") + self.assertEqual(MAINTENANCE.run_routed_style(conn).outcome, "success") + self.assertNotIn("last_error", MAINTENANCE.style_health(conn, coordinator)) + + def test_explicit_consolidate_preserves_nine_keys_and_original_connection(self) -> None: + conn = self.connection() + engine = self.engine(conn) + importer = types.SimpleNamespace(import_module=lambda name: STYLE if name == "truememory.personality_style_vec" + else types.SimpleNamespace(extract_preferences=lambda _conn: None)) + with patch.object(MAINTENANCE, "engine_layer_specs", public_specs), patch.object(MAINTENANCE, "importlib", importer), \ + patch.object(MAINTENANCE, "_installed_dependency", return_value=None): + result = engine.consolidate() + with patch.object(engine, "get_clustering_health", return_value={"status": "synthetic"}): + stats = engine.get_stats() + self.assertEqual(set(result), set(MAINTENANCE._MAINTENANCE_RESULT_KEYS)) + self.assertEqual(len(result), 9) + self.assertEqual(len(STORAGE._MAINTENANCE_LAYERS), 8) + self.assertEqual(len(stats["maintenance"]["layers"]), 8) + self.assertNotIn("style_vectors", stats["maintenance"]["layers"]) + self.assertEqual(stats["maintenance"]["style"]["outcome"], "success_empty") + self.assertIs(engine.conn, conn) + self.assertEqual(engine._maintenance_coordinator._last_report.style_result.outcome, "success_empty") + self.assertFalse(conn.in_transaction) + + def test_explicit_style_failure_changes_aggregate_and_private_health(self) -> None: + conn = self.connection() + engine = self.engine(conn) + importer = types.SimpleNamespace(import_module=lambda name: STYLE if name == "truememory.personality_style_vec" + else types.SimpleNamespace(extract_preferences=lambda _conn: None)) + with patch.object(MAINTENANCE, "engine_layer_specs", public_specs), patch.object(MAINTENANCE, "importlib", importer), \ + patch.object(MAINTENANCE, "_installed_dependency", return_value=None), \ + patch.object(STYLE, "_compute_entity_style_vectors", side_effect=ValueError("synthetic failure")): + result = engine.consolidate() + self.assertEqual(len(result), 9) + self.assertEqual(engine._maintenance_coordinator.status, ("failed", "ValueError")) + self.assertEqual(engine.get_style_health()["outcome"], "failed") + + def test_owned_manual_preserves_foreground_temp_rejection_on_dedicated_handle(self) -> None: + conn = self.connection() + self.add(conn) + conn.execute("CREATE TEMP TRIGGER synthetic_foreground AFTER INSERT ON main.metadata BEGIN SELECT 1; END") + conn.commit() + engine = self.engine(conn) + coordinator = MAINTENANCE.MaintenanceCoordinator(ROOT / "synthetic-manual-not-created.sqlite") + engine._maintenance_coordinator = coordinator + dedicated = self.connection() + conn.backup(dedicated) + self.assertFalse(MAINTENANCE._style_extra_triggers(dedicated)) + events = [] + class Owned: + def __getattr__(self, name: str) -> object: + return getattr(dedicated, name) + def close(self) -> None: + events.append("close") + importer = types.SimpleNamespace(import_module=lambda name: STYLE if name == "truememory.personality_style_vec" + else types.SimpleNamespace(extract_preferences=lambda _conn: None)) + with patch.object(engine._engine_test_module, "create_db", return_value=Owned()), \ + patch.object(MAINTENANCE, "engine_layer_specs", public_specs), patch.object(MAINTENANCE, "importlib", importer), \ + patch.object(MAINTENANCE, "_prepare_worker_extensions", return_value=None), \ + patch.object(MAINTENANCE, "_installed_dependency", return_value=None), \ + patch.object(MAINTENANCE, "_acquire_owner", return_value=123), \ + patch.object(MAINTENANCE, "_release_owner", side_effect=lambda _fd: events.append("release")): + result = engine.consolidate() + self.assertEqual(len(result), 9) + self.assertEqual(len(coordinator._last_report.results), 8) + self.assertEqual(coordinator._last_report.style_result.error_category, "StyleTriggersUnsupported") + self.assertEqual(coordinator._last_report.style_result.outcome, "deferred") + self.assertEqual(events, ["close", "release"]) + self.assertEqual(self.rows(dedicated), []) + self.assertIsNone(self.checkpoint(dedicated)) + + def test_delete_normalizes_only_the_style_entity_key(self) -> None: + conn = self.connection(bootstrap=True) + engine = self.engine(conn) + engine.add("synthetic source", sender="Synthetic") + self.assertEqual(conn.execute("SELECT entity FROM entity_style_vectors").fetchone(), ("synthetic",)) + conn.executemany("INSERT INTO entity_profiles(entity) VALUES (?)", [("Synthetic",), ("synthetic",)]) + conn.executemany("INSERT INTO summaries(entity,summary) VALUES (?,'synthetic summary')", [("Synthetic",), ("synthetic",)]) + conn.commit() + namespace = STORAGE.__dict__ + original = namespace["__builtins__"]["__import__"] + def safe_import(name: str, *args: object, **kwargs: object) -> object: + if name == "truememory.tier_config": + return types.SimpleNamespace(VALID_TIER_GROUPS=()) + return original(name, *args, **kwargs) + with patch.dict(namespace["__builtins__"], __import__=safe_import): + self.assertTrue(STORAGE.delete_message(conn, 1)) + self.assertEqual(self.rows(conn), []) + self.assertEqual(conn.execute("SELECT entity FROM entity_profiles").fetchall(), [("synthetic",)]) + self.assertEqual(conn.execute("SELECT entity FROM summaries").fetchall(), [("synthetic",)]) + self.assertTrue(MAINTENANCE.observe_style(conn).eligible) + + def test_actual_bulk_style_stage_runs_once_and_reports_borrowed_success(self) -> None: + for borrowed in (False, True): + with self.subTest(borrowed=borrowed): + conn = self.connection() + engine = self.engine(conn) + module = engine._engine_test_module + module._HAS_CONSOLIDATION = False + def load(connection: sqlite3.Connection, _path: Path) -> int: + STORAGE.insert_message(connection, {"content": "synthetic bulk", "sender": "Synthetic", "directive": False}) + if not borrowed: + connection.commit() + return 1 + with patch.object(module, "create_db", return_value=conn), \ + patch.object(module, "load_messages_from_file", load), \ + patch.object(engine._maintenance_coordinator, "observe_clustering_outcome"), \ + patch.object(STYLE, "_compute_entity_style_vectors", wraps=STYLE._compute_entity_style_vectors) as builds: + stats = engine.ingest("synthetic-inline-boundary") + self.assertEqual(builds.call_count, 1) + self.assertTrue(stats["build_style_vectors"].startswith("1 vectors")) + self.assertEqual("pending caller commit" in stats["build_style_vectors"], borrowed) + if not borrowed: + engine.add("synthetic after bulk", sender="Synthetic") + self.assertEqual(builds.call_count, 1) + self.assertEqual(conn.execute("SELECT message_count FROM entity_style_vectors").fetchone(), (2,)) + if borrowed: + self.assertIsNone(engine._maintenance_coordinator._last_report) + conn.rollback() + self.assertIsNone(self.checkpoint(conn)) + self.assertEqual(self.rows(conn), []) + + def test_deprecated_open_schedules_without_synchronous_style_build_or_hash_claim(self) -> None: + conn = self.connection() + probe = self.connection() + engine = self.engine(conn) + module = engine._engine_test_module + engine.db_path = (ROOT / "synthetic-existing-not-created.sqlite") + exists = Path.exists + closed_owners = [] + owner_os = types.SimpleNamespace(getpid=os.getpid, fspath=os.fspath, close=closed_owners.append) + with patch.object(Path, "exists", lambda path: True if path == engine.db_path else exists(path)), \ + patch.object(module.sqlite3, "connect", side_effect=[probe, conn]) as opened, \ + patch.object(MAINTENANCE, "try_file_lock", return_value=719), \ + patch.object(MAINTENANCE, "os", owner_os), \ + patch.object(engine, "_purge_legacy_entity_profile_summaries"), \ + patch.object(engine, "_maybe_auto_consolidate") as planned, \ + patch.object(STYLE, "_compute_entity_style_vectors", side_effect=AssertionError): + with self.assertWarns(DeprecationWarning): + self.assertIs(engine.open(rebuild_vectors=False), engine) + planned.assert_called_once() + self.assertIs(engine.conn, conn) + self.assertEqual(opened.call_count, 2) + self.assertEqual(closed_owners, [719]) + with self.assertRaises(sqlite3.ProgrammingError): + probe.execute("SELECT 1") + self.assertIsNone(conn.execute("SELECT value FROM metadata WHERE key='style_vec_hash_version'").fetchone()) + self.assertIsNone(self.checkpoint(conn)) + + def test_manual_pending_result_and_health_do_not_clear_prior_completed_failure(self) -> None: + conn = self.connection(bootstrap=True) + engine = self.engine(conn) + coordinator = engine._maintenance_coordinator + token = coordinator._begin_observation() + coordinator._finish_observation(token, MAINTENANCE.MaintenanceReport((), "SKIPPED"), "failed", "PriorFailure") + conn.execute("INSERT INTO sentinel VALUES ('caller')") + personality = types.SimpleNamespace(extract_preferences=lambda _conn: None) + importer = types.SimpleNamespace(import_module=lambda name: STYLE if name == "truememory.personality_style_vec" + else personality) + with patch.object(MAINTENANCE, "engine_layer_specs", public_specs), patch.object(MAINTENANCE, "importlib", importer), \ + patch.object(MAINTENANCE, "_installed_dependency", return_value=None), \ + patch.object(personality, "extract_preferences", + side_effect=AssertionError("unsupported preference work")) as preferences: + result = engine.consolidate() + preferences.assert_not_called() + self.assertEqual(len(result), 9) + self.assertEqual(set(result), set(MAINTENANCE._MAINTENANCE_RESULT_KEYS)) + self.assertTrue(all("pending caller commit" in result[key] + for key in MAINTENANCE._MAINTENANCE_RESULT_KEYS if key != "extract_preferences"), result) + self.assertEqual(result["extract_preferences"], "UNAVAILABLE (ScheduledPreferencesUnsupported)") + self.assertEqual(coordinator.status, ("failed", "PriorFailure")) + self.assertEqual(engine.get_style_health()["last_error"], "PriorFailure") + conn.rollback() + + def test_routed_owned_commit_and_borrowed_release_failures_keep_previous_output(self) -> None: + for borrowed in (False, True): + for cache in (0, 5, 100): + with self.subTest(borrowed=borrowed, cache=cache): + conn = sqlite3.connect(":memory:", cached_statements=cache) + self.addCleanup(conn.close) + conn.executescript(STORAGE._SCHEMA_SQL) + STORAGE._initialize_maintenance_tracking(conn) + conn.commit() + self.add(conn) + MAINTENANCE.run_routed_style(conn) + before, checkpoint = self.rows(conn), self.checkpoint(conn) + if borrowed: + conn.execute("INSERT INTO metadata(key,value) VALUES ('synthetic-caller','pending')") + wrapper = BASE["BASE"]["PublicationFailure"](conn, borrowed=borrowed) + result = MAINTENANCE.run_routed_style(wrapper, force=True, allow_caller_transaction=borrowed) + self.assertEqual(result.outcome, "failed") + self.assertEqual(wrapper.denied, 1) + self.assertEqual(self.rows(conn), before) + self.assertEqual(self.checkpoint(conn)[3:7], checkpoint[3:7]) + self.assertEqual(conn.in_transaction, borrowed) + if borrowed: + self.assertEqual(conn.execute("SELECT value FROM metadata WHERE key='synthetic-caller'").fetchone(), ("pending",)) + conn.rollback() + self.assertEqual(self.checkpoint(conn), checkpoint) + + def test_cancellation_after_style_compute_prevents_public_stages(self) -> None: + conn = self.connection(bootstrap=True) + before = self.checkpoint(conn) + cancel = threading.Event() + compute = STYLE._compute_entity_style_vectors + def stop_after_compute(*args: object, **kwargs: object) -> object: + result = compute(*args, **kwargs) + cancel.set() + return result + with patch.object(STYLE, "_compute_entity_style_vectors", stop_after_compute), \ + patch.object(MAINTENANCE, "_prepare_worker_extensions", side_effect=AssertionError): + report = MAINTENANCE.run_engine_maintenance(conn, MAINTENANCE.MaintenanceCoordinator(None), + include_style=True, force=True, cancel=cancel) + self.assertEqual(report.results, ()) + self.assertEqual(report.style_result.error_category, "StyleCancelled") + self.assertEqual(self.checkpoint(conn), before) + self.assertFalse(conn.in_transaction) + + def test_health_does_not_connect_when_disconnected_or_foreground_busy(self) -> None: + engine = self.engine(self.connection()) + with patch.object(engine, "_ensure_connection", side_effect=AssertionError), \ + patch.object(engine, "_maybe_auto_consolidate", side_effect=AssertionError): + with engine._write_lock: + self.assertEqual(engine.get_style_health()["pending_reason"], "foreground_busy") + engine.conn = None + self.assertEqual(engine.get_style_health()["pending_reason"], "not_connected") + + def test_integrity_opener_faults_clear_handler_and_close_before_returning(self) -> None: + for failure in ("busy", "cancel", "io", "unknown", "corrupt", "empty", "config"): + with self.subTest(failure=failure): + conn = self.connection() + cancel = threading.Event() + events = [] + handlers = [] + class Connection: + row_factory = None + def __getattr__(self, name: str) -> object: + return getattr(conn, name) + def close(self) -> None: + events.append("close") + def set_progress_handler(self, callback: object, interval: int) -> None: + handlers.append(callback) + events.append("clear" if callback is None else "install") + def execute(self, sql: str, *args: object) -> object: + if failure == "config" and sql == "PRAGMA foreign_keys=ON": + raise sqlite3.OperationalError("synthetic configuration") + if sql == "PRAGMA quick_check(1)": + if failure == "cancel": + cancel.set() + self_check = handlers[-1]() + if self_check != 1: + raise AssertionError("Owned cancellation handler did not stop") + raise sqlite3.OperationalError("interrupted") + if failure in {"corrupt", "empty"}: + return types.SimpleNamespace(fetchone=lambda: None if failure == "empty" else ("synthetic corruption",)) + raise sqlite3.OperationalError({"busy": "database is locked", "io": "disk I/O error", + "unknown": "synthetic unexpected"}[failure]) + return conn.execute(sql, *args) + with patch.object(MAINTENANCE.sqlite3, "connect", return_value=Connection()): + expected = MAINTENANCE._StyleOpenRejected if failure in {"busy", "cancel"} else STORAGE.DatabaseOpenError + with self.assertRaises(expected) as raised: + MAINTENANCE._open_style_database((ROOT / "synthetic-not-created.sqlite"), cancel) + self.assertEqual(events[-1], "close") + if failure != "config": + self.assertEqual(events, ["install", "clear", "close"]) + if failure in {"busy", "cancel"}: + self.assertEqual(raised.exception.result.error_category, "StyleWriterBusy" if failure == "busy" else "StyleCancelled") + + def test_integrity_opener_preflight_forbids_general_setup_and_profile_mutation(self) -> None: + conn = self.connection(bootstrap=True) + self.add(conn) + MAINTENANCE.run_routed_style(conn, force=True) + conn.execute("DELETE FROM maintenance_layers WHERE layer='landmarks'") + conn.execute("CREATE TRIGGER synthetic_initializer BEFORE INSERT ON maintenance_layers BEGIN " + "UPDATE entity_style_vectors SET message_count=message_count+1; END") + conn.commit() + class Connection: + row_factory = None + def __getattr__(self, name: str) -> object: + return getattr(conn, name) + def close(self) -> None: + pass + def execute(self, sql: str, *args: object) -> object: + if sql == "PRAGMA quick_check(1)": + raise AssertionError("Unsupported opener cannot claim integrity checking") + return conn.execute(sql, *args) + before = conn.total_changes + with patch.object(MAINTENANCE.sqlite3, "connect", return_value=Connection()), \ + patch.object(MAINTENANCE, "create_db", side_effect=AssertionError): + with self.assertRaises(MAINTENANCE._StyleOpenRejected): + MAINTENANCE._open_style_database((ROOT / "synthetic-not-created.sqlite")) + self.assertEqual(conn.total_changes, before) + self.assertEqual(conn.execute("SELECT message_count FROM entity_style_vectors").fetchone(), (1,)) + # Actual general initialization is the negative control: all eight + # BEFORE INSERT triggers run, even for rows ignored by their conflict. + STORAGE._initialize_maintenance_tracking(conn) + self.assertEqual(conn.execute("SELECT message_count FROM entity_style_vectors").fetchone(), (9,)) + + +class CoordinatorRouting(unittest.TestCase): + connection = InMemoryRouting.connection + no_native = InMemoryRouting.no_native + + def test_started_worker_binding_failure_reports_error_and_preserves_newer_evidence(self) -> None: + for newer in (False, True): + with self.subTest(newer=newer): + coordinator = MAINTENANCE.MaintenanceCoordinator(ROOT / "synthetic-binding-not-created.sqlite") + old_report = MAINTENANCE.MaintenanceReport((), "SKIPPED previous") + newer_report = MAINTENANCE.MaintenanceReport((), "SKIPPED newer") + coordinator._last_report = old_report + def fail_identity() -> None: + if newer: + token = coordinator._begin_observation() + coordinator._finish_observation(token, newer_report, "failed", "NewerManual") + raise OSError("synthetic identity failure") + with patch.object(MAINTENANCE, "_acquire_owner", return_value=123), \ + patch.object(MAINTENANCE, "_release_owner") as released, \ + patch.object(MAINTENANCE.uuid, "uuid4", fail_identity), \ + patch.object(MAINTENANCE, "create_db", side_effect=AssertionError) as opened: + self.assertTrue(coordinator.request(lambda *_args: self.fail("Binding failure ran work"))) + self.assertTrue(coordinator.wait(2)) + opened.assert_not_called() + released.assert_called_once_with(123) + self.assertEqual(coordinator.status, ("failed", "NewerManual" if newer else "OSError")) + self.assertIs(coordinator._last_report, newer_report if newer else old_report) + self.assertFalse(coordinator._active) + self.assertIsNone(coordinator._thread) + + def test_mixed_notifications_union_flags_and_keep_latest_threshold(self) -> None: + for reverse in (False, True): + with self.subTest(reverse=reverse): + coordinator = MAINTENANCE.MaintenanceCoordinator((ROOT / "synthetic-not-created.sqlite")) + coordinator._active = True + requests = [(True, False), (False, True)] + if reverse: + requests.reverse() + for index, (layers, style) in enumerate(requests): + self.assertFalse(coordinator.request_layers(threshold=24 + index, include_layers=layers, include_style=style)) + self.assertEqual(coordinator._pending, MAINTENANCE.MaintenanceRequest(2, 25, True, True)) + with self.assertRaises(ValueError): + coordinator.request_layers(include_layers=False, include_style=False) + + def test_busy_admission_merges_consumed_kind_with_newer_pending_kind(self) -> None: + coordinator = MAINTENANCE.MaintenanceCoordinator((ROOT / "synthetic-not-created.sqlite")) + def busy(_path: Path, on_busy: object) -> None: + coordinator.request_layers(threshold=31, include_layers=False, include_style=True) + on_busy() + with patch.object(MAINTENANCE, "_acquire_owner", busy): + self.assertFalse(coordinator.request_layers(threshold=25)) + self.assertEqual(coordinator._pending, MAINTENANCE.MaintenanceRequest(2, 31, True, True)) + + def test_worker_start_failure_unions_real_new_wake_without_self_retry(self) -> None: + for newer in (False, True): + with self.subTest(newer=newer): + coordinator = MAINTENANCE.MaintenanceCoordinator((ROOT / "synthetic-not-created.sqlite")) + coordinator._active = True + def start() -> None: + if newer: + coordinator.request_layers(threshold=30, include_layers=False, include_style=True) + raise RuntimeError("synthetic start") + with patch.object(MAINTENANCE, "_acquire_owner", return_value=123), \ + patch.object(MAINTENANCE, "_release_owner") as released, \ + patch.object(MAINTENANCE.threading, "Thread", return_value=types.SimpleNamespace(start=start)), \ + patch.object(coordinator, "_launch_layers") as successor: + with self.assertRaises(RuntimeError): + coordinator._launch(lambda *_args: None, MAINTENANCE.MaintenanceRequest(0, 25)) + released.assert_called_once_with(123) + self.assertEqual(successor.call_count, int(newer)) + if newer: + self.assertEqual(successor.call_args.args[0], MAINTENANCE.MaintenanceRequest(1, 30, True, True)) + + def test_observation_tokens_reject_late_completion_and_borrowed_results(self) -> None: + coordinator = MAINTENANCE.MaintenanceCoordinator(None) + one, two = coordinator._begin_observation(), coordinator._begin_observation() + newer = MAINTENANCE.MaintenanceReport((), "SKIPPED") + coordinator._finish_observation(two, newer, "failed", "NewerFailure") + coordinator._finish_observation(one, None, "success", None) + self.assertEqual(coordinator.status, ("failed", "NewerFailure")) + self.assertIs(coordinator._last_report, newer) + three = coordinator._begin_observation() + coordinator._finish_observation(three, MAINTENANCE.MaintenanceReport((), "SKIPPED"), "success", None, borrowed=True) + self.assertIs(coordinator._last_report, newer) + + def test_launched_opener_is_immutable_and_style_worker_never_initializes_general_schema(self) -> None: + coordinator = MAINTENANCE.MaintenanceCoordinator((ROOT / "synthetic-not-created.sqlite")) + with patch.object(coordinator, "_launch", return_value=True) as launch: + coordinator._launch_layers(MAINTENANCE.MaintenanceRequest(1, 25, False, True)) + work = launch.call_args.args[0] + self.assertEqual(launch.call_args.kwargs, {"style_only": True}) + coordinator._pending = MAINTENANCE.MaintenanceRequest(2, 30, True, False) + conn = self.connection() + events = [] + class Connection: + def __getattr__(self, name: str) -> object: + return getattr(conn, name) + def close(self) -> None: + events.append("close") + with patch.object(MAINTENANCE, "_open_style_database", return_value=Connection()) as opener, \ + patch.object(MAINTENANCE, "create_db", side_effect=AssertionError), \ + patch.object(MAINTENANCE, "_release_owner", side_effect=lambda _fd: events.append("release")), \ + patch.object(coordinator, "_launch_layers") as successor, self.no_native(): + coordinator._run(123, work, style_only=True) + opener.assert_called_once() + self.assertEqual(events, ["close", "release"]) + self.assertEqual(coordinator._last_report.style_result.outcome, "success_empty") + successor.assert_called_once_with(MAINTENANCE.MaintenanceRequest(2, 30, True, False)) + + def test_late_worker_cleanup_cannot_replace_newer_manual_failure(self) -> None: + coordinator = MAINTENANCE.MaintenanceCoordinator(ROOT / "synthetic-worker-cleanup-not-created.sqlite") + conn = self.connection() + events = [] + class Connection: + def __getattr__(self, name: str) -> object: + return getattr(conn, name) + def close(self) -> None: + events.append("close") + token = coordinator._begin_observation() + coordinator._finish_observation(token, None, "failed", "NewerFailure") + exists = Path.exists + with patch.object(Path, "exists", lambda path: False if path == coordinator.path else exists(path)), \ + patch.object(MAINTENANCE, "create_db", return_value=Connection()), \ + patch.object(MAINTENANCE, "_release_owner", side_effect=lambda _fd: events.append("release")): + coordinator._run(123, lambda *_args: MAINTENANCE.MaintenanceReport((), "SKIPPED")) + self.assertEqual(events, ["close", "release"]) + self.assertEqual(coordinator.status, ("failed", "NewerFailure")) + + def test_setup_and_close_failures_retain_report_and_release_after_cleanup(self) -> None: + for boundary in ("setup", "close"): + with self.subTest(boundary=boundary): + coordinator = MAINTENANCE.MaintenanceCoordinator(ROOT / "synthetic-worker-failure-not-created.sqlite") + old = MAINTENANCE.MaintenanceReport((), "SKIPPED") + coordinator._last_report = old + conn = self.connection() + events = [] + class Connection: + def __getattr__(self, name: str) -> object: + return getattr(conn, name) + def close(self) -> None: + events.append("close") + raise sqlite3.OperationalError("synthetic close") + exists = Path.exists + with patch.object(Path, "exists", lambda path: False if path == coordinator.path else exists(path)), \ + patch.object(MAINTENANCE, "create_db", side_effect=RuntimeError("synthetic setup") if boundary == "setup" else None, + return_value=Connection()), \ + patch.object(MAINTENANCE, "_release_owner", side_effect=lambda _fd: events.append("release")): + coordinator._run(123, lambda *_args: MAINTENANCE.MaintenanceReport((), "SKIPPED new")) + self.assertIs(coordinator._last_report, old) + self.assertEqual(coordinator.status, ("failed", "RuntimeError" if boundary == "setup" else "OperationalError")) + self.assertEqual(events, ["release"] if boundary == "setup" else ["close", "release"]) + + def test_unstarted_failure_cannot_overwrite_manual_observation_after_owner_release(self) -> None: + coordinator = MAINTENANCE.MaintenanceCoordinator(ROOT / "synthetic-start-not-created.sqlite") + newer = MAINTENANCE.MaintenanceReport((), "SKIPPED newer") + class Unstarted: + def __init__(self, **_kwargs: object) -> None: + pass + def start(self) -> None: + raise RuntimeError("synthetic start failure") + def released(_fd: int) -> None: + token = coordinator._begin_observation() + coordinator._finish_observation(token, newer, "failed", "NewerManual") + with patch.object(MAINTENANCE, "_acquire_owner", return_value=123), \ + patch.object(MAINTENANCE, "_release_owner", released), \ + patch.object(MAINTENANCE.threading, "Thread", Unstarted): + with self.assertRaisesRegex(RuntimeError, "synthetic start failure"): + coordinator.request_layers(include_layers=False, include_style=True) + self.assertEqual(coordinator.status, ("failed", "NewerManual")) + self.assertIs(coordinator._last_report, newer) + self.assertFalse(coordinator._active) + self.assertTrue(coordinator.wait(0)) + + +class FileOpenerRouting(unittest.TestCase): + def setUp(self) -> None: + self.directory = tempfile.TemporaryDirectory(prefix="synthetic-style-routing-") + self.addCleanup(self.directory.cleanup) + self.path = Path(self.directory.name) / "synthetic.sqlite" + conn = STORAGE.create_db(self.path) + conn.close() + + def test_uri_filenames_settings_and_missing_file(self) -> None: + for suffix in ("hash#mark", "question?mark", "percent%mark", "space mark", "unicode-é"): + with self.subTest(suffix=suffix): + if os.name == "nt" and "?" in suffix: + self.skipTest("Question marks are not valid Windows filenames") + path = self.path.with_name(suffix + ".sqlite") + original = STORAGE.create_db(path) + journal = original.execute("PRAGMA journal_mode").fetchone() + original.close() + conn = MAINTENANCE._open_style_database(path) + try: + self.assertIsNone(conn.row_factory) + self.assertEqual(conn.execute("PRAGMA busy_timeout").fetchone(), (10000,)) + self.assertEqual(conn.execute("PRAGMA foreign_keys").fetchone(), (1,)) + self.assertEqual(conn.execute("PRAGMA journal_mode").fetchone(), journal) + self.assertEqual(MAINTENANCE.run_routed_style(conn, connection_owned=True).outcome, "success_empty") + finally: + conn.close() + missing = self.path.with_name("missing.sqlite") + with self.assertRaises(STORAGE.DatabaseOpenError): + MAINTENANCE._open_style_database(missing) + self.assertFalse(missing.exists()) + + def test_unsupported_guard_precedes_general_initializer_side_effects(self) -> None: + conn = sqlite3.connect(self.path) + conn.execute("CREATE TABLE sentinel(value TEXT)") + conn.execute("DELETE FROM maintenance_layers WHERE layer='landmarks'") + conn.execute("CREATE TRIGGER synthetic_extra BEFORE INSERT ON maintenance_layers BEGIN INSERT INTO sentinel VALUES ('unexpected'); END") + conn.commit() + before = conn.total_changes + with self.assertRaises(MAINTENANCE._StyleOpenRejected) as raised: + MAINTENANCE._open_style_database(self.path) + self.assertEqual(raised.exception.result.error_category, "StyleTriggersUnsupported") + self.assertEqual(conn.total_changes, before) + self.assertEqual(conn.execute("SELECT count(*) FROM sentinel").fetchone(), (0,)) + self.assertIsNone(conn.execute("SELECT 1 FROM maintenance_layers WHERE layer='landmarks'").fetchone()) + conn.close() + + def test_missing_source_readiness_and_corruption_never_enroll_style(self) -> None: + for change in ("missing", "changed", "unready", "token", "schema", "metadata"): + with self.subTest(change=change): + path = self.path.with_name("synthetic-" + change + ".sqlite") + conn = STORAGE.create_db(path) + if change in {"missing", "changed"}: + conn.execute("DROP TRIGGER messages_maintenance_ai") + if change == "changed": + conn.execute("CREATE TRIGGER messages_maintenance_ai AFTER INSERT ON messages BEGIN SELECT 1; END") + elif change == "unready": + conn.execute("UPDATE maintenance_source_state SET tracking_ready=0") + elif change == "token": + conn.execute("DELETE FROM maintenance_source_state") + elif change == "schema": + conn.execute("ALTER TABLE messages RENAME COLUMN id TO synthetic_id") + else: + conn.execute("DROP TABLE metadata") + conn.commit() + conn.close() + with self.assertRaises(MAINTENANCE._StyleOpenRejected) as raised: + MAINTENANCE._open_style_database(path) + self.assertEqual(raised.exception.result.error_category, "StyleSourceUnavailable") + conn = sqlite3.connect(path) + self.assertIsNone(conn.execute("SELECT 1 FROM maintenance_layers WHERE layer='style_vectors'").fetchone()) + self.assertEqual(conn.execute("SELECT count(*) FROM entity_style_vectors").fetchone(), (0,)) + conn.close() + damaged = self.path.with_name("synthetic-corrupt.sqlite") + damaged.write_bytes(b"synthetic invalid database") + with self.assertRaises(STORAGE.DatabaseOpenError): + MAINTENANCE._open_style_database(damaged) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-landmark-resolution-755.py b/tests/test-landmark-resolution-755.py new file mode 100644 index 00000000..81a9056c --- /dev/null +++ b/tests/test-landmark-resolution-755.py @@ -0,0 +1,595 @@ +"""Synthetic actual-source landmark/temporal consumer controls, without startup. + +The provenance accessor collaborator tests the locked interface. Its producer +and metadata validation are covered independently by the source-provenance +module tests; these controls do not certify native/model or archive acceptance. +""" +from __future__ import annotations + +import ast +import copy +import hashlib +import json +import logging +import os +import sqlite3 +import sys +import types +import unittest +from contextlib import contextmanager +from pathlib import Path +from unittest.mock import Mock, patch + +ROOT = Path(__file__).resolve().parents[1] + + +def load_functions(name: str, selected: set[str], namespace: dict) -> types.ModuleType: + path = ROOT / "truememory" / (name + ".py") + module = types.ModuleType("synthetic_landmark755_" + name) + module.__dict__.update(namespace) + tree = ast.parse(path.read_text(), filename=str(path)) + body = [] + for node in tree.body: + if isinstance(node, ast.FunctionDef) and node.name in selected: + node.decorator_list = [] + body.append(node) + elif isinstance(node, ast.ClassDef): + for method in node.body: + if isinstance(method, ast.FunctionDef) and method.name in selected: + method.decorator_list = [] + body.append(method) + exec(compile(ast.Module(body=body, type_ignores=[]), str(path), "exec"), module.__dict__) + return module + + +STORAGE = types.ModuleType("synthetic_landmark755_storage") +exec(compile((ROOT / "truememory/storage.py").read_text(), "storage.py", "exec"), STORAGE.__dict__) +TEMPORAL = types.ModuleType("synthetic_landmark755_temporal") +TEMPORAL._row_to_dict = STORAGE._row_to_dict +TEMPORAL.select_message_cols = STORAGE.select_message_cols +tree = ast.parse((ROOT / "truememory/temporal.py").read_text()) +tree.body = [node for node in tree.body if not ( + isinstance(node, ast.ImportFrom) and node.module == "truememory.storage" +)] +exec(compile(tree, "temporal.py", "exec"), TEMPORAL.__dict__) + + +def interface_intervals(metadata: object) -> tuple[list[tuple[str, str]], bool] | None: + if not isinstance(metadata, dict) or "truememory_ingest" not in metadata: + return None + scope = metadata["truememory_ingest"] + if not isinstance(scope, dict) or scope.get("version") != 1: + return [], True + intervals = [] + unknown = False + for citation in scope.get("citations", []): + source_time = citation["scope"]["source_time"] + if source_time["status"] in {"known", "mixed"} and source_time.get("start") and source_time.get("end"): + intervals.append((source_time["start"], source_time["end"])) + unknown = unknown or source_time["status"] != "known" + return intervals, unknown + + +def metadata_intervals(*intervals: tuple[str, str], unknown: bool = False) -> dict: + citations = [{"scope": {"source_time": { + "status": "known", "start": start, "end": end, + }}} for start, end in intervals] + if unknown: + citations.append({"scope": {"source_time": {"status": "unknown", "start": None, "end": None}}}) + return {"truememory_ingest": {"version": 1, "timestamp_semantics": "source_scope_end", "citations": citations}} + + +@contextmanager +def provenance_interface(): + module = types.ModuleType("truememory.ingest.provenance") + module.source_intervals = interface_intervals + with patch.dict(sys.modules, {"truememory.ingest.provenance": module}): + yield + + +class LandmarkControls(unittest.TestCase): + def setUp(self) -> None: + self.conn = sqlite3.connect(":memory:") + self.addCleanup(self.conn.close) + self.conn.executescript( + "CREATE TABLE messages(id INTEGER PRIMARY KEY,content TEXT,sender TEXT,recipient TEXT," + "timestamp TEXT,category TEXT DEFAULT '',modality TEXT DEFAULT '',directive INTEGER DEFAULT 0,metadata TEXT DEFAULT '{}');" + "CREATE TABLE landmark_events(id INTEGER PRIMARY KEY,event_name TEXT,timestamp TEXT," + "event_type TEXT,related_entities TEXT DEFAULT '[]',source_message_id INTEGER);" + ) + + def event(self, phrase: str = "Demo Day", timestamp: str = "2025-06-15", sender: str = "synthetic-alice", + metadata: dict | None = None) -> int: + cursor = self.conn.execute( + "INSERT INTO messages(content,sender,recipient,timestamp,metadata) VALUES(?,?,?,?,?)", + (phrase + " arrived", sender, "", timestamp, json.dumps(metadata or {})), + ) + source_id = cursor.lastrowid + self.conn.execute( + "INSERT INTO landmark_events(event_name,timestamp,event_type,related_entities,source_message_id) VALUES(?,?,?,?,?)", + (phrase + " arrived", timestamp, "launch", json.dumps([sender]), source_id), + ) + return source_id + + def intent(self, query: str = "What happened in the first month after Demo Day?", sender: str | None = None) -> dict: + with provenance_interface(): + return TEMPORAL.resolve_temporal_intent(self.conn, query, sender) + + def test_named_event_resolves_live_source_and_existing_month(self) -> None: + source_id = self.event() + intent = self.intent() + self.assertEqual((intent["after"], intent["before"]), ("2025-06-15", "2025-07-16")) + diagnostic = intent["landmark_resolution"] + self.assertEqual(diagnostic["status"], "resolved") + self.assertEqual(diagnostic["candidates"][0]["source_message_id"], source_id) + self.assertEqual(diagnostic["candidates"][0]["timestamp_basis"], "source_message_timestamp") + + def test_before_and_week_year_windows(self) -> None: + self.event() + for query, after, before in [ + ("What happened before Demo Day?", None, "2025-06-15"), + ("What happened in the first week after Demo Day?", "2025-06-15", "2025-06-22"), + ("What happened in the first year after Demo Day?", "2025-06-15", "2026-06-15"), + ]: + with self.subTest(query=query): + intent = self.intent(query) + self.assertEqual((intent["after"], intent["before"]), (after, before)) + + def test_explicit_relative_date_precedence_no_landmark_lookup(self) -> None: + self.event(timestamp="2020-01-01") + for query in ["What happened after Demo Day (June 15, 2025)?", "What happened after January 15, 2026?", + "What happened after Demo Day last month?", "What happened yesterday?", + "What happened between March 5 2025 and March 5 2025?"]: + with self.subTest(query=query): + self.assertEqual(self.intent(query), TEMPORAL.detect_temporal_intent(query)) + + def test_multiple_dates_and_same_date_sources_are_ambiguous(self) -> None: + for second_date in ["2025-07-01", "2025-06-15"]: + with self.subTest(second_date=second_date): + self.conn.execute("DELETE FROM landmark_events") + self.conn.execute("DELETE FROM messages") + self.event() + self.event(timestamp=second_date, sender="synthetic-bob") + intent = self.intent() + self.assertEqual(intent["landmark_resolution"]["status"], "ambiguous") + self.assertIsNone(intent["after"]) + self.assertFalse(intent["has_temporal"]) + + def test_sender_scope_and_possessive_qualifier(self) -> None: + alice = self.event() + self.event(timestamp="2025-07-01", sender="synthetic-bob") + for query, sender in [("What happened after Demo Day?", "synthetic-alice"), + ("What happened after synthetic-alice's Demo Day?", None)]: + with self.subTest(query=query): + diagnostic = self.intent(query, sender)["landmark_resolution"] + self.assertEqual(diagnostic["status"], "resolved") + self.assertEqual([c["source_message_id"] for c in diagnostic["candidates"]], [alice]) + self.assertEqual(self.intent(sender="absent-sender")["landmark_resolution"]["candidates"], []) + + def test_literal_matching_quotes_whitespace_and_sql_metacharacters(self) -> None: + self.event("Demo Day") + self.assertEqual(self.intent('What happened after "DEMO DAY"?')["landmark_resolution"]["status"], "resolved") + self.conn.execute("DELETE FROM landmark_events") + self.conn.execute("DELETE FROM messages") + self.event("Cedar_%'s Launch") + self.assertEqual(self.intent('What happened after "Cedar_%\'s Launch"?')["landmark_resolution"]["status"], "resolved") + self.event("Cedar_% Launch") + self.assertEqual(self.intent('What happened after "Cedar_% Launch"?')["landmark_resolution"]["status"], "resolved") + self.assertEqual(self.conn.execute("SELECT COUNT(*) FROM messages").fetchone()[0], 2) + + def test_whole_phrase_no_fuzzy_or_substring_match(self) -> None: + self.event("SuperDemo Daylight") + self.assertEqual(self.intent()["landmark_resolution"]["status"], "unresolved") + self.assertEqual(self.intent("What happened after Demo Days?")["landmark_resolution"]["status"], "unresolved") + + def test_encoded_calendar_offsets_no_machine_timezone_conversion(self) -> None: + for timestamp in ["2025-06-15T23:30:00-07:00", "2025-06-15T00:30:00+14:00", "2025-06-15T12:00:00Z"]: + with self.subTest(timestamp=timestamp): + self.conn.execute("DELETE FROM landmark_events") + self.conn.execute("DELETE FROM messages") + self.event(timestamp=timestamp) + self.assertEqual(self.intent()["after"], "2025-06-15") + + def test_invalid_calendar_dates_and_non_iso_values_fail_open(self) -> None: + for timestamp in ["2025-02-30", "2025-13-01", "20250615", "not-a-date", "2025-06-15garbage", "9999-12-31"]: + with self.subTest(timestamp=timestamp): + self.conn.execute("DELETE FROM landmark_events") + self.conn.execute("DELETE FROM messages") + self.event(timestamp=timestamp) + intent = self.intent() + self.assertIsNone(intent["after"]) + self.assertEqual(intent["landmark_resolution"]["status"], "unresolved") + + def test_all_extraction_scopes_reject_precise_anchor(self) -> None: + examples = [metadata_intervals(("2025-06-15T00:00:00+00:00", "2025-06-15T00:00:00+00:00")), + metadata_intervals(("2025-06-15T00:00:00+00:00", "2025-06-15T23:00:00+00:00")), + metadata_intervals(("2025-06-14T00:00:00+00:00", "2025-06-15T00:00:00+00:00")), + metadata_intervals(unknown=True), {"truememory_ingest": {"version": 2}}, + {"truememory_ingest": "malformed"}] + for metadata in examples: + with self.subTest(metadata=metadata): + self.conn.execute("DELETE FROM landmark_events") + self.conn.execute("DELETE FROM messages") + self.event(metadata=metadata) + intent = self.intent() + self.assertIsNone(intent["after"]) + self.assertEqual(intent["landmark_resolution"]["reason"], "source_date_unresolved") + + def test_missing_source_stale_date_or_content_fail_open(self) -> None: + for mutation in ["DELETE FROM messages", "UPDATE messages SET timestamp='2025-07-01'", + "UPDATE messages SET content='unrelated synthetic fact'"]: + with self.subTest(mutation=mutation): + self.conn.execute("DELETE FROM landmark_events") + self.conn.execute("DELETE FROM messages") + self.event() + self.conn.execute(mutation) + self.assertIsNone(self.intent()["after"]) + + def test_missing_and_legacy_schema_fail_open(self) -> None: + self.conn.execute("DROP TABLE landmark_events") + self.assertEqual(self.intent()["landmark_resolution"]["reason"], "landmark_schema_unavailable") + self.conn.execute("CREATE TABLE landmark_events(id INTEGER PRIMARY KEY,event_name TEXT,timestamp TEXT)") + self.assertEqual(self.intent()["landmark_resolution"]["reason"], "landmark_schema_unavailable") + + def test_invalid_json_identity_and_oversized_matching_source_fail_open(self) -> None: + for mutation in ["UPDATE landmark_events SET related_entities='invalid-json'", + "UPDATE landmark_events SET source_message_id=NULL", + "UPDATE messages SET metadata='invalid-json'", + "UPDATE messages SET content=content || printf('%5000s','padding')"]: + with self.subTest(mutation=mutation): + self.conn.execute("DELETE FROM landmark_events") + self.conn.execute("DELETE FROM messages") + self.event() + self.conn.execute(mutation) + self.assertIsNone(self.intent()["after"]) + + def test_inventory_overflow_does_not_resolve_partial_subset(self) -> None: + self.event() + self.conn.executemany("INSERT INTO landmark_events(event_name,timestamp) VALUES(?,?)", + [("irrelevant", "2025-01-01")] * 1024) + intent = self.intent() + self.assertIsNone(intent["after"]) + self.assertEqual(intent["landmark_resolution"]["reason"], "landmark_inventory_limit") + + def test_candidate_truncation_never_resolves_and_duplicate_same_source_is_safe(self) -> None: + source_id = self.event() + self.conn.execute("INSERT INTO landmark_events(event_name,timestamp,source_message_id) VALUES('Demo Day arrived','2025-06-15',?)", (source_id,)) + self.assertEqual(self.intent()["landmark_resolution"]["status"], "resolved") + for index in range(18): + self.event(sender=f"synthetic-sender-{index}") + diagnostic = self.intent()["landmark_resolution"] + self.assertEqual(diagnostic["status"], "ambiguous") + self.assertEqual(len(diagnostic["candidates"]), 16) + self.assertTrue(diagnostic["candidates_truncated"]) + + def test_directive_anchor_is_not_disclosed(self) -> None: + self.event() + self.conn.execute("UPDATE messages SET directive=1") + self.assertEqual(self.intent()["landmark_resolution"]["candidates"], []) + + def test_unresolved_search_preserves_input_order_scores_and_metadata(self) -> None: + rows = [{"id": 100, "content": "synthetic high", "score": 0.8, "timestamp": "2024-01-01", "metadata": {"keep": 1}}, + {"id": 101, "content": "synthetic low", "score": 0.2, "timestamp": "2026-01-01", "metadata": {}}] + before = copy.deepcopy(rows) + with provenance_interface(): + results = TEMPORAL.search_temporal(self.conn, "What happened after Missing Event?", fts_results=rows) + self.assertEqual(rows, before) + self.assertEqual([{k: v for k, v in row.items() if k != "temporal_resolution"} for row in results], before) + + def test_ordinary_intervals_overlap_individually_not_envelope(self) -> None: + row = {"timestamp": "2025-09-01", "metadata": metadata_intervals( + ("2025-01-01T00:00:00+00:00", "2025-01-02T00:00:00+00:00"), + ("2025-09-01T00:00:00+00:00", "2025-09-02T00:00:00+00:00"))} + with provenance_interface(): + self.assertTrue(TEMPORAL._temporal_row_matches(row, "2025-01-01", "2025-01-03")) + self.assertFalse(TEMPORAL._temporal_row_matches(row, "2025-06-01", "2025-07-01")) + self.assertTrue(TEMPORAL._temporal_row_matches(row, "2025-09-01", "2025-09-03")) + + def test_unknown_intervals_excluded_only_for_real_window(self) -> None: + row = {"timestamp": "2025-06-15", "metadata": metadata_intervals(unknown=True)} + with provenance_interface(): + self.assertFalse(TEMPORAL._temporal_row_matches(row, "2025-06-01", "2025-07-01")) + self.assertTrue(TEMPORAL._temporal_row_matches(row, None, None)) + self.assertFalse(TEMPORAL._temporal_row_matches( + {"timestamp": "2025-06-15", "metadata": {"truememory_ingest": {"version": 2}}}, "2025-06-01", "2025-07-01")) + + def test_timeline_fallback_finds_earlier_disjoint_interval(self) -> None: + self.event("Synthetic evidence", "2025-09-01", metadata=metadata_intervals( + ("2025-01-01T00:00:00+00:00", "2025-01-02T00:00:00+00:00"), + ("2025-09-01T00:00:00+00:00", "2025-09-02T00:00:00+00:00"))) + with provenance_interface(): + early = TEMPORAL.get_timeline(self.conn, after="2025-01-01", before="2025-01-02") + gap = TEMPORAL.get_timeline(self.conn, after="2025-06-01", before="2025-06-30") + self.assertEqual(len(early), 1) + self.assertEqual(gap, []) + + def test_timeline_exclusive_day_directive_entity_and_unknown_scope(self) -> None: + self.event("Synthetic evidence", "2025-06-15", metadata=metadata_intervals( + ("2025-06-16T00:00:00+00:00", "2025-06-16T00:00:00+00:00"))) + with provenance_interface(): + self.assertEqual(TEMPORAL.get_timeline(self.conn, after="2025-06-15", before="2025-06-15"), []) + self.conn.execute("UPDATE messages SET metadata=?", (json.dumps(metadata_intervals( + ("2025-06-15T23:59:59+00:00", "2025-06-15T23:59:59+00:00"))),)) + self.assertEqual(len(TEMPORAL.get_timeline(self.conn, entity="synthetic-alice", after="2025-06-15", before="2025-06-15")), 1) + self.assertEqual(TEMPORAL.get_timeline(self.conn, entity="synthetic-bob", after="2025-06-15", before="2025-06-15"), []) + self.conn.execute("UPDATE messages SET directive=1") + self.assertEqual(TEMPORAL.get_timeline(self.conn, after="2025-06-15", before="2025-06-15"), []) + + def test_timeline_overflow_exposes_incomplete_coverage(self) -> None: + metadata = json.dumps(metadata_intervals(("2025-01-01T00:00:00+00:00", "2025-01-01T00:00:00+00:00"))) + self.conn.executemany("INSERT INTO messages(content,sender,timestamp,metadata) VALUES('synthetic','','2025-09-01',?)", [(metadata,)] * 1025) + with provenance_interface(): + results = TEMPORAL.get_timeline(self.conn, after="2025-01-01", before="2025-01-01") + self.assertEqual(len(results), 1024) + self.assertEqual(results[0]["temporal_scope_coverage"]["status"], "incomplete") + + def test_fallback_caps_visited_raw_rows_before_namespace_filtering(self) -> None: + self.conn.execute("INSERT INTO messages(content,sender,timestamp) VALUES('synthetic in-window','','2025-01-01')") + self.conn.executemany("INSERT INTO messages(content,sender,timestamp) VALUES('synthetic legacy','','2025-09-01')", [()] * 1400) + late = self.event("Synthetic late source", "2025-09-01", metadata=metadata_intervals( + ("2025-01-01T00:00:00+00:00", "2025-01-01T00:00:00+00:00"))) + with provenance_interface(): + results = TEMPORAL.get_timeline(self.conn, after="2025-01-01", before="2025-01-01") + self.assertNotIn(late, {row["id"] for row in results}) + self.assertEqual(results[0]["temporal_scope_coverage"]["status"], "incomplete") + + def test_empty_client_status_preserves_raw_overflow_and_schema_unavailable(self) -> None: + client = load_functions("client", {"resolve_temporal_query"}, {}) + self.conn.executemany("INSERT INTO messages(content,sender,timestamp) VALUES('synthetic legacy','','2025-09-01')", [()] * 1025) + engine = types.SimpleNamespace(conn=self.conn, _ensure_connection=lambda: None) + with patch.dict(sys.modules, {"truememory.temporal": TEMPORAL}): + diagnostic = client.resolve_temporal_query(types.SimpleNamespace(_engine=engine), "What happened in January 2025?") + self.assertEqual(diagnostic["source_interval_coverage"]["status"], "incomplete") + self.assertEqual(diagnostic["source_interval_coverage"]["reason"], "source_interval_inventory_limit") + self.conn.execute("ALTER TABLE messages RENAME COLUMN metadata TO synthetic_legacy_metadata") + with patch.dict(sys.modules, {"truememory.temporal": TEMPORAL}): + diagnostic = client.resolve_temporal_query(types.SimpleNamespace(_engine=engine), "What happened in January 2025?") + self.assertEqual(diagnostic["source_interval_coverage"]["reason"], "source_interval_lookup_unavailable") + + def test_raw_lookup_sqlite_work_remains_bounded_with_5005_legacy_rows(self) -> None: + self.conn.executemany("INSERT INTO messages(content,sender,timestamp) VALUES('synthetic legacy','','2025-09-01')", [()] * 5005) + state = {"active": False, "steps": 0} + + def trace(sql: str) -> None: + state["active"] = sql.startswith("SELECT id,substr(metadata") + + def progress() -> int: + if state["active"]: + state["steps"] += 1 + return 0 + + self.conn.set_trace_callback(trace) + self.conn.set_progress_handler(progress, 1) + try: + candidates, diagnostic = TEMPORAL._source_interval_candidates(self.conn) + finally: + self.conn.set_progress_handler(None, 0) + self.conn.set_trace_callback(None) + self.assertEqual(candidates, []) + self.assertEqual(diagnostic["reason"], "source_interval_inventory_limit") + self.assertEqual(diagnostic["candidate_limit"], 1024) + self.assertEqual(diagnostic["visited_row_limit"], 1025) + self.assertLess(state["steps"], 25 * (TEMPORAL._SOURCE_INTERVAL_ROW_LIMIT + 1)) + + def test_unindexed_legacy_ids_do_not_trigger_unbounded_sort_or_lookup(self) -> None: + self.conn.execute("ALTER TABLE messages RENAME TO synthetic_original_messages") + self.conn.execute("CREATE TABLE messages(id INTEGER,content TEXT,timestamp TEXT,metadata TEXT)") + self.conn.executemany("INSERT INTO messages VALUES(1,'synthetic','2025-01-01','{}')", [()] * 2000) + statements = [] + self.conn.set_trace_callback(statements.append) + try: + candidates, diagnostic = TEMPORAL._source_interval_candidates(self.conn) + finally: + self.conn.set_trace_callback(None) + self.assertEqual(candidates, []) + self.assertEqual(diagnostic["reason"], "source_interval_lookup_unavailable") + self.assertFalse(any(statement.startswith("SELECT") for statement in statements)) + + def test_actual_mcp_empty_envelope_exposes_source_inventory_overflow(self) -> None: + self.conn.executemany("INSERT INTO messages(content,sender,timestamp) VALUES('synthetic legacy','','2025-09-01')", [()] * 1025) + client = load_functions("client", {"resolve_temporal_query"}, {}) + engine = types.SimpleNamespace(conn=self.conn, _ensure_connection=lambda: None) + memory = types.SimpleNamespace(_engine=engine, search_deep=lambda *args, **kwargs: []) + memory.resolve_temporal_query = lambda query, user_id=None: client.resolve_temporal_query(memory, query, user_id) + + @contextmanager + def operation(): + yield memory, types.SimpleNamespace(reranker_id="unchanged", tier="free") + + tool = load_functions("mcp_server", {"truememory_search"}, { + "json": json, "_touch_search_time": lambda: None, "_search_operation": operation, + "_set_reranker": lambda _: None, "_SEARCH_INTERNAL_LIMIT": 100, + }) + with patch.dict(sys.modules, {"truememory.temporal": TEMPORAL}): + response = json.loads(tool.truememory_search("What happened in January 2025?", include_temporal_resolution=True)) + self.assertEqual(response["results"], []) + self.assertEqual(response["temporal_resolution"]["source_interval_coverage"]["reason"], "source_interval_inventory_limit") + + def test_actual_engine_search_uses_anchor_and_preserves_ambiguity(self) -> None: + self.event() + rows = [{"id": 101, "content": "synthetic before", "sender": "synthetic-alice", "timestamp": "2025-05-01", "score": 0.2}, + {"id": 102, "content": "synthetic after", "sender": "synthetic-alice", "timestamp": "2025-06-20", "score": 0.2}] + engine = load_functions("engine", {"search"}, { + "_HAS_QUERY_CLASSIFIER": False, "search_fts": lambda *args, **kwargs: copy.deepcopy(rows), + "resolve_temporal_intent": TEMPORAL.resolve_temporal_intent, + "search_temporal": TEMPORAL.search_temporal, "attach_temporal_resolution": TEMPORAL.attach_temporal_resolution, + "logger": logging.getLogger("synthetic-landmark"), "os": os, + }) + instance = types.SimpleNamespace(conn=self.conn, _ensure_connection=lambda: None, + _has_hybrid=False, _has_temporal=True, _has_personality=False, _has_consolidation=False, + _has_salience=False, _has_reranker=False, _apply_surprise_boost=lambda rows: rows) + with provenance_interface(): + resolved = engine.search(instance, "What happened in the first month after Demo Day?", limit=10) + self.event(timestamp="2025-07-01", sender="synthetic-bob") + ambiguous = engine.search(instance, "What happened in the first month after Demo Day?", limit=10) + self.assertTrue(all(row["temporal_resolution"]["status"] == "resolved" for row in resolved)) + after = next(row for row in resolved if row["id"] == 102) + self.assertEqual(after["score"], 0.2 * 1.3) + self.assertEqual([(row["id"], row["score"]) for row in ambiguous], [(101, 0.2), (102, 0.2)]) + self.assertTrue(all(row["temporal_resolution"]["status"] == "ambiguous" for row in ambiguous)) + + def test_actual_agentic_cleaner_retains_original_query_diagnostic(self) -> None: + self.event() + consumer = load_functions("engine", {"search_agentic"}, { + "resolve_temporal_intent": TEMPORAL.resolve_temporal_intent, + "attach_temporal_resolution": TEMPORAL.attach_temporal_resolution, + "logger": logging.getLogger("synthetic-landmark"), "os": os, + }) + agentic = load_functions("agentic_search", {"normalize_scores", "clean_results"}, {}) + query = "What happened in the first month after Demo Day?" + rows = [{"id": 101, "content": "synthetic result", "timestamp": "2025-06-20", "score": 0.2, "source": "fts"}] + instance = types.SimpleNamespace(conn=self.conn, _ensure_connection=lambda: None, + _has_temporal=True, _has_reranker=False, _has_salience=False, + search=Mock(return_value=copy.deepcopy(rows)), _entity_focused_search=lambda *args: [], + _check_sufficiency=lambda rows: True, _apply_surprise_boost=lambda rows: rows, + _clean_results=agentic.clean_results) + with provenance_interface(), patch.dict(sys.modules, {"truememory.agentic_search": agentic}): + result = consumer.search_agentic(instance, query, limit=10, use_hyde=False, use_clustering=False) + self.assertEqual(result[0]["temporal_resolution"]["status"], "resolved") + self.assertEqual(instance.search.call_args.kwargs["limit"], 30) + self.assertEqual(result[0]["metadata"], {}) + + def test_actual_agentic_entity_boost_uses_individual_intervals(self) -> None: + consumer = load_functions("engine", {"search_agentic"}, { + "resolve_temporal_intent": TEMPORAL.resolve_temporal_intent, + "attach_temporal_resolution": TEMPORAL.attach_temporal_resolution, + "logger": logging.getLogger("synthetic-landmark"), "os": os, + }) + agentic = load_functions("agentic_search", {"normalize_scores", "clean_results"}, {}) + rows = [{"id": 101, "content": "synthetic source fact", "timestamp": "2025-09-01", "score": 0.2, + "source": "fts", "metadata": metadata_intervals( + ("2025-01-01T00:00:00+00:00", "2025-01-02T00:00:00+00:00"), + ("2025-09-01T00:00:00+00:00", "2025-09-02T00:00:00+00:00"))}] + for month, score in [("January", 0.5 * 1.5), ("June", 0.5)]: + with self.subTest(month=month): + instance = types.SimpleNamespace(conn=self.conn, _ensure_connection=lambda: None, + _has_temporal=True, _has_reranker=False, _has_salience=False, + search=Mock(return_value=copy.deepcopy(rows)), _entity_focused_search=lambda *args: copy.deepcopy(rows), + _check_sufficiency=lambda rows: True, _apply_surprise_boost=lambda rows: rows, + _clean_results=agentic.clean_results) + with provenance_interface(), patch.dict(sys.modules, { + "truememory.agentic_search": agentic, "truememory.temporal": TEMPORAL, + }): + result = consumer.search_agentic(instance, "What happened in " + month + " 2025?", limit=10, + use_hyde=False, use_clustering=False) + self.assertEqual(result[0]["score"], score) + + def test_actual_engine_rescope_rejects_disjoint_gap_and_unknown_coverage(self) -> None: + self.event() + primary = [{"id": 101, "content": "synthetic primary", "sender": "synthetic-alice", + "timestamp": "2025-06-20", "score": 0.2}] + scoped = [{"id": 102, "content": "synthetic in-window", "timestamp": "2025-06-20", "score": 0.2, + "metadata": metadata_intervals(("2025-06-20T00:00:00+00:00", "2025-06-20T00:00:00+00:00"))}, + {"id": 103, "content": "synthetic unrelated gap", "timestamp": "2025-06-20", "score": 0.2, + "metadata": metadata_intervals(("2025-01-01T00:00:00+00:00", "2025-01-02T00:00:00+00:00"), + ("2025-09-01T00:00:00+00:00", "2025-09-02T00:00:00+00:00"))}, + {"id": 104, "content": "synthetic unknown scope", "timestamp": "2025-06-20", "score": 0.2, + "metadata": metadata_intervals(unknown=True)}] + consumer = load_functions("engine", {"search"}, { + "_HAS_QUERY_CLASSIFIER": False, "search_hybrid": lambda *args, **kwargs: copy.deepcopy(primary), + "resolve_temporal_intent": TEMPORAL.resolve_temporal_intent, + "search_temporal": TEMPORAL.search_temporal, "attach_temporal_resolution": TEMPORAL.attach_temporal_resolution, + "_source_intervals": TEMPORAL._source_intervals, "_temporal_row_matches": TEMPORAL._temporal_row_matches, + "_exclusive_upper_bound": TEMPORAL._exclusive_upper_bound, + "logger": logging.getLogger("synthetic-landmark"), "os": os, + }) + fts = types.ModuleType("truememory.fts_search") + fts.search_fts_in_range = lambda *args, **kwargs: copy.deepcopy(scoped) + instance = types.SimpleNamespace(conn=self.conn, _ensure_connection=lambda: None, + _has_hybrid=True, _has_temporal=True, _has_personality=False, _has_consolidation=False, + _has_salience=False, _has_reranker=False, _apply_surprise_boost=lambda rows: rows) + with provenance_interface(), patch.dict(sys.modules, {"truememory.fts_search": fts}): + result = consumer.search(instance, "What happened in the first month after Demo Day?", limit=10) + self.assertIn(102, {row["id"] for row in result}) + self.assertNotIn(103, {row["id"] for row in result}) + self.assertNotIn(104, {row["id"] for row in result}) + + +class BoundaryControls(unittest.TestCase): + def test_original_parser_and_landmark_writer_ast_unchanged(self) -> None: + current = (ROOT / "truememory/temporal.py").read_text() + baseline_hashes = { + "detect_temporal_intent": "2a752baf88d85db0eb61b91c087284c4e02dd3d4b89dece12bc5bf9e04ded287", + "detect_landmark_events": "d3bbd201a06107765f0b80448f5cec8d2dde800d54f20e84cd442c137b91fe9c", + "parse_date_reference": "8f8d13024c23b012ee9a960992060f7abf77d74939cb8000110f55f599df98ad", + } + for name, baseline_hash in baseline_hashes.items(): + with self.subTest(name=name): + node = next(node for node in ast.parse(current).body if isinstance(node, ast.FunctionDef) and node.name == name) + self.assertEqual(hashlib.sha256(ast.get_source_segment(current, node).encode()).hexdigest(), baseline_hash) + + def test_client_scope_threading_and_existing_limits(self) -> None: + client = load_functions("client", {"search", "search_deep", "resolve_temporal_query"}, {}) + engine = types.SimpleNamespace(search=Mock(return_value=[]), search_agentic=Mock(return_value=[])) + memory = types.SimpleNamespace(_engine=engine) + client.search(memory, "after Demo Day", user_id="synthetic-alice", limit=10) + self.assertEqual(engine.search.call_args.kwargs["limit"], 30) + self.assertEqual(engine.search.call_args.kwargs["landmark_sender"], "synthetic-alice") + client.search_deep(memory, "after Demo Day", limit=100) + self.assertEqual(engine.search_agentic.call_args.kwargs["limit"], 100) + self.assertNotIn("landmark_sender", engine.search_agentic.call_args.kwargs) + + def test_client_resolution_exposes_bounded_policy_without_retrieval(self) -> None: + client = load_functions("client", {"resolve_temporal_query"}, {}) + connection = sqlite3.connect(":memory:") + self.addCleanup(connection.close) + engine = types.SimpleNamespace(conn=connection, _ensure_connection=lambda: None) + with patch.dict(sys.modules, {"truememory.temporal": TEMPORAL}): + intent = client.resolve_temporal_query(types.SimpleNamespace(_engine=engine), "What happened in January 2025?") + self.assertEqual(intent["source_interval_lookup_policy"]["candidate_limit"], 1024) + self.assertFalse(intent["source_interval_lookup_policy"]["complete_archive"]) + + def test_mcp_default_lists_opt_in_empty_status_and_internal_limits(self) -> None: + diagnostics = {"landmark_resolution": {"status": "ambiguous", "reason": "multiple_sources"}} + memory = types.SimpleNamespace(search_deep=Mock(return_value=[]), resolve_temporal_query=Mock(return_value=diagnostics)) + operation = types.SimpleNamespace(reranker_id="unchanged", tier="free") + + @contextmanager + def search_operation(**kwargs): + yield memory, operation + + mcp = load_functions("mcp_server", {"truememory_search", "truememory_search_deep"}, { + "json": json, "_touch_search_time": lambda: None, "_search_operation": search_operation, + "_set_reranker": lambda _: None, "_SEARCH_INTERNAL_LIMIT": 100, "_DEEP_INTERNAL_LIMIT": 500, + "_DEEP_RERANKER": "unchanged-deep", "_resolve_deepsearch_llm": lambda _: None, + "_parallel_search": lambda *args, **kwargs: [], + }) + for name, limit in [("truememory_search", 100), ("truememory_search_deep", 500)]: + with self.subTest(name=name): + function = getattr(mcp, name) + self.assertEqual(json.loads(function("after Demo Day")), []) + self.assertEqual(memory.search_deep.call_args.kwargs["limit"], limit) + result = json.loads(function("after Demo Day", user_id="synthetic-alice", include_temporal_resolution=True)) + self.assertEqual(result["results"], []) + self.assertEqual(result["temporal_resolution"]["landmark_resolution"]["status"], "ambiguous") + self.assertEqual(memory.resolve_temporal_query.call_args.kwargs["user_id"], "synthetic-alice") + self.assertEqual(len(json.loads(function("after Demo Day | before Launch", include_temporal_resolution=True))["temporal_resolutions"]), 2) + + def test_tools_count_and_empty_query_unchanged_by_default(self) -> None: + source = (ROOT / "truememory/mcp_server.py").read_text() + tree = ast.parse(source) + tools = [node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name.startswith("truememory_")] + self.assertEqual(len(tools), 11) + functions = load_functions("mcp_server", {"truememory_search", "truememory_search_deep"}, {"json": json}) + self.assertEqual(json.loads(functions.truememory_search("")), []) + self.assertEqual(json.loads(functions.truememory_search_deep("", include_temporal_resolution=True))["results"], []) + + def test_engine_source_all_consumers_and_post_clean_diagnostics(self) -> None: + tree = ast.parse((ROOT / "truememory/engine.py").read_text()) + engine = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == "TrueMemoryEngine") + methods = {node.name: node for node in engine.body if isinstance(node, ast.FunctionDef)} + search_calls = [node for node in ast.walk(methods["search"]) if isinstance(node, ast.Call)] + self.assertTrue(any(isinstance(node.func, ast.Name) and node.func.id == "resolve_temporal_intent" for node in search_calls)) + temporal_calls = [node for node in search_calls if isinstance(node.func, ast.Name) and node.func.id == "search_temporal"] + self.assertEqual(len(temporal_calls), 2) + self.assertTrue(all(any(keyword.arg == "resolved_intent" for keyword in node.keywords) for node in temporal_calls)) + agentic_calls = [node for node in ast.walk(methods["search_agentic"]) if isinstance(node, ast.Call)] + self.assertTrue(any(isinstance(node.func, ast.Name) and node.func.id == "_temporal_row_matches" for node in agentic_calls)) + self.assertEqual(sum(isinstance(node.func, ast.Name) and node.func.id == "attach_temporal_resolution" for node in agentic_calls), 2) + pool_assignment = next(node for node in ast.walk(methods["search_agentic"]) if isinstance(node, ast.Assign) + and any(isinstance(target, ast.Name) and target.id == "candidate_pool" for target in node.targets)) + self.assertIn("max(limit * 8, 100)", ast.unparse(pool_assignment)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-local-inference-ownership-740.py b/tests/test-local-inference-ownership-740.py new file mode 100644 index 00000000..21cc45dd --- /dev/null +++ b/tests/test-local-inference-ownership-740.py @@ -0,0 +1,361 @@ +"""Event-driven local ownership and recovery checks with synthetic models.""" +from __future__ import annotations + +import threading +import sqlite3 +import sys +import types +import weakref +from concurrent.futures import ThreadPoolExecutor + +import pytest +import numpy as np + +from truememory import mps_utils +from truememory.model_client import EmbeddingProxy + + +@pytest.fixture(autouse=True) +def no_allocator_calls(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(mps_utils, "flush_mps_cache", lambda: None) + torch = types.ModuleType("torch") + torch.backends = types.SimpleNamespace(mps=types.SimpleNamespace(is_available=lambda: True)) + monkeypatch.setitem(sys.modules, "torch", torch) + + +@pytest.fixture +def ownership_wait(monkeypatch: pytest.MonkeyPatch) -> threading.Event: + waiting = threading.Event() + factory = mps_utils._ModelOwnership + + class ObservedLock: + def __init__(self) -> None: + self.lock = threading.Lock() + + def __enter__(self) -> None: + if self.lock.locked(): + waiting.set() + self.lock.acquire() + + def __exit__(self, *_exc: object) -> None: + self.lock.release() + + monkeypatch.setattr(mps_utils, "_ModelOwnership", lambda: factory(lock=ObservedLock())) + return waiting + + +def test_same_instance_cannot_enter_during_cpu_retry(ownership_wait: threading.Event) -> None: + retry_started = threading.Event() + release_retry = threading.Event() + second_attempted = threading.Event() + second_entered = threading.Event() + moves: list[str] = [] + active = 0 + + class Model: + device = "mps" + + def encode(self, texts: list[str], **kwargs: object) -> list[list[int]]: + nonlocal active + active += 1 + try: + if self.device == "mps": + raise RuntimeError("MPS backend out of memory") + if texts == ["first"]: + retry_started.set() + assert release_retry.wait(3) + else: + second_entered.set() + return [[len(texts[0]), 2]] + finally: + active -= 1 + + def to(self, device: str) -> None: + assert active == 0, "device movement overlapped native inference" + moves.append(device) + self.device = device + + model = Model() + + def second() -> object: + second_attempted.set() + return mps_utils.encode_with_mps_fallback(model, ["second"]) + + with ThreadPoolExecutor(max_workers=2) as pool: + first = pool.submit(mps_utils.encode_with_mps_fallback, model, ["first"]) + try: + assert retry_started.wait(2) + other = pool.submit(second) + assert second_attempted.wait(2) + assert ownership_wait.wait(2) + assert not second_entered.is_set() + finally: + release_retry.set() + assert first.result(timeout=2) == [[5, 2]] + assert other.result(timeout=2) == [[6, 2]] + assert moves == ["cpu"] + assert model.device == "cpu" + assert id(model) not in mps_utils._model_owners + + +def test_unrelated_models_remain_concurrent() -> None: + barrier = threading.Barrier(2) + + class Model: + def encode(self, texts: list[str], **kwargs: object) -> list[str]: + barrier.wait(timeout=2) + return texts + + with ThreadPoolExecutor(max_workers=2) as pool: + futures = [ + pool.submit(mps_utils.encode_with_mps_fallback, Model(), [str(index)]) + for index in range(2) + ] + assert [future.result(timeout=3) for future in futures] == [["0"], ["1"]] + + +def test_concurrent_oom_calls_recover_once_without_repromotion() -> None: + start = threading.Barrier(8) + + class Model: + device = "mps" + + def __init__(self) -> None: + self.moves: list[str] = [] + self.oom_count = 0 + + def encode(self, texts: list[str], **kwargs: object) -> list[str]: + if self.device == "mps": + self.oom_count += 1 + raise RuntimeError("MPS backend out of memory") + return texts + + def to(self, device: str) -> None: + self.moves.append(device) + self.device = device + + model = Model() + + def run(index: int) -> object: + start.wait(timeout=2) + return mps_utils.encode_with_mps_fallback(model, [str(index)]) + + with ThreadPoolExecutor(max_workers=8) as pool: + futures = [pool.submit(run, index) for index in range(8)] + assert [future.result(timeout=3) for future in futures] == [[str(i)] for i in range(8)] + assert model.oom_count == 1 + assert model.moves == ["cpu"] + + +@pytest.mark.parametrize("failure", ["normal", "move", "retry"]) +def test_failures_release_instance_ownership(failure: str) -> None: + class Model: + failing = True + calls = 0 + + def encode(self, texts: list[str], **kwargs: object) -> list[str]: + self.calls += 1 + if self.failing: + if failure == "normal" or self.calls > 1: + raise RuntimeError("synthetic inference failure") + raise RuntimeError("MPS backend out of memory") + return texts + + def to(self, device: str) -> None: + if failure == "move" and self.failing: + raise RuntimeError("synthetic move failure") + + model = Model() + with pytest.raises(RuntimeError, match="synthetic"): + mps_utils.encode_with_mps_fallback(model, ["failure"]) + assert id(model) not in mps_utils._model_owners + model.failing = False + with ThreadPoolExecutor(max_workers=1) as pool: + future = pool.submit(mps_utils.encode_with_mps_fallback, model, ["reused"]) + assert future.result(timeout=2) == ["reused"] + assert id(model) not in mps_utils._model_owners + + +def test_release_traceback_and_move_before_allocator_cleanup(monkeypatch: pytest.MonkeyPatch) -> None: + events: list[str] = [] + + class Allocation: + pass + + class Model: + device = "mps" + failed_allocation: weakref.ReferenceType[Allocation] | None = None + + def encode(self, texts: list[str], **kwargs: object) -> list[str]: + if self.device == "mps": + allocation = Allocation() + self.failed_allocation = weakref.ref(allocation) + raise RuntimeError("MPS backend out of memory") + events.append("retry") + return texts + + def to(self, device: str) -> None: + assert self.failed_allocation is not None + assert self.failed_allocation() is None + events.append("move-" + device) + self.device = device + + monkeypatch.setattr(mps_utils, "flush_mps_cache", lambda: events.append("flush")) + model = Model() + assert mps_utils.encode_with_mps_fallback(model, ["synthetic"]) == ["synthetic"] + assert events == ["move-cpu", "flush", "retry"] + + +def test_unhashable_nonweakrefable_model_and_kwargs_are_supported() -> None: + class Model: + __slots__ = () + __hash__ = None + + def encode(self, texts: list[str], **kwargs: object) -> tuple: + return texts, kwargs + + model = Model() + assert mps_utils.encode_with_mps_fallback( + model, ["a", "a"], batch_size=2, show_progress_bar=False, + ) == (["a", "a"], {"batch_size": 2, "show_progress_bar": False}) + assert id(model) not in mps_utils._model_owners + + +def test_proxy_requests_keep_server_concurrency() -> None: + barrier = threading.Barrier(2) + + class Proxy(EmbeddingProxy): + def encode(self, texts: list[str], **kwargs: object) -> list[str]: + barrier.wait(timeout=2) + return texts + + proxy = Proxy() + with ThreadPoolExecutor(max_workers=2) as pool: + futures = [ + pool.submit(mps_utils.encode_with_mps_fallback, proxy, [str(index)]) + for index in range(2) + ] + assert [future.result(timeout=3) for future in futures] == [["0"], ["1"]] + assert id(proxy) not in mps_utils._model_owners + + +def test_proxy_errors_do_not_trigger_local_recovery(monkeypatch: pytest.MonkeyPatch) -> None: + calls: list[str] = [] + + class Proxy(EmbeddingProxy): + def encode(self, texts: list[str], **kwargs: object) -> list[str]: + calls.append("request") + raise RuntimeError("Model server error: MPS backend out of memory") + + monkeypatch.setattr(mps_utils, "flush_mps_cache", lambda: calls.append("flush")) + with pytest.raises(RuntimeError, match="Model server error"): + mps_utils.encode_with_mps_fallback(Proxy(), ["synthetic"]) + assert calls == ["request"] + + +@pytest.mark.parametrize("caller", ["clustering", "gate", "worker-primary", "worker-separation"]) +def test_real_callers_share_ownership_with_fallback( + caller: str, monkeypatch: pytest.MonkeyPatch, ownership_wait: threading.Event, +) -> None: + from truememory import clustering, vector_search + from truememory.ingest.encoding_gate import EncodingGate + from truememory.tier_switch.worker import RebuildWorker + + entered = threading.Event() + release = threading.Event() + guard = threading.Lock() + blocked_call = 2 if caller == "worker-separation" else 1 + + class Model: + device = "mps" + active = 0 + peak = 0 + calls = 0 + + def __init__(self) -> None: + self.moves: list[str] = [] + + def encode(self, texts: list[str], **kwargs: object) -> np.ndarray: + with guard: + self.calls += 1 + call = self.calls + self.active += 1 + self.peak = max(self.peak, self.active) + try: + if call == blocked_call: + entered.set() + assert release.wait(3) + if texts == ["contender"] and self.device == "mps": + raise RuntimeError("MPS backend out of memory") + return np.asarray([[len(text), 1.0] for text in texts], dtype=np.float32) + finally: + with guard: + self.active -= 1 + + def to(self, device: str) -> None: + assert self.active == 0 + self.moves.append(device) + self.device = device + + model = Model() + monkeypatch.setattr(vector_search, "get_model", lambda: model) + conn = sqlite3.connect(":memory:", check_same_thread=False) + try: + conn.execute("CREATE TABLE cluster_centroids (id INTEGER)") + conn.execute("CREATE TABLE vec_fixture (rowid INTEGER, embedding BLOB)") + conn.execute("CREATE TABLE sep_fixture (rowid INTEGER, embedding BLOB)") + gate = types.SimpleNamespace( + _embed_model=model, _last_search_results=[{"content": "synthetic nearby fact"}], + _pe_available=True, _pe_degradation_count=0, + ) + worker = types.SimpleNamespace(conn=conn) + + def existing_call() -> object: + if caller == "clustering": + return clustering.search_clustered(conn, "synthetic query") + if caller == "gate": + return EncodingGate._compute_prediction_error(gate, "synthetic fact") + return RebuildWorker._process_batch( + worker, [{"id": 1, "content": "synthetic record"}], model, + "vec_fixture", "sep_fixture", lambda vector: vector.tobytes(), + lambda *_parts: "synthetic separation", + ) + + with ThreadPoolExecutor(max_workers=2) as pool: + first = pool.submit(existing_call) + try: + assert entered.wait(2) + other = pool.submit(mps_utils.encode_with_mps_fallback, model, ["contender"]) + assert ownership_wait.wait(2) + assert model.moves == [] + finally: + release.set() + first.result(timeout=2) + np.testing.assert_array_equal(other.result(timeout=2), [[9, 1]]) + assert model.peak == 1 + assert model.moves == ["cpu"] + assert gate._pe_available + if caller.startswith("worker"): + for table, text in (("vec_fixture", "synthetic record"), + ("sep_fixture", "synthetic separation")): + rows = conn.execute(f"SELECT rowid, embedding FROM {table}").fetchall() + assert len(rows) == 1 + assert rows[0][0] == 1 + np.testing.assert_array_equal(np.frombuffer(rows[0][1], dtype=np.float32), [len(text), 1]) + assert id(model) not in mps_utils._model_owners + finally: + conn.close() + + +def test_ownership_only_entry_preserves_outer_oom_policy() -> None: + class Model: + def encode(self, texts: list[str], **kwargs: object) -> object: + raise RuntimeError("MPS backend out of memory") + + def to(self, device: str) -> None: + raise AssertionError("ownership-only caller must retain its retry policy") + + model = Model() + with pytest.raises(RuntimeError, match="MPS backend out of memory"): + mps_utils.encode_with_model_ownership(model, ["synthetic"]) + assert id(model) not in mps_utils._model_owners diff --git a/tests/test-maintenance-adapters-753.py b/tests/test-maintenance-adapters-753.py new file mode 100644 index 00000000..ebc0601c --- /dev/null +++ b/tests/test-maintenance-adapters-753.py @@ -0,0 +1,957 @@ +"""Eight real maintenance adapters with SQLite and synthetic native boundaries.""" + +import ast +import importlib +import runpy +import sqlite3 +import struct +import sys +import tempfile +import threading +import types +import unittest +from pathlib import Path +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] +TESTS = Path(__file__).resolve().parent +PRIMITIVES = runpy.run_path(str(TESTS / "test-maintenance-source-revision-753.py")) +STORAGE, MAINTENANCE = PRIMITIVES["STORAGE"], PRIMITIVES["MAINTENANCE"] +CLUSTERS = runpy.run_path(str(TESTS / "test-cluster-publication-749.py")) + + +def source_modules() -> dict[str, types.ModuleType]: + modules = {} + for name in ("consolidation", "predictive", "temporal", "personality"): + module = types.ModuleType("synthetic_adapter_" + name) + path = ROOT / "truememory" / (name + ".py") + tree = ast.parse(path.read_text()) + tree.body = [node for node in tree.body if not ( + isinstance(node, ast.ImportFrom) and node.module in {"truememory.storage", "truememory.fts_search"} + )] + module._initialize_dunbar_ownership = STORAGE._initialize_dunbar_ownership + module._dunbar_ownership_ready = STORAGE._dunbar_ownership_ready + exec(compile(tree, str(path), "exec"), module.__dict__) + modules["truememory." + name] = module + return modules + + +class AdapterFixture(unittest.TestCase): + def setUp(self) -> None: + directory = tempfile.TemporaryDirectory(prefix="synthetic-adapters-") + self.addCleanup(directory.cleanup) + self.path = Path(directory.name) / "synthetic.sqlite" + self.conn = self.open_db() + self.modules = source_modules() + self.cluster, self.vector, self.native = CLUSTERS["load_clustering"]() + self.modules["truememory.clustering"] = self.cluster + self.native.fit = lambda: setattr(self.native, "labels", [-1] * len(self.native.inputs[-1])) + self.conn.execute("CREATE TABLE vec_messages_edge(embedding BLOB)") + self.conn.execute( + "INSERT INTO vector_cache_registry VALUES " + "('edge','vec_messages_edge','vec_messages_sep_edge',0,0,'synthetic-model-a',2,0,0)" + ) + self.conn.executemany("INSERT INTO metadata(key,value,updated_at) VALUES (?,?,'synthetic-0')", [ + ("embed_model", "synthetic-model-a"), ("embed_dim", "2"), + ("vec_source_v1:vec_messages_edge", '{"version":1,"generation":"synthetic-a","cursor":0,"complete":true}'), + ]) + self.conn.commit() + # Detached production workers must not discover this fixture's model + # or hold its lock through the process-wide module registry. + self.enterContext(patch.object(MAINTENANCE, "sys", types.SimpleNamespace( + modules={"truememory.vector_search": self.vector}, version_info=sys.version_info, + ))) + self.enterContext(patch.object(MAINTENANCE, "importlib", types.SimpleNamespace( + import_module=lambda name: self.modules[name], metadata=importlib.metadata, util=importlib.util, + ))) + self.versions = {"numpy": "synthetic-1", "hdbscan": "synthetic-1", "sqlite_vec": "synthetic-1"} + self.enterContext(patch.object(MAINTENANCE, "_installed_dependency", lambda name, dist: self.versions[name])) + + # TestCase.enterContext was added after the oldest supported Python. + def enterContext(self, context): + result = context.__enter__() + self.addCleanup(context.__exit__, None, None, None) + return result + + def open_db(self) -> sqlite3.Connection: + conn = STORAGE.create_db(self.path) + conn.execute("PRAGMA busy_timeout=100") + self.addCleanup(conn.close) + return conn + + def add(self, count: int = 1, *, commit: bool = True) -> None: + for _ in range(count): + cursor = self.conn.execute( + "INSERT INTO messages(content,sender,recipient,timestamp,category) VALUES " + "('synthetic team launched Cedar','synthetic-primary','synthetic-contact','2026-01-01','synthetic-session')" + ) + self.conn.execute("INSERT INTO vec_messages_edge(rowid,embedding) VALUES (?,?)", (cursor.lastrowid, struct.pack("2f", 1, 2))) + self.conn.execute("UPDATE metadata SET updated_at=? WHERE key IN ('embed_model','embed_dim')", (str(cursor.lastrowid),)) + self.conn.execute("UPDATE vector_cache_registry SET last_embedded_id=?,vector_count=?,last_updated=?", + (cursor.lastrowid, cursor.lastrowid, cursor.lastrowid)) + if commit: + self.conn.commit() + + def specs(self, conn: sqlite3.Connection | None = None): + return MAINTENANCE.all_layer_specs(conn or self.conn) + + def spec(self, layer: str = "clusters"): + return next(spec for spec in self.specs() if spec.layer == layer) + + def run_layer(self, layer: str = "clusters", **kwargs): + return MAINTENANCE.run_layers(self.conn, (self.spec(layer),), **kwargs)[0] + + def state(self, layer: str = "clusters", conn: sqlite3.Connection | None = None): + conn = conn or self.conn + spec = next(spec for spec in self.specs(conn) if spec.layer == layer) + return MAINTENANCE.read_layer_states(conn, (spec,))[layer] + + def output(self, conn: sqlite3.Connection | None = None) -> tuple: + conn = conn or self.conn + return (conn.execute("SELECT * FROM message_clusters ORDER BY message_id").fetchall(), + conn.execute("SELECT * FROM cluster_centroids ORDER BY cluster_id").fetchall()) + + +class TestEightAdapters(AdapterFixture): + def test_all_eight_actual_builders_and_original_six_contract(self) -> None: + self.add(5) + self.native.fit = lambda: setattr(self.native, "labels", [0, 0, -1, 1, 1]) + with patch.object(self.modules["truememory.personality"], "build_entity_profiles", side_effect=AssertionError("not enrolled")), \ + patch.object(self.modules["truememory.personality"], "extract_preferences", side_effect=AssertionError("separate nonpersisted call")): + results = MAINTENANCE.run_layers(self.conn, self.specs()) + self.assertEqual([item.result_key for item in results], [ + "cluster_messages", "build_summaries", "detect_contradictions", "structured_facts", + "build_surprise_index", "detect_episodes", "detect_landmarks", "dunbar_hierarchy", + ]) + self.assertTrue(all(item.outcome in {"success", "success_empty"} for item in results), results) + self.assertEqual(results[0].output_count, 2) + self.assertEqual(results[-1].output_count, 1) + self.assertEqual(results[0].coverage, "vector_generation_unverified") + self.assertTrue(all(item.coverage == "complete" for item in results[1:])) + self.assertEqual(len(MAINTENANCE.nonvector_layer_specs()), 6) + self.assertTrue(all(spec.publication_guard is None for spec in MAINTENANCE.nonvector_layer_specs())) + self.assertFalse(self.conn.in_transaction) + + def test_empty_all_layers_checkpoint_once_across_reopen(self) -> None: + results = MAINTENANCE.run_layers(self.conn, self.specs()) + self.assertTrue(all(item.outcome == "success_empty" for item in results), results) + reopened = self.open_db() + self.assertEqual(MAINTENANCE.plan_layers(reopened, self.specs(reopened)), ()) + self.assertTrue(all(not item.attempted for item in MAINTENANCE.run_layers(reopened, self.specs(reopened)))) + + def test_all_noise_retains_assignments_as_success_empty(self) -> None: + self.add(5) + result = self.run_layer() + self.assertEqual((result.outcome, result.output_count, result.coverage), + ("success_empty", 0, "vector_generation_unverified")) + self.assertEqual(len(self.output()[0]), 5) + self.assertTrue(all(row[1:] == (-1, 1) for row in self.output()[0])) + + def test_missing_hdbscan_and_runtime_import_failure_are_unavailable_with_siblings(self) -> None: + self.add(1) + self.versions["hdbscan"] = None + results = MAINTENANCE.run_layers(self.conn, self.specs()) + self.assertEqual(results[0].outcome, "unavailable") + self.assertTrue(all(item.outcome in {"success", "success_empty"} for item in results[1:])) + self.versions["hdbscan"] = "synthetic-1" + self.native.missing = True + result = self.run_layer() + self.assertEqual((result.outcome, result.error_category), ("unavailable", "LayerUnavailableError")) + self.assertIsNone(self.state().successful_epoch) + self.assertFalse(self.run_layer().attempted) + + def test_missing_vector_table_and_active_rebuild_do_not_certify_empty(self) -> None: + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('vec_build_state:vec_messages_edge','in_progress')") + self.conn.commit() + self.assertEqual(self.run_layer().outcome, "unavailable") + self.assertIsNone(self.state().successful_epoch) + self.conn.execute("UPDATE metadata SET value='complete' WHERE key='vec_build_state:vec_messages_edge'") + self.conn.execute("ALTER TABLE vec_messages_edge RENAME TO synthetic_old_vectors") + self.conn.commit() + result = self.run_layer() + self.assertEqual((result.outcome, result.error_category), ("unavailable", "VectorIndexMissing")) + + def test_manifest_absence_is_unverified_and_invalid_or_incomplete_is_unavailable(self) -> None: + self.conn.execute("DELETE FROM metadata WHERE key='vec_source_v1:vec_messages_edge'") + self.conn.commit() + self.assertEqual(self.run_layer().coverage, "vector_generation_unverified") + for descriptor in (b'synthetic bytes', 'synthetic invalid JSON', 'null', '[]', '{}', '{"version":2,"complete":true}', + '{"version":true,"complete":true}', '{"version":1,"complete":1}', + '{"version":1,"complete":"true"}', '{"version":1,"complete":false}'): + with self.subTest(descriptor=descriptor): + self.conn.execute("INSERT OR REPLACE INTO metadata(key,value) VALUES ('vec_source_v1:vec_messages_edge',?)", + (descriptor,)) + self.conn.commit() + result = self.run_layer() + self.assertEqual(result.outcome, "unavailable") + self.assertEqual(result.coverage, "vector_generation_unverified") + + def test_dunbar_primary_ties_legacy_contacts_and_empty_retirement(self) -> None: + self.add(3) + self.conn.execute( + "INSERT INTO entity_relationships(entity_a,entity_b,relationship_type) " + "VALUES ('synthetic-primary','synthetic-contact','contact')" + ) + self.conn.commit() + result = self.run_layer("dunbar") + self.assertEqual((result.output_count, result.coverage), (1, "legacy_contacts_unowned")) + self.assertEqual(self.conn.execute("SELECT count(*) FROM entity_relationships").fetchone()[0], 2) + self.conn.execute("DELETE FROM messages") + self.conn.commit() + result = self.run_layer("dunbar") + self.assertEqual((result.outcome, result.output_count, result.coverage), ("success_empty", 0, "legacy_contacts_unowned")) + self.assertEqual(self.conn.execute("SELECT count(*) FROM entity_relationships").fetchone()[0], 1) + + def test_actual_primary_selection_matches_existing_tied_sender_query(self) -> None: + self.conn.executemany("INSERT INTO messages(content,sender,recipient) VALUES ('synthetic',?,?)", [ + ("synthetic-beta", "synthetic-contact"), ("synthetic-alpha", "synthetic-other"), + ]) + self.conn.commit() + expected = self.conn.execute( + "SELECT sender,COUNT(*) AS cnt FROM messages WHERE sender != '' AND sender IS NOT NULL " + "GROUP BY sender ORDER BY cnt DESC LIMIT 1" + ).fetchone()[0].lower() + self.run_layer("dunbar") + self.assertEqual(self.conn.execute("SELECT DISTINCT entity_a FROM entity_relationships").fetchall(), [(expected,)]) + + +class TestAdapterScheduling(AdapterFixture): + def test_24_actual_appends_with_metadata_progress_rewrites_wait_for_25(self) -> None: + self.run_layer() + original_key = self.spec().resolve_dependency().key + self.add(24) + self.assertEqual(self.spec().resolve_dependency().key, original_key) + self.assertFalse(self.run_layer().attempted) + self.add(1) + self.assertTrue(self.run_layer().attempted) + self.assertEqual(self.state().attempted_insert_count, 25) + + def test_failed_and_unavailable_initial_cluster_attempts_use_25_insert_baseline(self) -> None: + for unavailable in (False, True): + with self.subTest(unavailable=unavailable): + self.conn.execute("DELETE FROM maintenance_layers WHERE layer='clusters'") + self.conn.commit() + self.versions["hdbscan"] = None if unavailable else "synthetic-1" + with patch.object(self.cluster, "cluster_messages", side_effect=ValueError("synthetic failure")): + result = self.run_layer() + self.assertEqual(result.outcome, "unavailable" if unavailable else "failed") + self.add(1) + self.assertFalse(self.run_layer().attempted) + self.add(23) + self.assertFalse(self.run_layer().attempted) + self.add(1) + self.assertTrue(self.run_layer().attempted) + + def test_correction_replace_dependency_and_force_wake_below_25(self) -> None: + self.add(1) + self.run_layer() + for sql in ("UPDATE messages SET content='synthetic changed' WHERE id=1", + "INSERT OR REPLACE INTO messages(id,content) VALUES (1,'synthetic replacement')"): + self.conn.execute(sql) + self.conn.commit() + self.assertTrue(self.run_layer().attempted) + self.versions["hdbscan"] = "synthetic-2" + self.assertTrue(self.run_layer().attempted) + self.assertTrue(self.run_layer(force=True).attempted) + self.assertFalse(self.run_layer().attempted) + + def test_manifest_progress_and_same_dimensional_model_identity_change_key(self) -> None: + previous = self.spec().resolve_dependency().key + self.conn.execute("UPDATE metadata SET value=? WHERE key='vec_source_v1:vec_messages_edge'", + ('{"version":1,"generation":"synthetic-a","cursor":1,"complete":true}',)) + self.conn.commit() + current = self.spec().resolve_dependency().key + self.assertNotEqual(current, previous) + self.vector.EMBEDDING_MODEL = "synthetic-model-c" + self.assertNotEqual(self.spec().resolve_dependency().key, current) + + def test_foreground_eligibility_never_imports_native_modules_or_waits_for_model(self) -> None: + original = MAINTENANCE.importlib.import_module + imported = [] + + def safe_import(name): + imported.append(name) + self.assertNotIn(name, {"numpy", "hdbscan", "sqlite_vec", "truememory.clustering", "truememory.vector_search"}) + return original(name) + + with patch.object(MAINTENANCE.importlib, "import_module", safe_import): + self.vector._lock.acquire() + try: + self.assertEqual(self.spec().resolve_dependency().error_category, "ModelBusy") + planned = MAINTENANCE.plan_layers(self.conn, self.specs()) + self.assertEqual(len(planned), 7) + self.assertNotIn("clusters", {spec.layer for spec in planned}) + finally: + self.vector._lock.release() + self.assertNotIn("truememory.clustering", imported) + self.assertFalse(self.conn.in_transaction) + + def test_adapters_reject_wrong_connection_before_running_or_recording(self) -> None: + other = self.open_db() + with self.assertRaisesRegex(ValueError, "another connection"): + MAINTENANCE.run_layers(other, self.specs()) + self.assertEqual(other.execute("SELECT DISTINCT outcome FROM maintenance_layers").fetchall(), [("pending",)]) + + +class TestTransientModelContention(AdapterFixture): + def row(self) -> tuple | None: + row = self.conn.execute("SELECT * FROM maintenance_layers WHERE layer='clusters'").fetchone() + return tuple(row) if row is not None else None + + def release_model(self) -> None: + if self.vector._lock.locked(): + self.vector._lock.release() + + def block_during_compute(self, cancel: threading.Event | None = None) -> None: + def fit(): + self.native.labels = [-1] * len(self.native.inputs[-1]) + self.vector._lock.acquire() + if cancel is not None: + cancel.set() + self.native.fit = fit + self.addCleanup(self.release_model) + + def test_initial_busy_and_force_never_write_or_fabricate_attempt(self) -> None: + self.add(5) + for absent in (False, True): + if absent: + self.conn.execute("DELETE FROM maintenance_layers WHERE layer='clusters'") + self.conn.commit() + old, changes = self.row(), self.conn.total_changes + self.vector._lock.acquire() + try: + for force in (False, True): + with self.subTest(absent=absent, force=force): + result = self.run_layer(force=force) + self.assertEqual((result.outcome, result.error_category, result.attempted), + ("deferred", "ModelBusy", False)) + self.assertEqual(self.row(), old) + self.assertEqual(self.conn.total_changes, changes) + self.assertEqual(MAINTENANCE.plan_layers(self.conn, (self.spec(),), force=force), ()) + self.assertEqual(MAINTENANCE.layer_freshness( + self.state().source, self.state(), self.spec().resolve_dependency()), "dependency_deferred") + finally: + self.vector._lock.release() + self.assertEqual(self.run_layer().outcome, "success_empty") + self.assertFalse(self.run_layer().attempted) + + def test_busy_after_one_append_preserves_twenty_five_append_baseline(self) -> None: + self.add(5) + self.run_layer() + previous = self.row() + self.add(1) + self.assertFalse(self.run_layer().attempted) + self.vector._lock.acquire() + try: + self.assertEqual(self.run_layer(force=True).outcome, "deferred") + finally: + self.vector._lock.release() + self.assertEqual(self.row(), previous) + self.assertFalse(self.run_layer().attempted) + self.add(23) + self.assertFalse(self.run_layer().attempted) + self.add(1) + self.assertTrue(self.run_layer().attempted) + + def test_mid_compute_busy_restores_every_prior_field_and_original_output(self) -> None: + self.add(5) + self.run_layer() + self.conn.execute("UPDATE maintenance_layers SET builder_version=7,outcome='failed'," + "error_category='SyntheticFailure',run_generation='synthetic-old' WHERE layer='clusters'") + self.conn.commit() + previous, output = self.row(), self.output() + self.add(1) + self.block_during_compute() + result = self.run_layer(force=True) + self.release_model() + self.assertEqual((result.outcome, result.attempted), ("deferred", True)) + self.assertEqual(self.row(), previous) + self.assertEqual(self.output(), output) + self.assertFalse(self.conn.in_transaction) + self.assertFalse(self.run_layer().attempted) + + def test_mid_compute_busy_restores_absent_row_only_after_output_rollback(self) -> None: + self.add(1) + self.conn.execute("DELETE FROM maintenance_layers WHERE layer='clusters'") + self.conn.commit() + self.block_during_compute() + original, observations = MAINTENANCE._restore_deferred_attempt, [] + + def restore(*args): + observations.append((self.conn.in_transaction, self.output())) + return original(*args) + + with patch.object(MAINTENANCE, "_restore_deferred_attempt", restore): + result = self.run_layer() + self.release_model() + self.assertEqual((result.outcome, result.attempted), ("deferred", True)) + self.assertEqual(observations, [(False, ([], []))]) + self.assertIsNone(self.row()) + self.native.fit = lambda: setattr(self.native, "labels", [-1]) + self.assertTrue(self.run_layer().attempted) + + def test_busy_at_postbuild_dependency_and_outer_guard_rolls_back_checkpoint(self) -> None: + self.add(1) + self.run_layer() + self.add(1) + old_row, old_output = self.row(), self.output() + for boundary in ("dependency", "guard"): + with self.subTest(boundary=boundary): + spec, after_build = self.spec(), [] + + def count(conn): + after_build.append(0) + return spec.count_output(conn) + + def resolve(): + if after_build: + after_build[0] += 1 + if boundary == "dependency" and after_build[0] == 1: + self.vector._lock.acquire() + dependency = spec.resolve_dependency() + if boundary == "guard" and after_build and after_build[0] == 2: + self.vector._lock.acquire() + return dependency + + try: + result = MAINTENANCE.run_layers(self.conn, (spec._replace( + count_output=count, resolve_dependency=resolve),), force=True)[0] + finally: + self.release_model() + self.assertEqual((result.outcome, result.attempted), ("deferred", True)) + self.assertEqual(self.row(), old_row) + self.assertEqual(self.output(), old_output) + self.assertFalse(self.run_layer().attempted) + + def test_plain_runtime_error_is_a_failed_attempt_not_a_busy_deferral(self) -> None: + self.add(1) + with patch.object(self.cluster, "cluster_messages", side_effect=RuntimeError("Embedding model is busy")): + result = self.run_layer() + self.assertEqual((result.outcome, result.error_category, result.attempted), ("failed", "RuntimeError", True)) + self.assertEqual(self.row()[2], "failed") + + def test_cancellation_racing_busy_restores_baseline_and_stops_siblings(self) -> None: + self.add(1) + previous = self.row() + cancel = threading.Event() + self.block_during_compute(cancel) + results = MAINTENANCE.run_layers(self.conn, self.specs(), cancel=cancel) + self.release_model() + self.assertEqual(len(results), 1) + self.assertEqual((results[0].outcome, results[0].attempted), ("deferred", True)) + self.assertEqual(self.row(), previous) + self.assertEqual(self.output(), ([], [])) + + def test_changed_diagnostic_is_not_overwritten_even_with_same_run_generation(self) -> None: + self.add(1) + self.block_during_compute() + restore = MAINTENANCE._restore_deferred_attempt + writer = self.open_db() + + def replace_diagnostic(*args): + writer.execute("UPDATE maintenance_layers SET error_category='ExternalChange' WHERE layer='clusters'") + writer.commit() + return restore(*args) + + with patch.object(MAINTENANCE, "_restore_deferred_attempt", replace_diagnostic): + with self.assertRaisesRegex(MAINTENANCE.MaintenanceBusyError, "diagnostic changed"): + self.run_layer() + self.assertEqual(self.conn.execute("SELECT error_category FROM maintenance_layers WHERE layer='clusters'").fetchone()[0], + "ExternalChange") + self.assertEqual(self.output(), ([], [])) + self.assertFalse(self.conn.in_transaction) + + def test_changed_owner_or_generation_is_not_overwritten(self) -> None: + self.add(1) + self.block_during_compute() + restore = MAINTENANCE._restore_deferred_attempt + for kind in ("owner", "generation"): + with self.subTest(kind=kind): + def change_ownership(conn, layer, previous, running, owner): + if kind == "generation": + conn.execute("UPDATE maintenance_layers SET run_generation='synthetic-other' WHERE layer=?", (layer,)) + conn.commit() + return restore(conn, layer, previous, running, owner) + owners = MAINTENANCE._thread_owners.owners + owners[owner.path] = owner._replace(generation="synthetic-other") + try: + return restore(conn, layer, previous, running, owner) + finally: + owners[owner.path] = owner + + with patch.object(MAINTENANCE, "_restore_deferred_attempt", change_ownership): + with self.assertRaises(MAINTENANCE.MaintenanceBusyError): + self.run_layer(force=True) + self.release_model() + self.assertEqual(self.row()[2], "running") + if kind == "generation": + self.assertEqual(self.conn.execute("SELECT run_generation FROM maintenance_layers WHERE layer='clusters'").fetchone()[0], + "synthetic-other") + self.assertEqual(self.output(), ([], [])) + + def test_failed_restoration_commit_propagates_and_leaves_running_diagnostic(self) -> None: + self.add(1) + self.block_during_compute() + restore, rejected = MAINTENANCE._restore_deferred_attempt, [] + + def fail_commit(*args): + def authorize(action, first, *_args): + if action == sqlite3.SQLITE_TRANSACTION and first == "COMMIT" and not rejected: + rejected.append(True) + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + self.conn.set_authorizer(authorize) + return restore(*args) + + try: + with patch.object(MAINTENANCE, "_restore_deferred_attempt", fail_commit): + with self.assertRaises(sqlite3.DatabaseError): + self.run_layer() + finally: + self.conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.assertEqual(rejected, [True]) + self.assertEqual(self.row()[2], "running") + self.assertFalse(self.conn.in_transaction) + self.assertEqual(self.output(), ([], [])) + + def test_output_rollback_failure_never_restores_diagnostic(self) -> None: + self.add(1) + restore = MAINTENANCE._restore_deferred_attempt + + def fail(conn): + conn.execute("INSERT INTO message_clusters(message_id,cluster_id,noise) VALUES (1,-1,1)") + conn.set_authorizer(lambda action, first, *_args: sqlite3.SQLITE_DENY + if action == sqlite3.SQLITE_TRANSACTION and first == "ROLLBACK" else sqlite3.SQLITE_OK) + raise self.cluster.ClusterModelBusyError("synthetic contention") + + try: + with patch.object(self.cluster, "cluster_messages", fail), \ + patch.object(MAINTENANCE, "_restore_deferred_attempt", wraps=restore) as restoration: + with self.assertRaises(MAINTENANCE._LayerRollbackFailed): + self.run_layer() + restoration.assert_not_called() + finally: + self.conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.conn.rollback() + self.assertEqual(self.row()[2], "running") + self.assertEqual(self.output(), ([], [])) + + def test_borrowed_busy_rolls_back_only_layer_without_attempt_metadata(self) -> None: + self.add(1, commit=False) + previous = self.row() + self.block_during_compute() + result = self.run_layer(allow_caller_transaction=True) + self.release_model() + self.assertEqual((result.outcome, result.attempted, result.pending_caller_commit), ("deferred", True, True)) + self.assertEqual(self.row(), previous) + self.assertEqual(self.output(), ([], [])) + self.assertTrue(self.conn.in_transaction) + self.conn.commit() + self.assertEqual(self.conn.execute("SELECT count(*) FROM messages").fetchone()[0], 1) + self.assertEqual(self.output(), ([], [])) + + def test_model_owner_arriving_after_probe_never_blocks_inside_transaction(self) -> None: + self.add(1) + self.run_layer() + self.add(1) + previous, output = self.row(), self.output() + for borrowed in (False, True): + with self.subTest(borrowed=borrowed): + if borrowed: + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('synthetic-caller','retained')") + spec, attempts = self.spec(), [] + held = threading.Lock() + + class BoundaryLock: + armed = False + + def acquire(lock, blocking=True): + if lock.armed: + attempts.append(blocking) + if blocking: + raise AssertionError("Blocking model acquisition while SQLite transaction is retained") + return held.acquire(blocking=blocking) + + def release(lock): + held.release() + + def __enter__(lock): + lock.acquire() + return lock + + def __exit__(lock, *_args): + lock.release() + + boundary = BoundaryLock() + + def build(conn): + self.assertTrue(conn.in_transaction) + held.acquire() + boundary.armed = True + return spec.build(conn) + + with patch.object(self.vector, "_lock", boundary): + try: + result = MAINTENANCE.run_layers(self.conn, (spec._replace(build=build),), + force=True, allow_caller_transaction=borrowed)[0] + self.assertTrue(held.locked(), "The other model owner's lock must not be released") + finally: + held.release() + self.assertEqual((result.outcome, result.attempted), ("deferred", True)) + self.assertEqual(attempts, [False]) + self.assertEqual(self.row(), previous) + self.assertEqual(self.output(), output) + self.assertEqual(self.conn.in_transaction, borrowed) + if borrowed: + self.assertEqual(self.conn.execute("SELECT value FROM metadata WHERE key='synthetic-caller'").fetchone()[0], "retained") + self.conn.rollback() + self.assertIsNone(self.conn.execute("SELECT value FROM metadata WHERE key='synthetic-caller'").fetchone()) + + def test_standalone_initial_capture_wait_policy_stays_outside_sqlite_transaction(self) -> None: + self.add(1) + attempts = [] + held = threading.Lock() + conn = self.conn + + class RecordingLock: + def acquire(lock, blocking=True): + attempts.append((blocking, conn.in_transaction)) + return held.acquire(blocking=blocking) + + def release(lock): + held.release() + + def __enter__(lock): + lock.acquire() + return lock + + def __exit__(lock, *_args): + lock.release() + + with patch.object(self.vector, "_lock", RecordingLock()): + self.assertEqual(self.cluster.cluster_messages(self.conn), 0) + self.assertEqual(attempts, [(True, False), (False, True)]) + + +class TestRowFactoryCompatibility(AdapterFixture): + def test_row_factory_supports_real_cluster_builder_and_all_adapters(self) -> None: + self.add(5) + original_key = self.spec().resolve_dependency().key + self.conn.row_factory = sqlite3.Row + self.assertEqual(self.cluster.cluster_messages(self.conn), 0) + dependency = self.spec().resolve_dependency() + self.assertTrue(dependency.available, dependency) + self.assertEqual(dependency.key, original_key) + results = MAINTENANCE.run_layers(self.conn, self.specs()) + self.assertTrue(all(item.outcome in {"success", "success_empty"} for item in results), results) + self.assertFalse(self.run_layer().attempted) + self.assertTrue(all(not state.dependency.deferred for state in MAINTENANCE.read_layer_states(self.conn, self.specs()).values())) + + def test_row_factory_detects_explicit_rebuild_in_progress(self) -> None: + self.conn.row_factory = sqlite3.Row + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('vec_build_state:vec_messages_edge','in_progress')") + self.conn.commit() + dependency = self.spec().resolve_dependency() + self.assertEqual((dependency.available, dependency.error_category, dependency.deferred), + (False, "VectorRebuildInProgress", False)) + self.assertEqual(self.run_layer().outcome, "unavailable") + + +class TestCoveragePublication(AdapterFixture): + def test_failure_preserves_success_coverage_and_previous_output(self) -> None: + self.add(1) + self.run_layer() + old_output, old_state = self.output(), self.state() + with patch.object(self.cluster, "cluster_messages", side_effect=ValueError("synthetic failure")): + result = self.run_layer(force=True) + self.assertEqual(result.coverage, "vector_generation_unverified") + self.assertEqual(self.output(), old_output) + self.assertEqual(self.state().successful_coverage, old_state.successful_coverage) + self.assertEqual(self.state().successful_dependency, old_state.successful_dependency) + + def test_output_coverage_checkpoint_visible_together_at_real_commit(self) -> None: + self.add(1) + reader = self.open_db() + observations = [] + + def trace(sql): + if sql == "COMMIT" and self.vector._lock.locked(): + observations.append((self.output(reader), self.state(conn=reader).successful_coverage)) + + spec = self.spec() + original = spec.count_output + + def count(conn): + conn.set_trace_callback(trace) + return original(conn) + + result = MAINTENANCE.run_layers(self.conn, (spec._replace(count_output=count),))[0] + self.conn.set_trace_callback(None) + self.assertEqual(result.outcome, "success_empty") + self.assertEqual(observations, [(([], []), "unverified")]) + self.assertEqual(self.state(conn=reader).successful_coverage, "vector_generation_unverified") + self.assertEqual(len(self.output(reader)[0]), 1) + + def test_coverage_failure_rolls_back_generated_output_and_old_coverage(self) -> None: + self.add(1) + self.run_layer() + previous = self.output(), self.state().successful_coverage + spec = self.spec()._replace(read_coverage=lambda conn: "invalid") + result = MAINTENANCE.run_layers(self.conn, (spec,), force=True)[0] + self.assertEqual(result.outcome, "failed") + self.assertEqual((self.output(), self.state().successful_coverage), previous) + + def test_metadata_timestamp_change_does_not_schedule_but_rejects_inflight_publication(self) -> None: + self.add(1) + spec = self.spec() + original = spec.count_output + + def changed(conn): + result = original(conn) + conn.execute("UPDATE metadata SET updated_at='synthetic-new' WHERE key='embed_model'") + return result + + result = MAINTENANCE.run_layers(self.conn, (spec._replace(count_output=changed),))[0] + self.assertEqual(result.outcome, "failed") + self.assertEqual(self.output(), ([], [])) + self.assertEqual(self.state().successful_coverage, "unverified") + + def test_coverage_migration_preserves_epoch_and_provenance_and_is_idempotent(self) -> None: + self.add(1) + self.run_layer() + token, old = MAINTENANCE.read_source_revision(self.conn), self.state() + self.conn.execute("ALTER TABLE maintenance_layers RENAME TO synthetic_old_layers") + fields = [row[1] for row in self.conn.execute("PRAGMA table_info(synthetic_old_layers)") if row[1] != "successful_coverage"] + self.conn.execute("CREATE TABLE maintenance_layers AS SELECT " + ",".join(fields) + " FROM synthetic_old_layers") + self.conn.execute("CREATE UNIQUE INDEX synthetic_layer_name ON maintenance_layers(layer)") + self.conn.commit() + STORAGE._initialize_maintenance_tracking(self.conn) + self.assertEqual(MAINTENANCE.read_source_revision(self.conn), token) + self.assertEqual(self.state().successful_dependency, old.successful_dependency) + self.assertEqual(self.state().successful_coverage, "unverified") + version, changes = self.conn.execute("PRAGMA schema_version").fetchone()[0], self.conn.total_changes + STORAGE._initialize_maintenance_tracking(self.conn) + self.assertEqual((self.conn.execute("PRAGMA schema_version").fetchone()[0], self.conn.total_changes), (version, changes)) + + def test_owned_commit_failure_preserves_old_output_and_coverage(self) -> None: + self.add(1) + self.run_layer() + previous = self.output(), self.state().successful_coverage + self.native.fit = lambda: setattr(self.native, "labels", [0]) + denied = [] + + def authorize(action, first, *_args): + if action == sqlite3.SQLITE_TRANSACTION and first == "COMMIT" and not denied: + denied.append(True) + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + + spec = self.spec() + original = spec.count_output + + def count(conn): + conn.set_authorizer(authorize) + return original(conn) + + try: + result = MAINTENANCE.run_layers(self.conn, (spec._replace(count_output=count),), force=True)[0] + finally: + self.conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.assertEqual((result.outcome, denied), ("failed", [True])) + self.assertEqual((self.output(), self.state().successful_coverage), previous) + self.assertFalse(self.vector._lock.locked()) + + def test_coverage_read_snapshot_does_not_mix_concurrent_publications(self) -> None: + self.add(1) + self.run_layer("dunbar") + writer = self.open_db() + spec = self.spec("dunbar") + with MAINTENANCE.layer_read_snapshot(self.conn, "dunbar", spec.resolve_dependency()) as state: + writer.execute("INSERT INTO entity_relationships(entity_a,entity_b,relationship_type) " + "VALUES ('synthetic-other','synthetic-contact','contact')") + writer.commit() + writer_spec = next(item for item in self.specs(writer) if item.layer == "dunbar") + result = MAINTENANCE.run_layers(writer, (writer_spec,), force=True)[0] + self.assertEqual(result.coverage, "legacy_contacts_unowned") + self.assertEqual(state.successful_coverage, "complete") + self.assertEqual(self.modules["truememory.personality"].read_dunbar_coverage(self.conn)["status"], "managed_only") + self.assertEqual(self.state("dunbar").successful_coverage, "legacy_contacts_unowned") + + +class TestBorrowedPublication(AdapterFixture): + def test_coverage_migration_and_output_remain_inside_caller_transaction(self) -> None: + self.conn.execute("ALTER TABLE maintenance_layers RENAME TO synthetic_old_layers") + fields = [row[1] for row in self.conn.execute("PRAGMA table_info(synthetic_old_layers)") if row[1] != "successful_coverage"] + self.conn.execute("CREATE TABLE maintenance_layers AS SELECT " + ",".join(fields) + " FROM synthetic_old_layers") + self.conn.execute("CREATE UNIQUE INDEX synthetic_layer_name ON maintenance_layers(layer)") + self.conn.commit() + token = MAINTENANCE.read_source_revision(self.conn) + self.add(1, commit=False) + STORAGE._initialize_maintenance_tracking(self.conn) + result = self.run_layer(allow_caller_transaction=True) + self.assertTrue(result.pending_caller_commit) + self.conn.rollback() + self.assertNotIn("successful_coverage", {row[1] for row in self.conn.execute("PRAGMA table_info(maintenance_layers)")}) + self.assertEqual(MAINTENANCE.read_source_revision(self.conn), token) + self.assertEqual(self.output(), ([], [])) + + def test_default_rejects_caller_transaction_and_opt_in_keeps_all_work_rollbackable(self) -> None: + self.add(2, commit=False) + with self.assertRaisesRegex(RuntimeError, "clean transaction"): + MAINTENANCE.run_layers(self.conn, self.specs()) + reader = self.open_db() + results = MAINTENANCE.run_layers(self.conn, self.specs(), allow_caller_transaction=True) + self.assertTrue(all(item.pending_caller_commit for item in results)) + self.assertTrue(all(item.outcome in {"success", "success_empty"} for item in results), results) + self.assertTrue(self.conn.in_transaction) + self.assertEqual(reader.execute("SELECT count(*) FROM messages").fetchone()[0], 0) + self.assertEqual(self.state(conn=reader).successful_coverage, "unverified") + self.conn.rollback() + self.assertEqual(self.conn.execute("SELECT count(*) FROM messages").fetchone()[0], 0) + self.assertEqual(self.output(), ([], [])) + self.assertEqual(self.state().successful_coverage, "unverified") + + def test_read_only_caller_does_not_take_writer_for_running_diagnostic(self) -> None: + self.add(1) + writer = self.open_db() + observed = [] + + def compute(): + self.native.labels = [-1] + writer.execute("INSERT INTO metadata(key,value) VALUES ('synthetic-writer','yes')") + writer.commit() + observed.append(True) + + self.native.fit = compute + self.conn.execute("BEGIN") + # The writer deliberately makes the read snapshot stale. Publication + # and its failure diagnostic cannot upgrade that snapshot either. + with self.assertRaises(sqlite3.OperationalError): + self.run_layer(allow_caller_transaction=True) + self.assertEqual(observed, [True]) + self.assertEqual(self.output(), ([], [])) + self.conn.rollback() + + def test_failed_borrowed_layer_and_successful_sibling_preserve_unrelated_writes(self) -> None: + self.add(1, commit=False) + with patch.object(self.cluster, "cluster_messages", side_effect=ValueError("synthetic failure")): + results = MAINTENANCE.run_layers(self.conn, self.specs(), allow_caller_transaction=True) + self.assertEqual(results[0].outcome, "failed") + self.assertTrue(all(item.outcome in {"success", "success_empty"} for item in results[1:])) + self.assertEqual(self.conn.execute("SELECT count(*) FROM messages").fetchone()[0], 1) + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + self.assertEqual(self.state().outcome, "pending") + + def test_cluster_savepoint_release_holds_model_lock_but_return_releases_it(self) -> None: + self.add(1, commit=False) + releases = [] + self.conn.set_trace_callback(lambda sql: releases.append(self.vector._lock.locked()) + if sql.startswith("RELEASE truememory_layer_") else None) + result = self.run_layer(allow_caller_transaction=True) + self.conn.set_trace_callback(None) + self.assertEqual(releases, [True]) + self.assertTrue(result.pending_caller_commit) + self.assertEqual(result.coverage, "vector_generation_unverified") + self.assertFalse(self.vector._lock.locked()) + self.vector.EMBEDDING_MODEL = "synthetic-model-c" + self.conn.commit() + self.assertEqual(self.state().successful_coverage, "vector_generation_unverified") + + def test_successful_release_failure_rolls_back_output_before_caller_commit(self) -> None: + self.add(1, commit=False) + rejected = [] + + def authorize(action, first, second, *_args): + if action == sqlite3.SQLITE_SAVEPOINT and first == "RELEASE" and second.startswith("truememory_layer_") and not rejected: + rejected.append(True) + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + + self.conn.set_authorizer(authorize) + try: + result = self.run_layer(allow_caller_transaction=True) + finally: + self.conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.assertEqual(result.outcome, "failed") + self.assertEqual(rejected, [True]) + self.conn.commit() + self.assertEqual(self.output(), ([], [])) + self.assertEqual(self.state().successful_coverage, "unverified") + self.assertEqual(self.conn.execute("SELECT count(*) FROM messages").fetchone()[0], 1) + + def test_rejected_rollback_never_continues_with_attempt_write_or_release(self) -> None: + self.add(1, commit=False) + seen = [] + + def authorize(action, first, second, *_args): + if action == sqlite3.SQLITE_SAVEPOINT: + seen.append(first) + if first == "ROLLBACK" and second.startswith("truememory_layer_"): + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + + self.conn.set_authorizer(authorize) + try: + with patch.object(self.cluster, "cluster_messages", side_effect=ValueError("synthetic failure")): + with self.assertRaises(MAINTENANCE._LayerRollbackFailed): + self.run_layer(allow_caller_transaction=True) + finally: + self.conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.assertEqual(seen, ["BEGIN", "ROLLBACK"]) + self.assertFalse(self.vector._lock.locked()) + self.assertEqual(self.state().outcome, "pending") + self.conn.rollback() + + def test_cancellation_and_keyboard_interrupt_keep_caller_transaction(self) -> None: + self.add(1, commit=False) + cancel = threading.Event() + spec = self.spec() + original = spec.count_output + + def count(conn): + cancel.set() + return original(conn) + + result = MAINTENANCE.run_layers(self.conn, (spec._replace(count_output=count),), + allow_caller_transaction=True, cancel=cancel)[0] + self.assertEqual(result.outcome, "abandoned") + self.assertTrue(result.pending_caller_commit) + self.assertEqual(self.output(), ([], [])) + with patch.object(self.cluster, "cluster_messages", side_effect=KeyboardInterrupt()): + with self.assertRaises(KeyboardInterrupt): + self.run_layer(allow_caller_transaction=True, force=True) + self.assertTrue(self.conn.in_transaction) + self.assertFalse(self.vector._lock.locked()) + self.conn.rollback() + + def test_generic_guard_requires_explicit_borrowed_contract(self) -> None: + self.add(1, commit=False) + spec = self.spec()._replace(borrowed_publication_guard=None) + result = MAINTENANCE.run_layers(self.conn, (spec,), allow_caller_transaction=True)[0] + self.assertEqual(result.outcome, "unavailable") + self.assertEqual(self.output(), ([], [])) + self.conn.rollback() + + def test_borrowed_source_change_after_compute_rolls_back_layer_only(self) -> None: + self.add(1, commit=False) + spec = self.spec() + original = spec.count_output + + def count(conn): + result = original(conn) + conn.execute("UPDATE messages SET content='synthetic stale result' WHERE id=1") + return result + + result = MAINTENANCE.run_layers(self.conn, (spec._replace(count_output=count),), + allow_caller_transaction=True)[0] + self.assertEqual(result.outcome, "failed") + self.assertEqual(self.output(), ([], [])) + self.assertEqual(self.conn.execute("SELECT content FROM messages").fetchone()[0], "synthetic team launched Cedar") + self.assertEqual(self.state().successful_coverage, "unverified") + self.conn.rollback() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-maintenance-engine-routing-753.py b/tests/test-maintenance-engine-routing-753.py new file mode 100644 index 00000000..2d557330 --- /dev/null +++ b/tests/test-maintenance-engine-routing-753.py @@ -0,0 +1,846 @@ +"""Engine routing with real SQLite, event-controlled owners and synthetic models.""" + +import ast +import builtins +import datetime +import gc +import logging +import os +import select +import signal +import runpy +import sqlite3 +import sys +import threading +import time +import types +import unittest +import warnings +import weakref +from contextlib import contextmanager +from pathlib import Path +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] +ADAPTERS = runpy.run_path(str(Path(__file__).with_name("test-maintenance-adapters-753.py"))) +STORAGE, MAINTENANCE = ADAPTERS["STORAGE"], ADAPTERS["MAINTENANCE"] + + +def stdlib_module(name: str, modules: dict) -> types.ModuleType: + module = types.ModuleType("synthetic_routing_" + name) + + def safe_import(name, globals=None, locals=None, fromlist=(), level=0): + if name in modules: + return modules[name] + if name.startswith("truememory"): + raise ImportError("Synthetic boundary excludes native modules") + return builtins.__import__(name, globals, locals, fromlist, level) + + module.__dict__["__builtins__"] = dict(vars(builtins), __import__=safe_import) + path = ROOT / "truememory" / (name + ".py") + missing = object() + previous = sys.modules.get(module.__name__, missing) + sys.modules[module.__name__] = module + try: + exec(compile(path.read_text(encoding="utf-8"), str(path), "exec"), module.__dict__) + finally: + if previous is missing: + sys.modules.pop(module.__name__, None) + else: + sys.modules[module.__name__] = previous + return module + + +def load_engine(modules: dict) -> types.ModuleType: + modules["truememory.tier_switch.writer"] = stdlib_module("tier_switch/writer", modules) + # Runtime guards execute unchanged; only native identities are synthetic. + modules["psutil"] = types.SimpleNamespace() + for name in ("embedding_target", "tier_config", "tier_switch/cache", + "tier_switch/job", "tier_switch/source", "tier_switch/activation", + "tier_switch/serving"): + modules["truememory." + name.replace("/", ".")] = stdlib_module(name, modules) + serving = modules["truememory.tier_switch.serving"] + modules["truememory.tier_switch"] = types.SimpleNamespace(serving=serving) + vector = modules["truememory.vector_search"] + vector._frozen_embedding_target = None + vector._runtime_policy_tier = None + vector.resolve_tier = lambda: "edge" + vector_source = ast.parse((ROOT / "truememory/vector_search.py").read_text(encoding="utf-8")) + sep = next(node for node in vector_source.body + if isinstance(node, ast.FunctionDef) and node.name == "_active_sep_table") + exec(compile(ast.Module(body=[sep], type_ignores=[]), "actual-separation-resolver", "exec"), + vector.__dict__) + modules["truememory"] = types.SimpleNamespace( + vector_search=vector, + reranker=types.SimpleNamespace(get_current_reranker_name=lambda: "synthetic/reranker"), + ) + runtime = stdlib_module("tier_switch/runtime", modules) + runtime.sys = types.SimpleNamespace(modules=modules) + modules["truememory.tier_switch.runtime"] = runtime + vector.__dict__.update(__builtins__=runtime.__dict__["__builtins__"], + datetime=datetime, _model_generation=0) + metadata_nodes = [ast.ImportFrom(module="__future__", names=[ast.alias(name="annotations")], level=0)] + metadata_nodes.extend(node for node in vector_source.body + if isinstance(node, (ast.FunctionDef, ast.ClassDef)) + and node.name in {"_write_foreground_embedder_metadata_no_commit", "VectorPublicationChanged"}) + exec(compile(ast.fix_missing_locations(ast.Module(body=metadata_nodes, type_ignores=[])), + "actual-foreground-metadata", "exec"), vector.__dict__) + module = types.ModuleType("synthetic_routing_engine") + + def safe_import(name, globals=None, locals=None, fromlist=(), level=0): + if name in modules: + return modules[name] + if name.startswith("truememory"): + raise ImportError("Synthetic boundary excludes native modules") + return builtins.__import__(name, globals, locals, fromlist, level) + + module.__dict__.update({"__builtins__": dict(vars(builtins), __import__=safe_import), + "__file__": str(ROOT / "truememory/engine.py"), "Path": Path, "sqlite3": sqlite3, + "threading": threading, "time": time, "os": os, "warnings": warnings, + "engine_operation": lambda function: function, + "engine_handle_operation": runtime.engine_handle_operation, + "logger": logging.getLogger("synthetic-routing"), "MAX_CONTENT_LENGTH": 50_000, + "_env_int": lambda name, default, **kwargs: default}) + source = ast.parse((ROOT / "truememory/engine.py").read_text(encoding="utf-8")) + nodes = [ast.ImportFrom(module="__future__", names=[ast.alias(name="annotations")], level=0)] + for node in source.body: + if isinstance(node, (ast.FunctionDef, ast.ClassDef)): + nodes.append(node) + elif isinstance(node, ast.ImportFrom) and node.module == "truememory.storage": + module.__dict__.update({item.asname or item.name: getattr(STORAGE, item.name) for item in node.names}) + elif isinstance(node, ast.Assign): + for target in node.targets: + if isinstance(target, ast.Name): + if target.id.startswith("_HAS_"): + module.__dict__[target.id] = False + elif target.id in {"_ALLOWED_TABLES", "_ALLOWED_COLUMNS", "_ALL_VEC_TABLES", "_SQLITE_IN_CHUNK"}: + nodes.append(node) + module.__dict__["_HAS_CONSOLIDATION"] = True + exec(compile(ast.fix_missing_locations(ast.Module(body=nodes, type_ignores=[])), + str(ROOT / "truememory/engine.py"), "exec"), module.__dict__) + return module + + +class RoutingFixture(ADAPTERS["AdapterFixture"]): + def setUp(self) -> None: + style = stdlib_module("personality_style_vec", {}) + self.enterContext(patch.dict(sys.modules, {"truememory.personality_style_vec": style})) + super().setUp() + self.modules.update({"truememory.storage": STORAGE, "truememory.maintenance": MAINTENANCE, + "truememory.vector_search": self.vector, + "sqlite_vec": types.SimpleNamespace(load=lambda conn: None)}) + self.modules["truememory.rebuild_source"] = stdlib_module("rebuild_source", self.modules) + self.engine_module = load_engine(self.modules) + original_import = MAINTENANCE.__dict__["__builtins__"]["__import__"] + + def maintenance_import(name, globals=None, locals=None, fromlist=(), level=0): + if name in self.modules: + return self.modules[name] + if name.startswith("truememory"): + raise ImportError("Synthetic boundary excludes native modules") + return original_import(name, globals, locals, fromlist, level) + + self.enterContext(patch.dict(MAINTENANCE.__dict__["__builtins__"], + {"__import__": maintenance_import})) + create = STORAGE.create_db + + class ExtensionBoundary: + def __init__(boundary, conn): + boundary.conn = conn + + def __getattr__(boundary, name): + return getattr(boundary.conn, name) + + def enable_load_extension(boundary, enabled): + # The fixture uses ordinary BLOB tables and a fake sqlite-vec + # loader, including hosts compiled without extension support. + self.assertIs(type(enabled), bool) + + self.enterContext(patch.object(MAINTENANCE, "create_db", lambda path: ExtensionBoundary(create(path)))) + self.enterContext(patch.object(self.engine_module, "create_db", lambda path: ExtensionBoundary(create(path)))) + self.engine = self.new_engine(self.conn) + self.coordinator = self.engine._get_maintenance_coordinator() + self.addCleanup(self.coordinator.wait, 3) + self.addCleanup(self.coordinator.cancel) + + def new_engine(self, conn: sqlite3.Connection | None = None): + engine = self.engine_module.TrueMemoryEngine(self.path) + engine.conn = conn or self.open_db() + engine.ready = True + engine._has_consolidation = False + self.addCleanup(engine.close) + return engine + + def completed(self) -> None: + self.assertTrue(self.coordinator.wait(3), "Synthetic maintenance did not terminate") + self.assertFalse(self.coordinator.snapshot()["active"]) + + def fake_report(self): + return MAINTENANCE.MaintenanceReport((), "SKIPPED (synthetic)") + + +class TestCoordinatorHandoff(RoutingFixture): + def test_local_owner_retains_one_pending_wake_after_last_engine_close(self) -> None: + path = self.path.with_name("synthetic-detached.sqlite") + engine = self.engine_module.TrueMemoryEngine(path) + engine.conn, engine.ready = STORAGE.create_db(path), True + coordinator = engine._get_maintenance_coordinator() + reference, engine_reference = weakref.ref(coordinator), weakref.ref(engine) + entered, release = threading.Event(), threading.Event() + observed = [] + + def work(conn, owner, **kwargs): + observed.append(kwargs["threshold"]) + entered.set() + self.assertTrue(release.wait(3)) + return self.fake_report() + + with patch.object(MAINTENANCE, "run_engine_maintenance", work): + try: + with MAINTENANCE.maintenance_owner(path): + coordinator.request_layers(threshold=31) + coordinator.request_layers(threshold=37) + self.assertEqual(len(MAINTENANCE._owner_waiters), 1) + engine.close() + del engine, coordinator + gc.collect() + self.assertIsNone(engine_reference()) + self.assertIsNotNone(reference()) + self.assertTrue(entered.wait(2)) + self.assertEqual(MAINTENANCE._owner_waiters, {}) + coordinator = reference() + self.assertIsNotNone(coordinator) + finally: + release.set() + if reference() is not None: + self.assertTrue(reference().wait(3)) + self.assertEqual(observed, [37]) + + def test_cancel_releases_local_waiter_and_admission_cannot_restore_it(self) -> None: + original = MAINTENANCE._acquire_owner + + def cancelled_admission(path, on_busy=None): + self.coordinator.cancel() + return original(path, on_busy=on_busy) + + with patch.object(MAINTENANCE, "run_engine_maintenance") as work: + with MAINTENANCE.maintenance_owner(self.path): + self.coordinator.request_layers() + self.assertEqual(len(MAINTENANCE._owner_waiters), 1) + self.coordinator.cancel() + self.assertEqual(MAINTENANCE._owner_waiters, {}) + with patch.object(MAINTENANCE, "_acquire_owner", cancelled_admission): + self.assertFalse(self.coordinator.request_layers()) + self.assertEqual(self.coordinator.snapshot()["pending_generations"], 0) + self.assertEqual(MAINTENANCE._owner_waiters, {}) + work.assert_not_called() + self.assertEqual(self.coordinator.status, ("cancelled", None)) + + def test_delayed_start_exception_cannot_cancel_successor_or_its_pending_wake(self) -> None: + start = threading.Thread.start + entered, release = threading.Event(), threading.Event() + attempts, observed, cancellation = [], [], [] + + def work(conn, coordinator, **kwargs): + observed.append(kwargs["threshold"]) + if kwargs["threshold"] == 31: + entered.set() + self.assertTrue(release.wait(3)) + cancellation.append(kwargs["cancel"].is_set()) + return self.fake_report() + + def late_failure(worker): + attempts.append(worker) + start(worker) + if len(attempts) == 1: + self.completed() + self.assertTrue(self.coordinator.request_layers(threshold=31)) + self.assertTrue(entered.wait(2)) + self.assertFalse(self.coordinator.request_layers(threshold=37)) + raise RuntimeError("synthetic delayed start failure") + + with patch.object(MAINTENANCE.threading.Thread, "start", late_failure), \ + patch.object(MAINTENANCE, "run_engine_maintenance", work): + try: + with self.assertRaisesRegex(RuntimeError, "synthetic delayed start failure"): + self.coordinator.request_layers() + self.assertFalse(self.coordinator._cancel.is_set()) + self.assertEqual(self.coordinator.snapshot()["pending_generations"], 1) + finally: + release.set() + self.completed() + self.assertEqual(observed, [25, 31, 37]) + self.assertEqual(cancellation, [False]) + + @unittest.skipUnless(hasattr(os, "fork"), "POSIX fork required") + def test_fork_discards_inherited_pending_worker_and_rejects_stale_coordinator(self) -> None: + entered, release = threading.Event(), threading.Event() + + def active(conn, cancel): + entered.set() + self.assertTrue(release.wait(3)) + + self.coordinator.request(active) + self.assertTrue(entered.wait(2)) + self.coordinator.request_layers() + waiting_path = self.path.with_name("synthetic-fork-waiter.sqlite") + self.enterContext(MAINTENANCE.maintenance_owner(waiting_path)) + waiting = MAINTENANCE.get_coordinator(waiting_path) + self.addCleanup(waiting.cancel) + waiting.request_layers() + self.assertEqual(len(MAINTENANCE._owner_waiters), 1) + read_fd, write_fd = os.pipe() + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + child = os.fork() + if child == 0: + os.close(read_fd) + try: + rejected = False + try: + self.coordinator.request_layers() + except RuntimeError: + rejected = True + fresh = MAINTENANCE.get_coordinator(self.path) + clean = (not fresh.snapshot()["active"] and fresh.snapshot()["pending_generations"] == 0 + and not MAINTENANCE._owner_waiters) + os.write(write_fd, b"PASS" if rejected and clean else b"FAIL") + finally: + os.close(write_fd) + os._exit(0) + os.close(write_fd) + try: + self.assertTrue(select.select([read_fd], [], [], 2)[0], "Synthetic child did not answer") + self.assertEqual(os.read(read_fd, 4), b"PASS") + finally: + os.close(read_fd) + if os.waitpid(child, os.WNOHANG)[0] == 0: + os.kill(child, signal.SIGKILL) + os.waitpid(child, 0) + self.coordinator.cancel() + release.set() + self.completed() + + def test_start_failure_services_only_a_real_later_notification(self) -> None: + start = threading.Thread.start + attempts, observed = [], [] + + def fail_once(worker): + attempts.append(True) + if len(attempts) == 1: + self.assertFalse(self.coordinator.request_layers(threshold=31)) + raise RuntimeError("synthetic start failure") + return start(worker) + + with patch.object(MAINTENANCE.threading.Thread, "start", fail_once), \ + patch.object(MAINTENANCE, "run_engine_maintenance", lambda *args, **kwargs: observed.append(kwargs["threshold"]) or self.fake_report()): + with self.assertRaisesRegex(RuntimeError, "synthetic start failure"): + self.coordinator.request_layers() + self.completed() + self.assertEqual(observed, [31]) + self.assertEqual(len(attempts), 2) + + def test_start_failure_without_notification_has_no_retry(self) -> None: + with patch.object(MAINTENANCE.threading.Thread, "start", side_effect=RuntimeError("synthetic")) as start: + with self.assertRaises(RuntimeError): + self.coordinator.request_layers() + self.completed() + start.assert_called_once() + self.assertEqual(self.coordinator.snapshot()["pending_generations"], 0) + self.assertEqual(self.coordinator.status, ("failed", "worker_start")) + + def test_close_detaches_without_cancelling_or_retaining_engine(self) -> None: + entered, release = threading.Event(), threading.Event() + + def active(conn, cancel): + entered.set() + self.assertTrue(release.wait(2)) + self.assertFalse(cancel.is_set()) + self.assertEqual(conn.execute("SELECT 1").fetchone()[0], 1) + + with patch.object(MAINTENANCE, "run_engine_maintenance", lambda *args, **kwargs: self.fake_report()): + self.coordinator.request(active) + self.assertTrue(entered.wait(2)) + engine = self.engine_module.TrueMemoryEngine(self.path) + engine.conn, engine.ready = self.open_db(), True + reference = weakref.ref(engine) + engine._maybe_auto_consolidate() + self.assertEqual(self.coordinator.snapshot()["pending_generations"], 1) + engine.close() + del engine + gc.collect() + self.assertIsNone(reference()) + self.assertFalse(self.coordinator.wait(0)) + release.set() + self.completed() + self.assertEqual(self.coordinator.status, ("success", None)) + + def test_busy_admission_and_concurrent_owner_release_cannot_lose_wake(self) -> None: + held, release, leaving, ran = (threading.Event() for _ in range(4)) + original_acquire, original_release = MAINTENANCE._acquire_owner, MAINTENANCE._release_owner + + def owner(): + with MAINTENANCE.maintenance_owner(self.path): + held.set() + self.assertTrue(release.wait(2)) + + def release_owner(fd): + leaving.set() + original_release(fd) + + def acquire(path, on_busy=None): + def busy(): + on_busy() + release.set() + self.assertTrue(leaving.wait(2)) + return original_acquire(path, on_busy=busy if on_busy is not None else None) + + owner_thread = threading.Thread(target=owner) + with patch.object(MAINTENANCE, "_release_owner", release_owner), \ + patch.object(MAINTENANCE, "run_engine_maintenance", lambda *args, **kwargs: ran.set() or self.fake_report()): + owner_thread.start() + try: + self.assertTrue(held.wait(2)) + with patch.object(MAINTENANCE, "_acquire_owner", acquire): + self.assertFalse(self.coordinator.request_layers()) + self.assertTrue(ran.wait(2)) + self.completed() + finally: + release.set() + owner_thread.join(3) + self.assertFalse(owner_thread.is_alive()) + + def test_many_active_notifications_coalesce_to_one_static_successor(self) -> None: + entered, release = threading.Event(), threading.Event() + observed = [] + + def active(conn, cancel): + entered.set() + self.assertTrue(release.wait(2)) + + def successor(conn, coordinator, **kwargs): + observed.append((conn is self.conn, kwargs["threshold"])) + return self.fake_report() + + with patch.object(MAINTENANCE, "run_engine_maintenance", successor): + self.assertTrue(self.coordinator.request(active)) + self.assertTrue(entered.wait(2)) + for threshold in range(1, 101): + self.assertFalse(self.coordinator.request_layers(threshold=threshold)) + self.assertEqual(self.coordinator.snapshot()["pending_generations"], 1) + self.assertFalse(self.coordinator.wait(0)) + release.set() + self.completed() + self.assertEqual(observed, [(False, 100)]) + self.assertEqual(self.coordinator.snapshot()["pending_generations"], 0) + + def test_local_synchronous_owner_release_services_pending_wake(self) -> None: + observed = [] + with patch.object(MAINTENANCE, "run_engine_maintenance", lambda *args, **kwargs: observed.append(True) or self.fake_report()): + with MAINTENANCE.maintenance_owner(self.path): + self.assertFalse(self.coordinator.request_layers()) + self.assertEqual(self.coordinator.status, ("busy", None)) + self.assertEqual(self.coordinator.snapshot()["pending_generations"], 1) + self.assertEqual(observed, []) + self.completed() + self.assertEqual(observed, [True]) + + def test_external_busy_requires_new_boundary_and_does_not_spin(self) -> None: + observed = [] + with patch.object(MAINTENANCE, "try_file_lock", return_value=None), \ + patch.object(MAINTENANCE.threading, "Thread") as thread: + self.assertFalse(self.coordinator.request_layers()) + thread.assert_not_called() + self.assertEqual(self.coordinator.snapshot()["pending_generations"], 1) + self.assertEqual(MAINTENANCE._owner_waiters, {}) + with patch.object(MAINTENANCE, "run_engine_maintenance", lambda *args, **kwargs: observed.append(True) or self.fake_report()): + self.assertTrue(self.coordinator.request_layers()) + self.completed() + self.assertEqual(observed, [True]) + + def test_wake_between_descriptor_release_and_idle_transition_is_not_lost(self) -> None: + released, finish = threading.Event(), threading.Event() + original_release = MAINTENANCE._release_owner + observed = [] + first = [] + + def pause(fd): + original_release(fd) + if not first: + first.append(True) + released.set() + self.assertTrue(finish.wait(2)) + + with patch.object(MAINTENANCE, "_release_owner", pause), \ + patch.object(MAINTENANCE, "run_engine_maintenance", lambda *args, **kwargs: observed.append(True) or self.fake_report()): + self.assertTrue(self.coordinator.request(lambda conn, cancel: None)) + self.assertTrue(released.wait(2)) + self.assertFalse(self.coordinator.request_layers()) + self.assertFalse(self.coordinator.wait(0)) + finish.set() + self.completed() + self.assertEqual(observed, [True]) + + def test_model_deferral_has_no_self_generated_successor(self) -> None: + observed = [] + report = MAINTENANCE.MaintenanceReport((MAINTENANCE.LayerResult( + "clusters", "cluster_messages", "deferred", None, 0., "ModelBusy", False),), "SKIPPED") + with patch.object(MAINTENANCE, "run_engine_maintenance", lambda *args, **kwargs: observed.append(True) or report): + self.assertTrue(self.coordinator.request_layers()) + self.completed() + self.assertEqual(observed, [True]) + self.assertEqual(self.coordinator.status, ("deferred", "ModelBusy")) + self.assertEqual(self.coordinator.snapshot()["pending_generations"], 0) + + def test_cancellation_drops_pending_but_keeps_active_ownership(self) -> None: + entered, release = threading.Event(), threading.Event() + + def active(conn, cancel): + entered.set() + self.assertTrue(release.wait(2)) + + with patch.object(MAINTENANCE, "run_engine_maintenance") as successor: + self.coordinator.request(active) + self.assertTrue(entered.wait(2)) + self.coordinator.request_layers() + self.coordinator.cancel() + self.assertFalse(self.coordinator.wait(0)) + self.assertFalse(self.coordinator.request_layers()) + self.assertEqual(self.coordinator.snapshot()["pending_generations"], 0) + release.set() + self.completed() + successor.assert_not_called() + self.assertEqual(self.coordinator.status, ("cancelled", None)) + + +class TestEngineRouting(RoutingFixture): + def test_status_reports_categorical_layer_failure_and_caller_transaction(self) -> None: + self.add(1) + with patch.object(self.cluster, "cluster_messages", side_effect=RuntimeError("synthetic private sentinel")): + self.engine.consolidate() + state = self.engine.get_stats()["maintenance"] + self.assertEqual(len(state["layers"]), 8) + self.assertEqual(state["layers"]["clusters"]["outcome"], "failed") + self.assertEqual(state["layers"]["clusters"]["error_category"], "RuntimeError") + self.assertEqual(state["layers"]["clusters"]["freshness"], "untrusted") + self.assertNotIn("synthetic private sentinel", str(state)) + self.assertNotIn(str(self.path), str(state)) + self.conn.execute("UPDATE messages SET content='synthetic uncommitted status'") + self.assertTrue(self.engine.get_stats()["maintenance"]["pending_caller_commit"]) + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + + def test_manual_repair_restores_actual_cluster_search_and_automatic_eligibility(self) -> None: + self.add(5) + self.native.fit = lambda: setattr(self.native, "labels", [0, 0, 1, 1, 2]) + self.engine._has_clustering = False + self.engine._has_vectors = True + self.modules["truememory.agentic_search"] = types.SimpleNamespace(normalize_scores=lambda rows: rows) + with patch.object(self.coordinator, "request_layers") as request, \ + patch.object(self.engine, "search", return_value=[{"id": 1, "score": .5}]), \ + patch.object(self.engine, "_entity_focused_search", return_value=[]), \ + patch.object(self.engine, "_check_sufficiency", return_value=True), \ + patch.object(self.engine, "_apply_surprise_boost", side_effect=lambda rows: rows), \ + patch.object(self.engine, "_clean_results", side_effect=lambda rows, *_args, **_kwargs: rows), \ + patch.object(self.engine_module, "search_clustered", return_value=[{"id": 2, "score": .5}], create=True) as search: + self.engine._maybe_auto_consolidate() + request.assert_not_called() + self.engine.search_agentic("synthetic query", use_hyde=False, use_reranker=False) + search.assert_not_called() + result = self.engine.consolidate() + self.assertIn("3 clusters", result["cluster_messages"]) + self.assertTrue(self.engine._has_clustering) + self.assertTrue(self.engine._has_consolidation) + rows = self.engine.search_agentic("synthetic query", use_hyde=False, use_reranker=False) + search.assert_called_once() + self.assertEqual([row["id"] for row in rows], [1, 2]) + request.assert_not_called() + self.conn.execute("UPDATE messages SET content='synthetic repaired correction' WHERE id=1") + self.conn.commit() + self.engine._maybe_auto_consolidate() + request.assert_called_once_with(threshold=25) + + def test_failed_manual_repair_does_not_enable_capabilities_or_remove_prior_success(self) -> None: + self.engine._has_clustering = False + with patch.object(self.cluster, "cluster_messages", side_effect=RuntimeError("synthetic")), \ + patch.object(self.modules["truememory.consolidation"], "build_summaries", side_effect=RuntimeError("synthetic")): + result = self.engine.consolidate() + self.assertIn("ERROR", result["cluster_messages"]) + self.assertIn("ERROR", result["build_summaries"]) + self.assertFalse(self.engine._has_clustering) + self.assertFalse(self.engine._has_consolidation) + self.engine._has_clustering = self.engine._has_consolidation = True + self.engine.consolidate() + self.assertTrue(self.engine._has_clustering) + self.assertTrue(self.engine._has_consolidation) + + def test_unavailable_manual_cluster_repair_preserves_only_prior_capability(self) -> None: + self.engine._has_clustering = False + with patch.object(self.modules["sqlite_vec"], "load", side_effect=RuntimeError("synthetic")): + self.assertIn("UNAVAILABLE", self.engine.consolidate()["cluster_messages"]) + self.assertFalse(self.engine._has_clustering) + self.engine._has_clustering = True + self.assertIn("UNAVAILABLE", self.engine.consolidate()["cluster_messages"]) + self.assertTrue(self.engine._has_clustering) + + def test_first_open_manual_does_not_start_its_own_competitor(self) -> None: + self.add(1) + engine = self.engine_module.TrueMemoryEngine(self.path) + self.addCleanup(engine.close) + with patch.object(self.coordinator, "request_layers") as request: + result = engine.consolidate() + request.assert_not_called() + self.assertTrue(engine.ready) + self.assertEqual(set(result), set(MAINTENANCE._MAINTENANCE_RESULT_KEYS)) + + def test_current_empty_startup_and_rolled_back_correction_do_not_schedule(self) -> None: + self.engine.consolidate() + self.engine._has_consolidation = True + with patch.object(self.coordinator, "request_layers") as request: + self.engine._maybe_startup_consolidate() + self.engine._ensure_connection() + request.assert_not_called() + self.add(1) + self.engine.consolidate() + with patch.object(self.coordinator, "request_layers") as request: + self.conn.execute("UPDATE messages SET content='synthetic uncommitted'") + self.engine._ensure_connection() + self.assertEqual(self.engine._maintenance_pending_reason, "pending_caller_commit") + self.conn.rollback() + self.engine._ensure_connection() + request.assert_not_called() + self.conn.execute("UPDATE messages SET content='synthetic committed'") + self.conn.commit() + self.engine._ensure_connection() + request.assert_called_once_with(threshold=25) + + def test_add_probe_uses_no_corpus_scan_native_import_or_blocking_model_lock(self) -> None: + self.engine.consolidate() + self.engine._has_consolidation = True + operations = [] + underlying = self.vector._lock + + class ProbeLock: + def acquire(lock, blocking=True): + self.assertFalse(blocking) + return underlying.acquire(blocking=False) + + def release(lock): + underlying.release() + + def authorize(action, table, column, *_args): + if action == sqlite3.SQLITE_READ: + operations.append((table, column)) + return sqlite3.SQLITE_OK + + self.conn.set_authorizer(authorize) + try: + with patch.object(self.vector, "_lock", ProbeLock()), \ + patch.object(MAINTENANCE, "_installed_dependency", side_effect=AssertionError("per-add package scan")), \ + patch.object(self.coordinator, "request_layers", side_effect=AssertionError("unexpected work")): + self.engine._maybe_auto_consolidate() + self.assertEqual(self.engine._maintenance_pending_reason, "awaiting_eligibility") + finally: + self.conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.assertFalse({table for table, _ in operations}.intersection({"messages", "vec_messages_edge", "summaries", "cluster_centroids"})) + + def test_source_commit_followed_by_a17_vector_error_still_notifies(self) -> None: + self.add(1) + self.engine.consolidate() + self.engine._has_vectors = self.engine._has_consolidation = True + changed = type("VectorPublicationChanged", (RuntimeError,), {}) + + @contextmanager + def publication(*args, **kwargs): + raise changed("synthetic model replacement") + yield + + def capture(conn: sqlite3.Connection) -> tuple[object, str]: + self.assertIs(conn, self.engine.conn) + return object(), "synthetic-identity" + + fields = {"_capture_rebuild_model": capture, + "_encode_with_mps_fallback": lambda *args: [[1., 2.]], + "_build_sep_text": lambda *args: "synthetic separated text", + "_foreground_vector_publication": publication, + "serialize_f32": lambda values: b"synthetic-unused-vector", + "_write_embedder_metadata_no_commit": lambda conn: None, + "VectorPublicationChanged": changed} + with patch.dict(self.vector.__dict__, fields), patch.object(self.coordinator, "request_layers") as request: + with self.assertRaisesRegex(changed, "Source update committed"): + self.engine.update(1, content="synthetic corrected source") + request.assert_called_once_with(threshold=25) + self.assertFalse(self.conn.in_transaction) + self.assertFalse(self.engine._write_lock.locked()) + self.assertEqual(self.conn.execute("SELECT content FROM messages WHERE id=1").fetchone()[0], "synthetic corrected source") + + def test_manual_busy_is_truthful_and_does_not_queue_forced_work(self) -> None: + entered, release = threading.Event(), threading.Event() + + def active(conn, cancel): + entered.set() + self.assertTrue(release.wait(2)) + + self.coordinator.request(active) + self.assertTrue(entered.wait(2)) + try: + result = self.engine.consolidate() + self.assertEqual(set(result), set(MAINTENANCE._MAINTENANCE_RESULT_KEYS)) + self.assertTrue(all(value.startswith("BUSY") for value in result.values())) + self.assertEqual(self.coordinator.snapshot()["pending_generations"], 0) + finally: + release.set() + self.completed() + + def test_file_manual_uses_dedicated_connection_all_eight_and_preferences(self) -> None: + self.add(5) + seen = [] + original = MAINTENANCE.run_engine_maintenance + + def run(conn, coordinator, **kwargs): + seen.append(conn) + self.assertIsNot(conn, self.conn) + self.assertTrue(self.engine._write_lock.acquire(blocking=False)) + self.engine._write_lock.release() + return original(conn, coordinator, **kwargs) + + with patch.object(MAINTENANCE, "run_engine_maintenance", run), \ + patch.object(self.coordinator, "request_layers") as automatic: + result = self.engine.consolidate() + automatic.assert_not_called() + self.assertEqual(set(result), set(MAINTENANCE._MAINTENANCE_RESULT_KEYS)) + self.assertTrue(all("ERROR" not in value and "UNAVAILABLE" not in value + for key, value in result.items() if key != "extract_preferences"), result) + self.assertEqual(result["extract_preferences"], "UNAVAILABLE (ScheduledPreferencesUnsupported)") + self.assertIn("vector_generation_unverified", result["cluster_messages"]) + with self.assertRaises(sqlite3.ProgrammingError): + seen[0].execute("SELECT 1") + + def test_24_then_25_committed_adds_across_engines_use_durable_counts(self) -> None: + self.engine.consolidate() + second = self.new_engine() + self.engine._has_consolidation = second._has_consolidation = True + with patch.object(self.coordinator, "request_layers", wraps=self.coordinator.request_layers) as request: + for index in range(24): + engine = self.engine if index % 2 else second + engine.add("synthetic append", sender="synthetic-sender") + request.assert_not_called() + second.add("synthetic twenty fifth", sender="synthetic-sender") + self.completed() + self.assertGreaterEqual(request.call_count, 1) + states = MAINTENANCE.read_layer_states(self.conn, self.specs()) + self.assertTrue(all(state.attempted_insert_count == 25 for state in states.values()), states) + + def test_borrowed_manual_preserves_caller_writes_and_rollback(self) -> None: + self.add(1, commit=False) + with patch.object(self.engine_module, "create_db", side_effect=AssertionError("new handle")), \ + patch.object(self.modules["truememory.personality"], "extract_preferences", + side_effect=AssertionError("unsupported preference work")) as preferences: + result = self.engine.consolidate() + preferences.assert_not_called() + self.assertEqual(set(result), set(MAINTENANCE._MAINTENANCE_RESULT_KEYS)) + self.assertTrue(all("pending caller commit" in result[key] + for key in MAINTENANCE._MAINTENANCE_RESULT_KEYS if key != "extract_preferences"), result) + self.assertEqual(result["extract_preferences"], "UNAVAILABLE (ScheduledPreferencesUnsupported)") + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + self.assertEqual(self.conn.execute("SELECT count(*) FROM messages").fetchone()[0], 0) + self.assertEqual(self.output(), ([], [])) + + def test_in_memory_auto_stays_pending_and_manual_uses_original_rows(self) -> None: + conn = STORAGE.create_db(":memory:") + self.addCleanup(conn.close) + engine = self.engine_module.TrueMemoryEngine(":memory:") + engine.conn, engine.ready = conn, True + self.addCleanup(engine.close) + conn.execute("INSERT INTO messages(content,timestamp) VALUES ('synthetic in memory','2026-01-01')") + conn.commit() + with patch.object(MAINTENANCE, "create_db", side_effect=AssertionError("new handle")), \ + patch.object(MAINTENANCE.threading, "Thread", side_effect=AssertionError("memory worker")): + engine._maybe_auto_consolidate() + self.assertEqual(engine._maintenance_coordinator.status, ("pending_in_memory", None)) + result = engine.consolidate() + self.assertIn("1 episodes", result["detect_episodes"]) + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], 1) + + +class TestCapabilityEvidence(RoutingFixture): + def test_transient_model_busy_cannot_clear_prior_extension_failure_baseline(self) -> None: + self.add(5) + with patch.object(self.modules["sqlite_vec"], "load", side_effect=RuntimeError("synthetic")): + self.engine.consolidate() + prior = self.coordinator.capability_snapshot()[2] + self.add(1) + worker = MAINTENANCE.create_db(self.path) + try: + with self.vector._lock: + MAINTENANCE._prepare_worker_extensions(worker, self.coordinator) + finally: + worker.close() + self.assertEqual(self.coordinator.capability_snapshot()[2], prior) + self.assertEqual(MAINTENANCE.plan_layers(self.conn, MAINTENANCE.engine_layer_specs(self.conn, self.coordinator)), ()) + + def test_reopen_during_worker_failure_preserves_current_run_local_evidence(self) -> None: + self.add(1) + + def fail_and_refresh(conn): + self.coordinator.refresh_capabilities() + raise RuntimeError("synthetic load failure after reopen") + + with patch.object(self.modules["sqlite_vec"], "load", fail_and_refresh): + result = self.engine.consolidate() + self.assertIn("VectorExtensionUnavailable", result["cluster_messages"]) + self.assertIsNone(self.coordinator.capability_snapshot()[2]) + + def test_worker_extension_failure_keeps_same_key_and_25_insert_baseline(self) -> None: + self.add(5) + loader = self.modules["sqlite_vec"] + with patch.object(loader, "load", side_effect=RuntimeError("synthetic private sentinel")): + result = self.engine.consolidate() + self.assertIn("VectorExtensionUnavailable", result["cluster_messages"]) + self.engine._has_consolidation = True + with patch.object(self.coordinator, "request_layers") as request: + self.engine.add("synthetic sixth") + self.engine._ensure_connection() + request.assert_not_called() + state = MAINTENANCE.read_layer_states(self.conn, MAINTENANCE.engine_layer_specs(self.conn, self.coordinator))["clusters"] + self.assertEqual(state.attempted_insert_count, 5) + self.assertEqual(state.dependency.key, state.attempted_dependency) + repaired = self.engine.consolidate() + self.assertNotIn("UNAVAILABLE", repaired["cluster_messages"]) + self.assertIsNone(self.coordinator.capability_snapshot()[2]) + + def test_reopen_refreshes_static_evidence_and_new_model_invalidates_failure_key(self) -> None: + self.add(1) + with patch.object(self.modules["sqlite_vec"], "load", side_effect=RuntimeError("synthetic")): + self.engine.consolidate() + epoch, versions, failure = self.coordinator.capability_snapshot() + self.assertEqual(len(versions), 3) + self.assertIsNotNone(failure) + self.vector.EMBEDDING_MODEL = "synthetic-model-c" + dependency = MAINTENANCE.engine_layer_specs(self.conn, self.coordinator)[0].resolve_dependency() + self.assertTrue(dependency.available) + engine = self.new_engine() + self.assertIs(engine._get_maintenance_coordinator(), self.coordinator) + self.assertGreater(self.coordinator.capability_snapshot()[0], epoch) + self.assertIsNone(self.coordinator.capability_snapshot()[2]) + + def test_old_worker_cannot_publish_capability_error_after_refresh(self) -> None: + epoch, _, _ = self.coordinator.capability_snapshot() + failure = MAINTENANCE.make_layer_dependency(1, available=False, error_category="VectorExtensionUnavailable") + self.coordinator.refresh_capabilities() + self.coordinator._publish_extension_evidence(epoch, failure) + self.assertIsNone(self.coordinator.capability_snapshot()[2]) + + def test_actual_connection_unavailability_is_not_overridden_by_cached_success(self) -> None: + dependency = self.spec().resolve_dependency() + unavailable = MAINTENANCE.make_layer_dependency(1, MAINTENANCE._dependency_parameters(dependency), + available=False, error_category="VectorExtensionUnavailable") + with patch.object(MAINTENANCE, "_cluster_schedule_dependency", return_value=unavailable): + observed = MAINTENANCE.engine_layer_specs(self.conn, self.coordinator)[0].resolve_dependency() + self.assertEqual(observed, unavailable) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-maintenance-fixture-isolation-753.py b/tests/test-maintenance-fixture-isolation-753.py new file mode 100644 index 00000000..d2c438b6 --- /dev/null +++ b/tests/test-maintenance-fixture-isolation-753.py @@ -0,0 +1,245 @@ +"""Cross-fixture worker isolation using actual fixtures and SQLite in memory.""" + +import ast +import builtins +import sqlite3 +import sys +import threading +import types +import unittest +import uuid +from collections.abc import Callable +from pathlib import Path +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] + + +def fixture_loader(process_sys: object) -> Callable[[str], dict]: + """Keep fixture imports and module registries inside one synthetic process.""" + loaded = {} + + def safe_import(name: str, globals: dict | None = None, locals: dict | None = None, + fromlist: tuple = (), level: int = 0) -> object: + if name == "sys": + return process_sys + if name == "runpy": + return types.SimpleNamespace(run_path=load) + if name == "truememory.personality_style_vec": + return process_sys.modules[name] + if name.startswith("truememory") or name.split(".")[0] in {"torch", "numpy", "hdbscan", "sqlite_vec"}: + raise AssertionError("Unexpected application/native import: " + name) + return builtins.__import__(name, globals, locals, fromlist, level) + + def safe_exec(code: object, namespace: dict) -> None: + namespace.setdefault("__builtins__", fixture_builtins) + name = namespace.get("__name__", "") + module = process_sys.modules.get(name) + if (not name.startswith("synthetic_") or not isinstance(module, types.ModuleType) + or module.__dict__ is not namespace): + exec(code, namespace) + return + # stdlib dataclasses resolves postponed annotations through real sys. + # Expose only this temporary synthetic namespace, never application peers. + absent = object() + previous = sys.modules.get(name, absent) + sys.modules[name] = module + try: + exec(code, namespace) + finally: + if previous is absent: + del sys.modules[name] + else: + sys.modules[name] = previous + + fixture_builtins = dict(vars(builtins), __import__=safe_import, exec=safe_exec) + + def load(path: str) -> dict: + path = Path(path) + if path.name in loaded: + return loaded[path.name] + tree = ast.parse(path.read_text(encoding="utf-8")) + # Existing fixture loaders also read production source. Pin their + # decoding in this harness on Python 3.10/Windows without editing it. + for node in ast.walk(tree): + if (isinstance(node, ast.Call) and isinstance(node.func, ast.Attribute) + and node.func.attr == "read_text" and not node.args + and not any(keyword.arg == "encoding" for keyword in node.keywords)): + node.keywords.append(ast.keyword(arg="encoding", value=ast.Constant(value="utf-8"))) + namespace = {"__file__": str(path), "__name__": "synthetic_fixture_" + path.stem, + "__builtins__": fixture_builtins} + exec(compile(ast.fix_missing_locations(tree), str(path), "exec"), namespace) + loaded[path.name] = namespace + return namespace + + return load + + +class TestFixtureIsolation(unittest.TestCase): + def setUp(self) -> None: + values = vars(sys).copy() + values["modules"] = {} + self.process_sys = types.SimpleNamespace(**values) + load = fixture_loader(self.process_sys) + self.routing = load(str(ROOT / "tests/test-maintenance-engine-routing-753.py")) + self.adapters = self.routing["ADAPTERS"] + self.storage = self.routing["STORAGE"] + self.maintenance = self.routing["MAINTENANCE"] + _, self.foreign_vector, _ = self.adapters["CLUSTERS"]["load_clustering"]() + self.process_sys.modules["truememory.vector_search"] = self.foreign_vector + load_stdlib = self.routing["stdlib_module"] + style = load_stdlib("personality_style_vec", {}) + self.process_sys.modules["truememory.personality_style_vec"] = style + self.enter_context(patch.dict(self.routing, {"stdlib_module": lambda name, modules: + style if name == "personality_style_vec" else load_stdlib(name, modules)})) + self.registry_before = self.process_sys.modules.copy() + # Primitive loaders have their own import guard. Bind their sys view + # explicitly so both modules start in the same synthetic process. + self.enter_context(patch.object(self.maintenance, "sys", self.process_sys)) + self.foreign = types.ModuleType("synthetic_foreign_maintenance") + self.foreign.__dict__.update(self.maintenance.__dict__) + # Functions need their own globals to represent an unrelated worker. + source = (ROOT / "truememory/maintenance.py").read_text(encoding="utf-8") + exec(compile(source, "synthetic-foreign-maintenance", "exec"), self.foreign.__dict__) + self.foreign.sys = self.process_sys + # This fixture invokes a worker directly for an in-memory coordinator. + # Production workers reopen file paths; give its real opener the explicit + # in-memory identity instead of the scheduler's non-launchable None. + class MemoryWorkerCoordinator(self.foreign.MaintenanceCoordinator): + def __init__(self, path: object) -> None: + super().__init__(":memory:" if path is None else path) + + self.foreign.MaintenanceCoordinator = MemoryWorkerCoordinator + + self.uri = "file:synthetic-fixture-" + uuid.uuid4().hex + "?mode=memory&cache=shared" + self.connect = sqlite3.connect + self.keeper = self.connect(self.uri, uri=True, check_same_thread=False) + self.addCleanup(self.keeper.close) + self.create = self.storage.create_db + self.coordinator = self.maintenance.MaintenanceCoordinator(None) + fixture_test = self + + class MemoryDirectory: + name = ":memory:" + + def cleanup(self) -> None: + pass + + class MemoryFixture(self.routing["RoutingFixture"]): + def open_db(self) -> sqlite3.Connection: + conn = fixture_test.open_memory() + self.addCleanup(conn.close) + return conn + + self.enter_context(patch.dict(self.adapters, {"tempfile": types.SimpleNamespace( + TemporaryDirectory=lambda **kwargs: MemoryDirectory())})) + self.enter_context(patch.object(self.storage, "create_db", self.open_memory)) + self.enter_context(patch.object(self.maintenance, "get_coordinator", lambda path: self.coordinator)) + self.fixture = MemoryFixture() + self.addCleanup(self.fixture.doCleanups) + self.fixture.setUp() + + def enter_context(self, context: object) -> object: + result = context.__enter__() + self.addCleanup(context.__exit__, None, None, None) + return result + + def open_memory(self, path: object = None) -> sqlite3.Connection: + sqlite_view = types.SimpleNamespace(**dict(vars(sqlite3), connect=lambda *args, **kwargs: + self.connect(self.uri, uri=True, check_same_thread=False))) + with patch.object(self.storage, "sqlite3", sqlite_view): + return self.create(":memory:") + + def states(self, conn: sqlite3.Connection | None = None) -> dict: + conn = conn or self.fixture.engine.conn + specs = self.maintenance.engine_layer_specs(conn, self.coordinator) + return self.maintenance.read_layer_states(conn, specs) + + def reopen(self) -> object: + self.fixture.engine.close() + reopened = self.fixture.new_engine() + reopened._has_consolidation = True + return reopened + + def test_fixture_does_not_publish_its_vector_into_process_registry(self) -> None: + self.assertIs(self.process_sys.modules["truememory.vector_search"], self.foreign_vector) + self.assertIs(self.maintenance.sys.modules["truememory.vector_search"], self.fixture.vector) + self.assertIsNot(self.maintenance.sys.modules, self.process_sys.modules) + self.assertEqual(self.process_sys.modules, self.registry_before) + + def test_interloper_cannot_defer_bootstrap_or_reset_reopen_threshold(self) -> None: + foreign_conn = self.connect(":memory:", check_same_thread=False) + self.fixture.conn.backup(foreign_conn) + entered, release = threading.Event(), threading.Event() + observed = [] + + class BlockedSnapshot: + def __getattr__(boundary, name: str) -> object: + return getattr(foreign_conn, name) + + def execute(boundary, sql: str, *args: object) -> sqlite3.Cursor: + if sql.startswith("SELECT type,name,rootpage,sql"): + entered.set() + if not release.wait(3): + raise AssertionError("Synthetic worker was not released") + return foreign_conn.execute(sql, *args) + + worker_coordinator = self.foreign.MaintenanceCoordinator(None) + + def work(conn: sqlite3.Connection, cancel: threading.Event) -> object: + observed.append(self.foreign.sys.modules["truememory.vector_search"]) + self.foreign._cluster_schedule_dependency(conn, versions=dict(self.fixture.versions)) + return self.foreign.MaintenanceReport((), "SKIPPED (synthetic)") + + with patch.object(self.foreign, "create_db", lambda path: BlockedSnapshot()), \ + patch.object(self.foreign, "_release_owner", lambda fd: None): + worker = threading.Thread(target=worker_coordinator._run, args=(-1, work)) + worker.start() + try: + self.assertTrue(entered.wait(3)) + result = self.fixture.engine.consolidate() + states = self.states() + finally: + release.set() + worker.join(3) + foreign_conn.close() + self.assertFalse(worker.is_alive()) + self.assertEqual(worker_coordinator.status, ("success", None)) + self.assertNotIn("DEFERRED", result["cluster_messages"]) + self.assertEqual(observed, [self.foreign_vector]) + self.assertEqual(len(states), 8) + self.assertTrue(all(state.attempted_insert_count == 0 for state in states.values()), states) + self.assertTrue(all(state.outcome == "success_empty" for state in states.values()), states) + self.fixture.add(24) + before = self.maintenance.read_source_revision(self.fixture.conn) + versions = self.coordinator.capability_snapshot()[1] + reopened = self.reopen() + with patch.object(self.coordinator, "request_layers") as request: + reopened._maybe_auto_consolidate() + request.assert_not_called() + self.assertEqual(self.maintenance.read_source_revision(reopened.conn), before) + self.assertEqual(self.coordinator.capability_snapshot()[1], versions) + reopened.add("synthetic twenty fifth") + request.assert_called_once_with(threshold=25) + + def test_intentionally_deferred_bootstrap_still_recovers_at_twenty_four(self) -> None: + with self.fixture.vector._lock: + result = self.fixture.engine.consolidate() + self.assertEqual(result["cluster_messages"], "DEFERRED (ModelBusy)") + self.assertIsNone(self.states()["clusters"].attempted_insert_count) + self.fixture.add(24) + reopened = self.reopen() + with patch.object(self.coordinator, "request_layers") as request: + reopened._maybe_auto_consolidate() + request.assert_called_once_with(threshold=25) + report = self.maintenance.run_engine_maintenance( + reopened.conn, self.coordinator, threshold=25, prepare_extensions=False) + self.assertEqual([item.layer for item in report.results if item.attempted], ["clusters"]) + states = self.states(reopened.conn) + self.assertEqual(states["clusters"].attempted_insert_count, 24) + self.assertTrue(all(state.attempted_insert_count == 0 for name, state in states.items() if name != "clusters")) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-maintenance-foreground-admission-753.py b/tests/test-maintenance-foreground-admission-753.py new file mode 100644 index 00000000..e95b4fd8 --- /dev/null +++ b/tests/test-maintenance-foreground-admission-753.py @@ -0,0 +1,501 @@ +"""Foreground publication admission with real SQLite and synthetic models.""" + +import runpy +import sqlite3 +import threading +import types +import unittest +from pathlib import Path +from unittest.mock import patch + + +FOREGROUND = runpy.run_path(str(Path(__file__).with_name("test-rebuild-foreground-publication-756.py"))) +CLUSTERS = runpy.run_path(str(Path(__file__).with_name("test-cluster-publication-749.py"))) + + +class AdmissionFixture(unittest.TestCase): + setUp = FOREGROUND["TestForegroundPublication"].setUp + seed = FOREGROUND["TestForegroundPublication"].seed + ids = FOREGROUND["TestForegroundPublication"].ids + writer = FOREGROUND["TestForegroundPublication"].writer + engine = FOREGROUND["TestForegroundPublication"].engine + + def start(self, action, name="synthetic-worker"): + results, done = [], threading.Event() + + def run(): + try: + results.append(action()) + except BaseException as error: + results.append(error) + finally: + done.set() + + thread = threading.Thread(target=run, name=name, daemon=True) + thread.start() + self.addCleanup(thread.join, 3) + return thread, results, done + + def released(self): + self.assertTrue(self.vector._lock.acquire(blocking=False)) + self.vector._lock.release() + + def sentinel(self): + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('synthetic-caller','pending')") + + def preserved(self): + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.conn.execute("SELECT value FROM metadata WHERE key='synthetic-caller'").fetchone()[0], "pending") + + +class TestForegroundAdmission(AdmissionFixture): + def test_style_computation_runs_outside_capture_and_publication_locks(self) -> None: + engine = self.engine() + engine._has_style_vec = True + maintenance = self.modules["maintenance"] + maintenance.prepare_style_maintenance(self.conn) + self.conn.execute("BEGIN IMMEDIATE") + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('style_vec_hash_version','2')") + maintenance.record_layer_success_in_transaction( + self.conn, layer="style_vectors", + dependency=maintenance.style_layer_spec(self.conn).resolve_dependency(), + source=maintenance.read_source_revision(self.conn), output_count=0, + ) + self.conn.commit() + events = [] + style = [0.25, 0.75] + + def compute(content: str) -> list[float]: + self.assertFalse(engine._write_lock.locked()) + self.assertFalse(self.vector._lock.locked()) + self.assertFalse(self.conn.in_transaction) + events.append("compute") + return style + + def publish(conn: sqlite3.Connection, sender: str, content: str, + _pre_computed_vec: list[float]) -> None: + self.assertTrue(engine._write_lock.locked()) + self.assertTrue(self.vector._lock.locked()) + self.assertTrue(conn.in_transaction) + self.assertIs(_pre_computed_vec, style) + events.append("publish") + + module = types.ModuleType("synthetic_style") + module.compute_style_vector = compute + with patch.dict(self.modules, personality_style_vec=module), \ + patch.dict(engine.add.__globals__, _update_style_vec=publish): + engine.add("synthetic styled content", sender="synthetic sender") + self.assertEqual(events, ["compute", "publish"]) + self.assertEqual(self.ids(), [1]) + self.assertEqual(self.ids("vec_messages_sep"), [1]) + + def test_actual_cluster_snapshot_delays_add_before_writer_then_add_commits_both_vectors(self): + engine = self.engine() + cluster, runtime, _native = CLUSTERS["load_clustering"]() + encoding, snapshot, admission, release = (threading.Event() for _ in range(4)) + underlying = self.vector._lock + fixture = self + + class ObservedLock: + def acquire(self, blocking=True): + if threading.current_thread().name == "synthetic-add" and snapshot.is_set(): + fixture.assertTrue(blocking) + fixture.assertFalse(engine.conn.in_transaction) + fixture.assertTrue(engine._write_lock.locked()) + admission.set() + return underlying.acquire(blocking=blocking) + + def release(self): + underlying.release() + + def __enter__(self): + self.acquire() + return self + + def __exit__(self, *_args): + self.release() + + self.vector._lock = runtime._lock = ObservedLock() + runtime.EMBEDDING_MODEL = self.vector.EMBEDDING_MODEL + cluster_conn, independent = self.writer(), self.writer() + original = cluster._get_all_embeddings + + def read(conn): + snapshot.set() + self.assertTrue(release.wait(3)) + return original(conn) + + def encode(texts): + if len(self.calls) == 1: + encoding.set() + self.assertTrue(snapshot.wait(3)) + return [[1., 0.] for _ in texts] + + def consolidate(): + self.assertTrue(encoding.wait(3)) + return cluster.cluster_messages(cluster_conn) + + self.on_encode = encode + with patch.object(cluster, "_get_all_embeddings", read): + cluster_thread, cluster_result, cluster_done = self.start(consolidate) + add_thread, result, done = self.start(lambda: engine.add("synthetic first add"), "synthetic-add") + try: + self.assertTrue(admission.wait(2)) + self.assertFalse(done.is_set()) + independent.execute("INSERT INTO metadata(key,value) VALUES ('synthetic-independent','committed')") + independent.commit() + self.assertFalse(self.conn.in_transaction) + finally: + release.set() + add_thread.join(3) + cluster_thread.join(3) + self.assertTrue(done.is_set()) + self.assertTrue(cluster_done.is_set()) + self.assertIsInstance(cluster_result[0], (int, RuntimeError), cluster_result) + self.assertIsInstance(result[0], dict, result) + self.assertEqual(self.ids(), [1]) + self.assertEqual(self.ids("vec_messages_sep"), [1]) + self.assertEqual(self.conn.execute("SELECT content FROM messages").fetchone()[0], "synthetic first add") + self.released() + + def test_writer_owner_defers_nonblocking_model_fence_and_foreground_finishes(self): + owner = self.writer() + owner.execute("BEGIN IMMEDIATE") + admission, attempted = threading.Event(), threading.Event() + underlying = self.vector._lock + + class AdmittedLock: + def acquire(self, blocking=True): + acquired = underlying.acquire(blocking=blocking) + if acquired and threading.current_thread().name == "synthetic-publish": + admission.set() + return acquired + + def release(self): + underlying.release() + + self.vector._lock = AdmittedLock() + identity = (0, self.vector.EMBEDDING_MODEL, 2) + + def publish(): + with self.vector._foreground_vector_publication(self.conn, identity, 1): + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('synthetic-published','yes')") + + thread, result, done = self.start(publish, "synthetic-publish") + try: + self.assertTrue(admission.wait(2)) + with self.assertRaisesRegex(RuntimeError, "model is busy"): + with self.vector._rebuild_model_fence(identity): + attempted.set() + self.assertFalse(attempted.is_set()) + finally: + owner.rollback() + thread.join(3) + self.assertTrue(done.is_set()) + self.assertEqual(result, [None]) + self.released() + + def test_add_waiting_for_same_engine_update_does_not_own_model(self): + self.seed((1,)) + engine = self.engine() + update_ready, add_waiting = threading.Event(), threading.Event() + underlying = engine._write_lock + + class EngineLock: + def __enter__(self): + if threading.current_thread().name == "synthetic-add": + add_waiting.set() + underlying.acquire() + return self + + def __exit__(self, *_args): + underlying.release() + + engine._write_lock = EngineLock() + original = engine.update.__globals__["update_message"] + + def update(conn, memory_id, **fields): + result = original(conn, memory_id, **fields) + update_ready.set() + self.assertTrue(add_waiting.wait(3)) + self.released() + return result + + with patch.dict(engine.update.__globals__, update_message=update): + update_thread, updated, update_done = self.start(lambda: engine.update(1, "synthetic updated"), "synthetic-update") + self.assertTrue(update_ready.wait(2)) + add_thread, added, add_done = self.start(lambda: engine.add("synthetic added"), "synthetic-add") + update_thread.join(3) + add_thread.join(3) + self.assertTrue(update_done.is_set()) + self.assertTrue(add_done.is_set()) + self.assertIsInstance(updated[0], dict, updated) + self.assertIsInstance(added[0], dict, added) + self.assertEqual(self.ids(), [1, 2]) + self.assertEqual(self.ids("vec_messages_sep"), [1, 2]) + + def test_same_engine_writer_finishes_before_capture_classifies_transaction(self) -> None: + self.seed((1,)) + for operation in ("add", "update"): + with self.subTest(operation=operation): + engine = self.engine() + writer_ready, waiting, release = (threading.Event() for _ in range(3)) + underlying = engine._write_lock + + class EngineLock: + def __enter__(self) -> "EngineLock": + if threading.current_thread().name == "synthetic-capture": + waiting.set() + underlying.acquire() + return self + + def __exit__(self, *_args: object) -> None: + underlying.release() + + engine._write_lock = EngineLock() + self.vector._model = None + loads = [] + + def load() -> object: + self.assertFalse(self.conn.in_transaction) + self.assertTrue(underlying.locked()) + loads.append(True) + self.vector._model = self.model + return self.model + + def writer() -> None: + with engine._write_lock: + self.conn.execute("BEGIN IMMEDIATE") + self.conn.execute("INSERT INTO metadata(key,value) VALUES (?, 'pending')", + ("synthetic-owner-" + operation,)) + writer_ready.set() + self.assertTrue(release.wait(3)) + self.conn.commit() + + with patch.object(self.vector, "get_model", load): + writer_thread, written, writer_done = self.start(writer) + self.assertTrue(writer_ready.wait(2)) + action = (lambda: engine.add("synthetic added")) if operation == "add" else ( + lambda: engine.update(1, "synthetic updated") + ) + mutation_thread, result, done = self.start(action, "synthetic-capture") + try: + self.assertTrue(waiting.wait(2)) + self.assertFalse(done.is_set()) + self.assertEqual(loads, []) + finally: + release.set() + mutation_thread.join(3) + writer_thread.join(3) + self.assertTrue(writer_done.is_set()) + self.assertEqual(written, [None]) + self.assertTrue(done.is_set()) + self.assertIsInstance(result[0], dict, result) + self.assertEqual(loads, [True]) + self.assertFalse(self.conn.in_transaction) + self.released() + + def test_generation_change_during_model_load_rejects_add_and_update_before_source_write(self): + self.seed((1,)) + engine = self.engine() + + def changed(): + self.vector._model_generation += 1 + return self.model + + with patch.object(self.vector, "get_model", changed): + for action in (lambda: engine.add("synthetic rejected"), lambda: engine.update(1, "synthetic rejected")): + with self.assertRaisesRegex(self.vector.VectorPublicationChanged, "changed while loading"): + action() + self.assertEqual(self.conn.execute("SELECT id,content FROM messages").fetchall(), [(1, "synthetic-1")]) + self.assertEqual(self.ids(), []) + self.released() + + def test_generation_change_while_waiting_rejects_before_opening_transaction(self): + captured = (0, self.vector.EMBEDDING_MODEL, 2) + attempted = threading.Event() + underlying = self.vector._lock + + class WaitingLock: + def acquire(self, blocking=True): + attempted.set() + return underlying.acquire(blocking=blocking) + + def release(self): + underlying.release() + + self.vector._lock = WaitingLock() + underlying.acquire() + + def publish(): + with self.vector._foreground_vector_publication(self.conn, captured, 1): + self.fail("Changed generation entered publication") + + thread, results, done = self.start(publish) + try: + self.assertTrue(attempted.wait(2)) + self.assertFalse(done.is_set()) + self.assertFalse(self.conn.in_transaction) + self.vector._model_generation += 1 + finally: + underlying.release() + thread.join(3) + self.assertIsInstance(results[0], self.vector.VectorPublicationChanged) + self.assertIn("model changed", str(results[0])) + self.assertFalse(self.conn.in_transaction) + self.released() + + def test_add_failure_after_source_insert_rolls_back_source_and_vectors(self): + engine = self.engine() + with patch.object(self.vector, "_validate_foreground_vector_target", side_effect=self.vector.VectorPublicationChanged("synthetic target changed")): + with self.assertRaises(self.vector.VectorPublicationChanged): + engine.add("synthetic rollback") + self.assertEqual(self.conn.execute("SELECT count(*) FROM messages").fetchone()[0], 0) + self.assertEqual(self.ids(), []) + self.assertEqual(self.ids("vec_messages_sep"), []) + self.assertFalse(self.conn.in_transaction) + self.released() + + def test_admission_begin_and_commit_failures_release_model(self): + real = self.conn + + class FailedConnection: + def __init__(self, phase): + self.phase = phase + + def __getattr__(self, name): + return getattr(real, name) + + def execute(self, sql, *args): + if self.phase == "begin" and sql == "BEGIN IMMEDIATE": + raise sqlite3.OperationalError("synthetic writer admission") + return real.execute(sql, *args) + + def commit(self): + if self.phase == "commit": + raise sqlite3.OperationalError("synthetic commit failure") + return real.commit() + + for phase in ("begin", "commit"): + with self.subTest(phase=phase): + with self.assertRaises(sqlite3.OperationalError): + self.engine(FailedConnection(phase)).add("synthetic rollback") + self.assertFalse(real.in_transaction) + self.assertEqual(real.execute("SELECT count(*) FROM messages").fetchone()[0], 0) + self.assertEqual(self.ids(), []) + self.released() + + def test_owned_model_load_and_encoding_finish_before_writer(self): + def load(): + self.assertFalse(self.conn.in_transaction) + return self.model + + def encode(texts): + self.assertFalse(self.conn.in_transaction) + self.released() + return [[1., 0.] for _ in texts] + + self.on_encode = encode + with patch.object(self.vector, "get_model", load): + self.engine().add("synthetic outside writer") + self.assertEqual(self.ids(), [1]) + + +class TestBorrowedAdmission(AdmissionFixture): + def actions(self): + engine = self.engine() + return ( + ("add", lambda: engine.add("synthetic rejected")), + ("update", lambda: engine.update(1, "synthetic rejected")), + ("embed_single", lambda: self.vector.embed_single(self.conn, 1, "synthetic-1")), + ("completion", lambda: self.vector.build_vectors(self.conn)), + ("separation", lambda: self.vector.build_separation_vectors(self.conn)), + ) + + def test_busy_capture_defers_every_mutation_without_waiting_or_loading(self): + self.seed((1,)) + self.sentinel() + calls = [] + + class BusyLock: + def acquire(self, blocking=True): + calls.append(blocking) + if blocking: + raise AssertionError("Borrowed caller attempted a blocking model acquisition") + return False + + with patch.object(self.vector, "_lock", BusyLock()), patch.object(self.vector, "get_model", side_effect=AssertionError("Borrowed model load")): + for name, action in self.actions(): + with self.subTest(action=name): + with self.assertRaisesRegex(self.vector.VectorPublicationChanged, "model is busy"): + action() + self.preserved() + self.assertEqual(calls, [False] * 5) + self.assertEqual(self.calls, []) + self.assertEqual(self.conn.execute("SELECT id,content FROM messages").fetchall(), [(1, "synthetic-1")]) + self.assertEqual(self.ids(), []) + self.conn.rollback() + self.released() + + def test_unloaded_capture_defers_every_mutation_and_preserves_sentinel(self): + self.seed((1,)) + self.sentinel() + self.vector._model = None + with patch.object(self.vector, "get_model", side_effect=AssertionError("Borrowed model load")) as loader: + for name, action in self.actions(): + with self.subTest(action=name): + with self.assertRaisesRegex(self.vector.VectorPublicationChanged, "not loaded"): + action() + self.preserved() + self.released() + loader.assert_not_called() + self.assertEqual(self.calls, []) + self.assertEqual(self.conn.execute("SELECT id,content FROM messages").fetchall(), [(1, "synthetic-1")]) + self.conn.rollback() + + def test_borrowed_loaded_snapshot_skips_loader_and_add_remains_rollbackable(self): + self.sentinel() + with patch.object(self.vector, "get_model", side_effect=AssertionError("Borrowed model load")) as loader: + item = self.engine().add("synthetic borrowed") + loader.assert_not_called() + self.preserved() + self.assertEqual(self.ids(), [item["id"]]) + self.assertEqual(self.ids("vec_messages_sep"), [item["id"]]) + self.conn.rollback() + self.assertEqual(self.conn.execute("SELECT count(*) FROM messages").fetchone()[0], 0) + self.assertEqual(self.ids(), []) + self.released() + + def test_borrowed_publication_contention_preserves_caller_transaction(self): + self.sentinel() + held, release = threading.Event(), threading.Event() + + def hold(): + with self.vector._lock: + held.set() + self.assertTrue(release.wait(3)) + + holder = [] + + def encode(texts): + if len(self.calls) == 2: + holder.append(self.start(hold)[0]) + self.assertTrue(held.wait(2)) + return [[1., 0.] for _ in texts] + + self.on_encode = encode + try: + with self.assertRaisesRegex(self.vector.VectorPublicationChanged, "model is busy"): + self.engine().add("synthetic rejected") + self.preserved() + self.assertEqual(self.conn.execute("SELECT count(*) FROM messages").fetchone()[0], 0) + finally: + release.set() + for thread in holder: + thread.join(3) + self.conn.rollback() + self.released() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-maintenance-ownership-753.py b/tests/test-maintenance-ownership-753.py new file mode 100644 index 00000000..e5f5eb1b --- /dev/null +++ b/tests/test-maintenance-ownership-753.py @@ -0,0 +1,362 @@ +"""Real process/file ownership tests; work and databases are synthetic.""" + +import os +import queue +import runpy +import sqlite3 +import subprocess +import sys +import tempfile +import threading +import unittest +import warnings +from pathlib import Path +from unittest.mock import patch + + +# Reuse the explicitly synthetic loader, without importing the application or +# any native/model package. This test file also serves as the child executable. +_loader = runpy.run_path(str(Path(__file__).with_name("test-maintenance-source-revision-753.py"))) +STORAGE, MAINTENANCE = _loader["STORAGE"], _loader["MAINTENANCE"] + + +class TestMaintenanceOwnership(unittest.TestCase): + def setUp(self) -> None: + self.temp = tempfile.TemporaryDirectory(prefix="synthetic-owner-") + self.addCleanup(self.temp.cleanup) + self.path = Path(self.temp.name) / "synthetic.sqlite" + self.conn = STORAGE.create_db(self.path) + self.addCleanup(self.conn.close) + + def child(self) -> subprocess.Popen: + process = subprocess.Popen( + [sys.executable, "-B", str(Path(__file__).resolve()), "--synthetic-owner", str(self.path)], + stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, + ) + + def stop() -> None: + if process.poll() is None: + process.terminate() + try: + process.communicate(timeout=3) + except subprocess.TimeoutExpired: + process.kill() + process.communicate(timeout=3) + + self.addCleanup(stop) + return process + + def line(self, process: subprocess.Popen) -> str: + received = queue.Queue() + reader = threading.Thread(target=lambda: received.put(process.stdout.readline()), daemon=True) + reader.start() + return received.get(timeout=5).strip() + + def blocked_work(self, coordinator) -> tuple[threading.Event, threading.Event, list]: + entered = threading.Event() + release = threading.Event() + connections = [] + + def work(conn: sqlite3.Connection, cancel: threading.Event) -> None: + connections.append(conn) + entered.set() + release.wait(3) + + self.assertTrue(coordinator.request(work)) + self.addCleanup(coordinator.wait, 4) + self.addCleanup(release.set) + self.assertTrue(entered.wait(2)) + return entered, release, connections + + def test_canonical_paths_share_coordinator_and_one_worker(self) -> None: + first = MAINTENANCE.get_coordinator(self.path) + second = MAINTENANCE.get_coordinator(self.path.parent / "." / self.path.name) + self.assertIs(first, second) + _, release, _ = self.blocked_work(first) + self.assertFalse(second.request(lambda conn, cancel: self.fail("duplicate worker"))) + self.assertEqual(self.line(self.child()), "BUSY") + release.set() + self.assertTrue(first.wait(2)) + self.assertEqual(first.status, ("success", None)) + + def test_symlink_alias_resolves_to_same_database_owner(self) -> None: + alias = self.path.parent / "synthetic-alias.sqlite" + try: + alias.symlink_to(self.path) + except OSError: + self.skipTest("Creating file symlinks is unavailable on this test host") + self.assertIs(MAINTENANCE.get_coordinator(alias), MAINTENANCE.get_coordinator(self.path)) + + def test_process_owner_crash_releases_actual_lock(self) -> None: + first = self.child() + self.assertEqual(self.line(first), "READY") + self.assertEqual(self.line(self.child()), "BUSY") + first.kill() + first.wait(timeout=3) + replacement = self.child() + self.assertEqual(self.line(replacement), "READY") + replacement.stdin.write("release\n") + replacement.stdin.flush() + self.assertEqual(self.line(replacement), "DONE") + + def test_cancellation_keeps_ownership_until_work_returns(self) -> None: + coordinator = MAINTENANCE.get_coordinator(self.path) + _, release, _ = self.blocked_work(coordinator) + coordinator.cancel() + self.assertFalse(coordinator.wait(0)) + self.assertEqual(self.line(self.child()), "BUSY") + release.set() + self.assertTrue(coordinator.wait(2)) + self.assertEqual(coordinator.status, ("cancelled", None)) + self.assertEqual(self.line(self.child()), "READY") + + def test_worker_owns_connection_and_foreground_close_cannot_close_it(self) -> None: + coordinator = MAINTENANCE.get_coordinator(self.path) + _, release, connections = self.blocked_work(coordinator) + self.assertIsNot(connections[0], self.conn) + self.conn.close() + self.assertEqual(connections[0].execute("SELECT count(*) FROM messages").fetchone(), (0,)) + release.set() + self.assertTrue(coordinator.wait(2)) + with self.assertRaises(sqlite3.ProgrammingError): + connections[0].execute("SELECT 1") + + def test_owner_is_retained_through_connection_close(self) -> None: + coordinator = MAINTENANCE.get_coordinator(self.path) + entered = threading.Event() + release = threading.Event() + create = MAINTENANCE.create_db + + class PausedClose: + def __init__(self, conn: sqlite3.Connection) -> None: + self.conn = conn + + def __getattr__(self, name: str) -> object: + return getattr(self.conn, name) + + def close(self) -> None: + entered.set() + release.wait(3) + self.conn.close() + + with patch.object(MAINTENANCE, "create_db", side_effect=lambda path: PausedClose(create(path))): + self.assertTrue(coordinator.request(lambda conn, cancel: None)) + self.addCleanup(coordinator.wait, 4) + self.addCleanup(release.set) + self.assertTrue(entered.wait(2)) + self.assertEqual(self.line(self.child()), "BUSY") + self.assertFalse(coordinator.wait(0)) + release.set() + self.assertTrue(coordinator.wait(2)) + + def test_thread_start_failure_releases_owner_for_next_request(self) -> None: + coordinator = MAINTENANCE.get_coordinator(self.path) + with patch.object(MAINTENANCE.threading.Thread, "start", side_effect=RuntimeError("synthetic start failure")): + with self.assertRaisesRegex(RuntimeError, "synthetic start failure"): + coordinator.request(lambda conn, cancel: None) + self.assertTrue(coordinator.wait(0)) + self.assertEqual(coordinator.status, ("failed", "worker_start")) + self.assertTrue(coordinator.request(lambda conn, cancel: None)) + self.assertTrue(coordinator.wait(2)) + self.assertEqual(coordinator.status, ("success", None)) + + def test_start_failure_after_launch_cannot_release_active_worker_ownership(self) -> None: + coordinator = MAINTENANCE.get_coordinator(self.path) + entered = threading.Event() + release = threading.Event() + start = threading.Thread.start + + def work(conn: sqlite3.Connection, cancel: threading.Event) -> None: + entered.set() + release.wait(3) + + def launch_then_fail(worker: threading.Thread) -> None: + start(worker) + if not entered.wait(2): + raise AssertionError("Synthetic worker did not start") + raise RuntimeError("synthetic failure after launch") + + self.addCleanup(coordinator.wait, 4) + self.addCleanup(release.set) + with patch.object(MAINTENANCE.threading.Thread, "start", launch_then_fail): + with self.assertRaisesRegex(RuntimeError, "synthetic failure after launch"): + coordinator.request(work) + self.assertFalse(coordinator.wait(0)) + self.assertEqual(self.line(self.child()), "BUSY") + release.set() + self.assertTrue(coordinator.wait(2)) + self.assertEqual(coordinator.status, ("cancelled", None)) + + def test_binding_failure_after_start_releases_claim(self) -> None: + coordinator = MAINTENANCE.get_coordinator(self.path) + with patch.object(MAINTENANCE.uuid, "uuid4", side_effect=OSError("synthetic identity failure")): + self.assertTrue(coordinator.request(lambda conn, cancel: None)) + self.assertTrue(coordinator.wait(2)) + self.assertEqual(coordinator.status, ("failed", "OSError")) + self.assertEqual(self.line(self.child()), "READY") + + def test_open_failure_and_callback_exception_release_owner_without_error_text(self) -> None: + coordinator = MAINTENANCE.get_coordinator(self.path) + with patch.object(MAINTENANCE, "create_db", side_effect=sqlite3.OperationalError("synthetic private sentinel")): + self.assertTrue(coordinator.request(lambda conn, cancel: None)) + self.assertTrue(coordinator.wait(2)) + self.assertEqual(coordinator.status, ("failed", "OperationalError")) + + def fail(conn: sqlite3.Connection, cancel: threading.Event) -> None: + conn.execute("INSERT INTO messages(content) VALUES ('synthetic rollback')") + raise RuntimeError("synthetic private sentinel") + + self.assertTrue(coordinator.request(fail)) + self.assertTrue(coordinator.wait(2)) + self.assertEqual(coordinator.status, ("failed", "RuntimeError")) + self.assertEqual(self.conn.execute("SELECT count(*) FROM messages").fetchone(), (0,)) + self.assertEqual(self.line(self.child()), "READY") + + def test_unfinished_worker_transaction_is_rolled_back_and_reported_failed(self) -> None: + coordinator = MAINTENANCE.get_coordinator(self.path) + self.assertTrue(coordinator.request(lambda conn, cancel: conn.execute("INSERT INTO messages(content) VALUES ('synthetic uncommitted')"))) + self.assertTrue(coordinator.wait(2)) + self.assertEqual(coordinator.status, ("failed", "RuntimeError")) + self.assertEqual(self.conn.execute("SELECT count(*) FROM messages").fetchone(), (0,)) + + def test_in_memory_request_is_pending_without_thread_or_file(self) -> None: + coordinator = MAINTENANCE.get_coordinator(":memory:") + with patch.object(MAINTENANCE, "try_file_lock") as file_lock, patch.object(MAINTENANCE, "create_db") as create: + self.assertFalse(coordinator.request(lambda conn, cancel: self.fail("in-memory worker"))) + self.assertEqual(coordinator.status, ("pending_in_memory", None)) + self.assertTrue(coordinator.wait(0)) + file_lock.assert_not_called() + create.assert_not_called() + + def test_synchronous_ownership_nests_without_releasing_outer_claim(self) -> None: + with MAINTENANCE.maintenance_owner(self.path) as outer: + with MAINTENANCE.maintenance_owner(self.path) as inner: + self.assertIs(inner, outer) + self.assertEqual(self.line(self.child()), "BUSY") + self.assertFalse(MAINTENANCE.get_coordinator(self.path).request(lambda conn, cancel: None)) + self.assertEqual(self.line(self.child()), "READY") + + def test_worker_can_call_synchronous_builder_without_reacquiring_ownership(self) -> None: + coordinator = MAINTENANCE.get_coordinator(self.path) + tokens = [] + + def work(conn: sqlite3.Connection, cancel: threading.Event) -> None: + path = MAINTENANCE.connection_database_path(conn) + self.assertEqual(path, self.path.resolve()) + with MAINTENANCE.maintenance_owner(path) as first: + with MAINTENANCE.maintenance_owner(path) as second: + tokens.extend([first, second]) + self.assertEqual(self.line(self.child()), "BUSY") + + self.assertTrue(coordinator.request(work)) + self.assertTrue(coordinator.wait(3)) + self.assertEqual(coordinator.status, ("success", None)) + self.assertEqual(len(tokens), 2) + self.assertIs(tokens[0], tokens[1]) + + def test_sync_owner_excludes_other_threads_and_releases_after_exception(self) -> None: + errors = [] + + def contender() -> None: + try: + with MAINTENANCE.maintenance_owner(self.path): + errors.append("incorrectly acquired") + except MAINTENANCE.MaintenanceBusyError: + errors.append("busy") + + with self.assertRaisesRegex(ValueError, "synthetic failure"): + with MAINTENANCE.maintenance_owner(self.path): + worker = threading.Thread(target=contender) + worker.start() + worker.join(2) + self.assertEqual(errors, ["busy"]) + raise ValueError("synthetic failure") + with MAINTENANCE.maintenance_owner(self.path): + pass + + def test_in_memory_sync_owner_uses_original_connection_without_new_file(self) -> None: + conn = STORAGE.create_db(":memory:") + self.addCleanup(conn.close) + conn.execute("INSERT INTO messages(content) VALUES ('synthetic pending')") + with patch.object(MAINTENANCE, "try_file_lock") as file_lock: + path = MAINTENANCE.connection_database_path(conn) + self.assertIsNone(path) + with MAINTENANCE.maintenance_owner(path) as token: + self.assertIsNone(token.path) + self.assertTrue(conn.in_transaction) + file_lock.assert_not_called() + conn.rollback() + + @unittest.skipUnless(hasattr(os, "fork"), "Fork inheritance applies only on POSIX") + def test_fork_child_exiting_sync_context_does_not_close_reused_descriptor(self) -> None: + with MAINTENANCE.maintenance_owner(self.path): + owner_fd = next(iter(MAINTENANCE._held_fds)) + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message="This process .* is multi-threaded", category=DeprecationWarning) + pid = os.fork() + if pid == 0: + replacement = os.open(os.devnull, os.O_RDWR) + if replacement != owner_fd: + os.dup2(replacement, owner_fd) + os.close(replacement) + else: + _, status = os.waitpid(pid, 0) + if pid == 0: + try: + os.fstat(owner_fd) + except OSError: + os._exit(2) + os._exit(0) + self.assertEqual(status, 0) + + @unittest.skipUnless(hasattr(os, "fork"), "Fork inheritance applies only on POSIX") + def test_fork_child_does_not_retain_parent_owner_descriptor(self) -> None: + coordinator = MAINTENANCE.get_coordinator(self.path) + _, release, _ = self.blocked_work(coordinator) + read_fd, write_fd = os.pipe() + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message="This process .* is multi-threaded", category=DeprecationWarning) + pid = os.fork() + if pid == 0: + os.close(write_fd) + try: + try: + coordinator.status + except RuntimeError: + pass + else: + os._exit(2) + os.read(read_fd, 1) + finally: + os._exit(0) + os.close(read_fd) + try: + release.set() + self.assertTrue(coordinator.wait(2)) + self.assertEqual(self.line(self.child()), "READY") + finally: + os.write(write_fd, b"x") + os.close(write_fd) + _, status = os.waitpid(pid, 0) + self.assertEqual(status, 0) + + +def child_main(path: str) -> None: + coordinator = MAINTENANCE.get_coordinator(path) + + def work(conn: sqlite3.Connection, cancel: threading.Event) -> None: + print("READY", flush=True) + sys.stdin.readline() + + if not coordinator.request(work): + print("BUSY", flush=True) + return + coordinator.wait() + print("DONE", flush=True) + + +if __name__ == "__main__": + if len(sys.argv) == 3 and sys.argv[1] == "--synthetic-owner": + child_main(sys.argv[2]) + else: + unittest.main() diff --git a/tests/test-maintenance-publication-753.py b/tests/test-maintenance-publication-753.py new file mode 100644 index 00000000..2aa7c3d0 --- /dev/null +++ b/tests/test-maintenance-publication-753.py @@ -0,0 +1,452 @@ +"""Synthetic SQLite checks for composable summary and landmark publication.""" + +import ast +import json +import re +import sqlite3 +import tempfile +import types +import unittest +from pathlib import Path +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] + + +def load_modules() -> tuple[types.ModuleType, types.ModuleType, types.ModuleType]: + modules = [] + for name in ("storage", "consolidation", "temporal"): + module = types.ModuleType("synthetic_publication_" + name) + path = ROOT / "truememory" / (name + ".py") + tree = ast.parse(path.read_text(), filename=str(path)) + # Avoid package/native startup. All production computation and SQL run + # unchanged; these imported search helpers are unused by these builders. + tree.body = [node for node in tree.body if not ( + isinstance(node, ast.ImportFrom) + and node.module in {"truememory.storage", "truememory.fts_search"} + )] + exec(compile(tree, str(path), "exec"), module.__dict__) + modules.append(module) + return tuple(modules) + + +STORAGE, CONSOLIDATION, TEMPORAL = load_modules() + + +class PublicationFixture(unittest.TestCase): + def setUp(self) -> None: + directory = tempfile.TemporaryDirectory(prefix="synthetic-publication-") + self.addCleanup(directory.cleanup) + self.path = Path(directory.name) / "synthetic.sqlite" + self.conn = self.open_db() + + def open_db(self) -> sqlite3.Connection: + conn = STORAGE.create_db(self.path) + conn.execute("PRAGMA busy_timeout = 200") + self.addCleanup(conn.close) + return conn + + def seed(self) -> None: + self.conn.executemany( + "INSERT INTO messages(content,sender,recipient,timestamp) VALUES (?,?,?,?)", + [(f"synthetic note {i:02d} is recorded here", "synthetic-sender", "synthetic-recipient", + f"2026-01-{i + 1:02d}") for i in range(12)], + ) + self.conn.execute( + "INSERT INTO messages(content,sender,recipient,timestamp) VALUES " + "('synthetic team launched Cedar and moved to Birch', 'synthetic-sender', 'synthetic-recipient', '2026-02-01')" + ) + self.conn.execute( + "INSERT INTO summaries(period,summary) VALUES ('monthly','synthetic previous summary')" + ) + self.conn.execute( + "INSERT INTO landmark_events(event_name,timestamp) VALUES ('synthetic previous event','2025-01-01')" + ) + self.conn.commit() + + def snapshot(self, table: str, conn: sqlite3.Connection | None = None) -> list[tuple]: + return (conn or self.conn).execute(f"SELECT * FROM {table} ORDER BY id").fetchall() + + def landmark_compute_hook(self, action): + called = False + + def findall(pattern: str, content: str) -> list[str]: + nonlocal called + if not called: + called = True + action() + return re.findall(pattern, content) + + return patch.object(TEMPORAL, "re", types.SimpleNamespace( + compile=re.compile, IGNORECASE=re.IGNORECASE, findall=findall, + )) + + def builders(self) -> tuple[tuple[str, object, str], ...]: + return ( + ("summaries", CONSOLIDATION.build_summaries, "summaries"), + ("structured", CONSOLIDATION.build_structured_facts, "summaries"), + ("contradictions", CONSOLIDATION.detect_contradictions, "fact_timeline"), + ("landmarks", TEMPORAL.detect_landmark_events, "landmark_events"), + ) + + +class TestSummaryPublication(PublicationFixture): + def test_owned_periods_only_and_empty_source_clears_them(self) -> None: + for period in ("monthly", "entity_monthly", "structured_fact", "entity_profile", "synthetic_other", None): + self.conn.execute("INSERT INTO summaries(period,summary) VALUES (?, 'synthetic prior')", (period,)) + self.conn.commit() + preserved = self.conn.execute( + "SELECT * FROM summaries WHERE period NOT IN ('monthly','entity_monthly') OR period IS NULL ORDER BY id" + ).fetchall() + self.assertEqual(CONSOLIDATION.build_summaries(self.conn), 0) + self.assertEqual(self.snapshot("summaries"), preserved) + self.assertFalse(self.conn.in_transaction) + + def test_directive_only_input_is_successful_empty(self) -> None: + self.seed() + self.conn.execute("UPDATE messages SET directive = 1") + self.conn.commit() + self.assertEqual(CONSOLIDATION.build_summaries(self.conn), 0) + self.assertEqual(self.snapshot("summaries"), []) + + def test_selection_content_dates_and_other_producers_are_preserved(self) -> None: + self.seed() + self.conn.execute("INSERT INTO summaries(period,summary) VALUES ('structured_fact','synthetic fact')") + self.conn.commit() + preserved = self.conn.execute("SELECT * FROM summaries WHERE period = 'structured_fact'").fetchall() + self.assertEqual(CONSOLIDATION.build_summaries(self.conn), 3) + rows = self.conn.execute( + "SELECT period,start_date,end_date,entity,summary,key_facts,message_ids " + "FROM summaries WHERE period IN ('monthly','entity_monthly') ORDER BY period,start_date" + ).fetchall() + january = [f"synthetic note {i:02d} is recorded here" for i in range(12)] + self.assertEqual(rows[0][:6], ( + "entity_monthly", "2026-01-01", "2026-01-05", "synthetic-sender", + "\n".join(january[:5]), "[]", + )) + self.assertEqual(json.loads(rows[0][6]), [1, 2, 3, 4, 5]) + self.assertEqual(rows[1][:6], ( + "monthly", "2026-01-01", "2026-01-08", "", + "\n".join("[synthetic-sender] " + value for value in january[:8]), "[]", + )) + self.assertEqual(set(json.loads(rows[1][6])), set(range(1, 9))) + self.assertEqual(rows[2][:6], ( + "monthly", "2026-02-01", "2026-02-01", "", + "[synthetic-sender] synthetic team launched Cedar and moved to Birch", "[]", + )) + self.assertEqual(self.conn.execute("SELECT * FROM summaries WHERE period = 'structured_fact'").fetchall(), preserved) + + def test_summary_computation_does_not_hold_writer(self) -> None: + self.seed() + writer = self.open_db() + original = CONSOLIDATION._message_salience + observed = [] + + def score(message: dict) -> float: + if not observed: + self.assertFalse(self.conn.in_transaction) + writer.execute("INSERT INTO metadata(key,value) VALUES ('synthetic_writer','committed')") + writer.commit() + observed.append(True) + return original(message) + + with patch.object(CONSOLIDATION, "_message_salience", score): + CONSOLIDATION.build_summaries(self.conn) + self.assertEqual(observed, [True]) + + def test_summary_compute_failure_preserves_complete_previous_output(self) -> None: + self.seed() + previous = self.snapshot("summaries") + with patch.object(CONSOLIDATION, "_message_salience", side_effect=RuntimeError("synthetic computation failure")): + with self.assertRaises(RuntimeError): + CONSOLIDATION.build_summaries(self.conn) + self.assertEqual(self.snapshot("summaries"), previous) + self.assertFalse(self.conn.in_transaction) + + def test_outer_read_snapshot_rejects_concurrent_source_change(self) -> None: + self.seed() + writer = self.open_db() + previous = self.snapshot("summaries") + original = CONSOLIDATION._message_salience + changed = [] + self.conn.execute("BEGIN") + self.conn.execute("SELECT revision FROM maintenance_source_state").fetchone() + + def score(message: dict) -> float: + if not changed: + writer.execute("UPDATE messages SET content='synthetic correction' WHERE id=1") + writer.commit() + changed.append(True) + return original(message) + + with patch.object(CONSOLIDATION, "_message_salience", score): + with self.assertRaises(sqlite3.OperationalError): + CONSOLIDATION.build_summaries(self.conn) + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + self.assertEqual(changed, [True]) + self.assertEqual(self.snapshot("summaries"), previous) + + def test_summary_midpublication_failure_does_not_delete_other_producers(self) -> None: + self.seed() + self.conn.execute("INSERT INTO summaries(period,summary) VALUES ('structured_fact','synthetic preserved')") + self.conn.execute( + "CREATE TRIGGER synthetic_summary_failure BEFORE INSERT ON summaries " + "WHEN new.period = 'entity_monthly' BEGIN SELECT RAISE(ABORT,'synthetic insertion failure'); END" + ) + self.conn.commit() + previous = self.snapshot("summaries") + with self.assertRaises(sqlite3.IntegrityError): + CONSOLIDATION.build_summaries(self.conn) + self.conn.commit() + self.assertEqual(self.snapshot("summaries"), previous) + + +class TestLandmarkPublication(PublicationFixture): + def test_patterns_priority_context_entities_and_empty_publication(self) -> None: + self.seed() + self.assertEqual(TEMPORAL.detect_landmark_events(self.conn), 1) + row = self.conn.execute( + "SELECT event_name,timestamp,event_type,related_entities,source_message_id FROM landmark_events" + ).fetchone() + self.assertEqual(row, ( + "hetic team launched Cedar and moved to Birch", "2026-02-01", "move", + '["synthetic-sender", "synthetic-recipient", "Cedar", "Birch"]', 13, + )) + self.conn.execute("UPDATE messages SET timestamp = ''") + self.conn.commit() + self.assertEqual(TEMPORAL.detect_landmark_events(self.conn), 0) + self.assertEqual(self.snapshot("landmark_events"), []) + + def test_compute_failure_and_cancellation_preserve_previous_output(self) -> None: + self.seed() + previous = self.snapshot("landmark_events") + for failure in (RuntimeError("synthetic computation failure"), KeyboardInterrupt()): + with self.subTest(failure=type(failure).__name__): + def fail() -> None: + raise failure + with self.landmark_compute_hook(fail): + with self.assertRaises(type(failure)): + TEMPORAL.detect_landmark_events(self.conn) + self.conn.commit() + self.assertEqual(self.snapshot("landmark_events"), previous) + + def test_concurrent_source_changes_reject_output_without_blocking_writer(self) -> None: + self.seed() + writer = self.open_db() + mutations = ( + "UPDATE messages SET content='synthetic changed' WHERE id=13", + "UPDATE messages SET sender='synthetic-other' WHERE id=13", + "UPDATE messages SET recipient='synthetic-other' WHERE id=13", + "UPDATE messages SET timestamp='2026-03-01' WHERE id=13", + "UPDATE messages SET id=113 WHERE id=13", + "DELETE FROM messages WHERE id=13", + "INSERT INTO messages(content,timestamp) VALUES ('synthetic launched Elm','2026-03-01')", + ) + for sql in mutations: + with self.subTest(mutation=sql.split()[0:4]): + # Each variation uses a separate committed fixture generation. + self.conn.execute("DELETE FROM messages") + self.conn.execute("DELETE FROM summaries") + self.conn.execute("DELETE FROM landmark_events") + self.conn.execute("DELETE FROM sqlite_sequence WHERE name='messages'") + self.conn.commit() + self.seed() + previous = self.snapshot("landmark_events") + + def mutate() -> None: + self.assertFalse(self.conn.in_transaction) + writer.execute(sql) + writer.commit() + + with self.landmark_compute_hook(mutate): + with self.assertRaisesRegex(sqlite3.OperationalError, "source changed"): + TEMPORAL.detect_landmark_events(self.conn) + self.conn.commit() + self.assertEqual(self.snapshot("landmark_events"), previous) + + def test_unrelated_concurrent_write_does_not_reject_stable_source(self) -> None: + self.seed() + writer = self.open_db() + + def mutate() -> None: + writer.execute("INSERT INTO metadata(key,value) VALUES ('synthetic_writer','committed')") + writer.commit() + + with self.landmark_compute_hook(mutate): + self.assertEqual(TEMPORAL.detect_landmark_events(self.conn), 1) + + def test_caller_stale_snapshot_is_not_restarted_or_committed(self) -> None: + self.seed() + writer = self.open_db() + previous = self.snapshot("landmark_events") + self.conn.execute("BEGIN") + self.conn.execute("SELECT count(*) FROM messages").fetchone() + + def mutate() -> None: + writer.execute("UPDATE messages SET content='synthetic changed' WHERE id=13") + writer.commit() + + with self.landmark_compute_hook(mutate): + with self.assertRaises(sqlite3.OperationalError): + TEMPORAL.detect_landmark_events(self.conn) + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + self.assertEqual(self.snapshot("landmark_events"), previous) + + def test_midpublication_failure_rolls_back_even_if_caller_later_commits(self) -> None: + self.seed() + self.conn.execute("INSERT INTO messages(content,timestamp) VALUES ('synthetic launched Elm','2026-03-01')") + self.conn.execute( + "CREATE TRIGGER synthetic_landmark_failure BEFORE INSERT ON landmark_events " + "WHEN new.source_message_id=14 BEGIN SELECT RAISE(ABORT,'synthetic insertion failure'); END" + ) + self.conn.commit() + previous = self.snapshot("landmark_events") + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('synthetic_caller','retained')") + with self.assertRaises(sqlite3.IntegrityError): + TEMPORAL.detect_landmark_events(self.conn) + self.assertTrue(self.conn.in_transaction) + self.conn.commit() + self.assertEqual(self.snapshot("landmark_events"), previous) + self.assertEqual(self.conn.execute("SELECT value FROM metadata WHERE key='synthetic_caller'").fetchone(), ("retained",)) + + +class TestPublicationTransactions(PublicationFixture): + def test_readers_see_complete_old_generation_until_publication(self) -> None: + self.seed() + reader = self.open_db() + for table, builder in (("summaries", CONSOLIDATION.build_summaries), ("landmark_events", TEMPORAL.detect_landmark_events)): + with self.subTest(table=table): + previous = self.snapshot(table, reader) + observed = [] + + def observe() -> int: + observed.append(self.snapshot(table, reader)) + return 0 + + self.conn.create_function("synthetic_observe", 0, observe) + self.conn.execute( + f"CREATE TRIGGER synthetic_reader AFTER INSERT ON {table} " + "BEGIN SELECT synthetic_observe(); END" + ) + builder(self.conn) + self.assertTrue(observed) + self.assertTrue(all(rows == previous for rows in observed)) + self.assertNotEqual(self.snapshot(table, reader), previous) + self.conn.execute("DROP TRIGGER synthetic_reader") + + def test_every_builder_retains_caller_transaction_and_rollback(self) -> None: + self.seed() + for name, builder, table in self.builders(): + with self.subTest(builder=name): + previous = self.snapshot(table) + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('synthetic_caller','pending')") + builder(self.conn) + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + self.assertEqual(self.snapshot(table), previous) + self.assertIsNone(self.conn.execute("SELECT value FROM metadata WHERE key='synthetic_caller'").fetchone()) + + def test_final_release_failure_rolls_back_standalone_and_caller_owned(self) -> None: + self.seed() + for name, builder, table in self.builders(): + for caller_owned in (False, True): + with self.subTest(builder=name, caller_owned=caller_owned): + self.conn.rollback() + self.conn.execute("DELETE FROM metadata WHERE key='synthetic_caller'") + self.conn.commit() + previous = self.snapshot(table) + if caller_owned: + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('synthetic_caller','retained')") + denied = [] + + def authorizer(action: int, operation: str | None, *unused: str | None) -> int: + if action == sqlite3.SQLITE_SAVEPOINT and operation == "RELEASE" and not denied: + denied.append(True) + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + + self.conn.set_authorizer(authorizer) + try: + with self.assertRaises(sqlite3.DatabaseError): + builder(self.conn) + finally: + self.conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.assertEqual(denied, [True]) + self.assertEqual(self.conn.in_transaction, caller_owned) + self.conn.commit() + self.assertEqual(self.snapshot(table), previous) + if caller_owned: + self.assertEqual(self.conn.execute("SELECT value FROM metadata WHERE key='synthetic_caller'").fetchone(), ("retained",)) + self.conn.execute("DELETE FROM metadata WHERE key='synthetic_caller'") + self.conn.commit() + + def test_actual_outermost_commit_constraint_failure_restores_output(self) -> None: + self.seed() + self.conn.execute("CREATE TABLE synthetic_parent(id INTEGER PRIMARY KEY)") + self.conn.execute( + "CREATE TABLE synthetic_child(parent_id INTEGER REFERENCES synthetic_parent(id) DEFERRABLE INITIALLY DEFERRED)" + ) + for table, builder in (("summaries", CONSOLIDATION.build_summaries), ("landmark_events", TEMPORAL.detect_landmark_events)): + with self.subTest(table=table): + self.conn.rollback() + self.conn.execute("DROP TRIGGER IF EXISTS synthetic_commit_failure") + self.conn.execute( + f"CREATE TRIGGER synthetic_commit_failure AFTER INSERT ON {table} " + "BEGIN INSERT INTO synthetic_child VALUES (1); END" + ) + previous = self.snapshot(table) + with self.assertRaises(sqlite3.IntegrityError): + builder(self.conn) + self.assertFalse(self.conn.in_transaction) + self.assertEqual(self.snapshot(table), previous) + self.assertEqual(self.conn.execute("SELECT * FROM synthetic_child").fetchall(), []) + self.conn.execute("DROP TRIGGER synthetic_commit_failure") + + def test_failed_rollback_does_not_release_and_commit_failed_output(self) -> None: + self.seed() + for table, builder in (("summaries", CONSOLIDATION.build_summaries), ("landmark_events", TEMPORAL.detect_landmark_events)): + with self.subTest(table=table): + self.conn.rollback() + self.conn.execute("DROP TRIGGER IF EXISTS synthetic_write_failure") + self.conn.execute( + f"CREATE TRIGGER synthetic_write_failure BEFORE INSERT ON {table} " + "BEGIN SELECT RAISE(ABORT,'synthetic write failure'); END" + ) + previous = self.snapshot(table) + observed = [] + + def authorizer(action: int, operation: str | None, *unused: str | None) -> int: + if action == sqlite3.SQLITE_SAVEPOINT: + observed.append(operation) + if operation == "ROLLBACK": + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + + self.conn.set_authorizer(authorizer) + try: + with self.assertRaises(sqlite3.DatabaseError): + builder(self.conn) + finally: + self.conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.assertTrue(self.conn.in_transaction) + self.assertNotIn("RELEASE", observed) + self.conn.rollback() + self.assertEqual(self.snapshot(table), previous) + self.conn.execute("DROP TRIGGER synthetic_write_failure") + + def test_cancellation_inside_shared_savepoint_restores_previous_output(self) -> None: + self.seed() + previous = self.snapshot("summaries") + with self.assertRaises(KeyboardInterrupt): + with CONSOLIDATION._consolidation_write(self.conn, "synthetic_cancel"): + self.conn.execute("DELETE FROM summaries") + raise KeyboardInterrupt() + self.conn.commit() + self.assertEqual(self.snapshot("summaries"), previous) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-maintenance-runner-753.py b/tests/test-maintenance-runner-753.py new file mode 100644 index 00000000..2477d8cf --- /dev/null +++ b/tests/test-maintenance-runner-753.py @@ -0,0 +1,589 @@ +"""Durable maintenance scheduling and publication on synthetic SQLite only.""" + +import ast +import runpy +import sqlite3 +import tempfile +import threading +import types +import unittest +from pathlib import Path +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] +_loader = runpy.run_path(str(Path(__file__).with_name("test-maintenance-source-revision-753.py"))) +STORAGE, MAINTENANCE = _loader["STORAGE"], _loader["MAINTENANCE"] + + +class RunnerFixture(unittest.TestCase): + def setUp(self) -> None: + directory = tempfile.TemporaryDirectory(prefix="synthetic-runner-") + self.addCleanup(directory.cleanup) + self.path = Path(directory.name) / "synthetic.sqlite" + self.conn = self.open_db() + self.conn.execute("CREATE TABLE synthetic_output(layer TEXT, value INTEGER)") + self.calls = [] + self.dependency = MAINTENANCE.make_layer_dependency(1, {"algorithm": "synthetic"}) + + def open_db(self) -> sqlite3.Connection: + conn = STORAGE.create_db(self.path) + conn.execute("PRAGMA busy_timeout=200") + self.addCleanup(conn.close) + return conn + + def add(self, count: int, conn: sqlite3.Connection | None = None) -> None: + conn = conn or self.conn + conn.executemany("INSERT INTO messages(content,timestamp) VALUES ('synthetic source','2026-01-01')", + [()] * count) + conn.commit() + + def spec(self, layer: str = "summaries", action=None): + def build(conn: sqlite3.Connection) -> None: + self.calls.append(layer) + conn.execute("DELETE FROM synthetic_output WHERE layer=?", (layer,)) + count = conn.execute("SELECT count(*) FROM messages").fetchone()[0] + if count: + conn.execute("INSERT INTO synthetic_output VALUES (?,?)", (layer, count)) + if action is not None: + action(conn) + + def output_count(conn: sqlite3.Connection) -> int: + return conn.execute("SELECT count(*) FROM synthetic_output WHERE layer=?", (layer,)).fetchone()[0] + + return MAINTENANCE.LayerSpec(layer, "synthetic_" + layer, lambda: self.dependency, build, output_count) + + def state(self, spec=None, conn: sqlite3.Connection | None = None): + spec = spec or self.spec() + return MAINTENANCE.read_layer_states(conn or self.conn, (spec,))[spec.layer] + + def run_layer(self, spec=None, **kwargs): + return MAINTENANCE.run_layers(self.conn, (spec or self.spec(),), **kwargs)[0] + + def output(self, conn: sqlite3.Connection | None = None) -> list[tuple]: + return (conn or self.conn).execute("SELECT * FROM synthetic_output ORDER BY layer").fetchall() + + +class TestRunnerEligibility(RunnerFixture): + def test_initial_empty_success_is_durable_and_skipped_after_reopen(self) -> None: + result = self.run_layer() + self.assertEqual((result.outcome, result.output_count), ("success_empty", 0)) + reopened = self.open_db() + self.assertEqual(MAINTENANCE.plan_layers(reopened, (self.spec(),)), ()) + self.assertEqual(MAINTENANCE.layer_freshness(self.state().source, self.state(), self.dependency), "current") + self.assertEqual(self.run_layer().attempted, False) + self.assertEqual(self.calls, ["summaries"]) + + def test_historical_zero_insert_count_gets_first_attempt(self) -> None: + self.add(3) + self.conn.execute("UPDATE maintenance_source_state SET revision=0,insert_count=0,nonappend_revision=0") + self.conn.commit() + self.assertEqual(self.run_layer().outcome, "success") + self.assertEqual(self.output(), [("summaries", 3)]) + self.assertEqual(self.state().attempted_insert_count, 0) + + def test_threshold_is_24_then_25_across_connections(self) -> None: + self.run_layer() + for _ in range(24): + self.add(1, self.open_db()) + self.assertEqual(MAINTENANCE.plan_layers(self.open_db(), (self.spec(),)), ()) + self.add(1, self.open_db()) + self.assertEqual(len(MAINTENANCE.plan_layers(self.open_db(), (self.spec(),))), 1) + self.assertEqual(self.run_layer().outcome, "success") + self.assertEqual(self.state().attempted_insert_count, 25) + self.add(24) + self.assertFalse(self.run_layer().attempted) + self.assertEqual(self.calls, ["summaries", "summaries"]) + + def test_rollback_does_not_advance_threshold(self) -> None: + self.run_layer() + self.conn.executemany("INSERT INTO messages(content) VALUES ('synthetic rolled back')", [()] * 25) + self.conn.rollback() + self.assertEqual(MAINTENANCE.plan_layers(self.conn, (self.spec(),)), ()) + self.assertEqual(self.state().source.insert_count, 0) + + def test_update_delete_and_replacement_wake_below_threshold(self) -> None: + self.add(1) + self.run_layer() + for sql in ( + "UPDATE messages SET content='synthetic correction' WHERE id=1", + "INSERT OR REPLACE INTO messages(id,content) VALUES (1,'synthetic replacement')", + "DELETE FROM messages WHERE id=1", + ): + with self.subTest(operation=sql.split()[0]): + self.conn.execute("PRAGMA recursive_triggers=OFF") + self.conn.execute(sql) + self.conn.commit() + self.assertTrue(self.run_layer().attempted) + self.assertEqual(self.state().source.correction_count, 2) + + def test_untrusted_existing_success_and_abandoned_state_attempt_once(self) -> None: + self.add(1) + self.run_layer() + self.conn.execute("UPDATE maintenance_layers SET full_rebuild_required=1 WHERE layer='summaries'") + self.conn.commit() + self.assertTrue(self.run_layer().attempted) + self.conn.execute("UPDATE maintenance_layers SET outcome='running',run_generation='synthetic-dead-owner' WHERE layer='summaries'") + self.conn.commit() + self.assertEqual(len(MAINTENANCE.plan_layers(self.conn, (self.spec(),))), 1) + self.assertTrue(self.run_layer().attempted) + self.assertFalse(self.run_layer().attempted) + + def test_failure_is_suppressed_for_exact_input_and_manual_force_retries(self) -> None: + def fail(conn: sqlite3.Connection) -> None: + raise ValueError("synthetic source-bearing error") + + spec = self.spec(action=fail) + result = self.run_layer(spec) + self.assertEqual((result.outcome, result.error_category), ("failed", "ValueError")) + self.assertFalse(self.run_layer(spec).attempted) + self.assertTrue(self.run_layer(spec, force=True).attempted) + self.assertEqual(self.calls, ["summaries", "summaries"]) + self.assertNotIn("source-bearing", str(self.state(spec))) + + def _assert_initial_terminal_attempt_respects_threshold(self, unavailable: bool) -> None: + def fail(conn: sqlite3.Connection) -> None: + raise ValueError("synthetic failure") + + if unavailable: + self.dependency = MAINTENANCE.make_layer_dependency(1, available=False, error_category="ImportError") + spec = self.spec(action=fail) + outcome = "unavailable" if unavailable else "failed" + self.assertEqual(self.run_layer(spec).outcome, outcome) + self.assertIsNone(self.state(spec).successful_epoch) + self.assertTrue(self.state(spec).full_rebuild_required) + for added, total in ((1, 1), (23, 24)): + with self.subTest(insert_count=total): + self.add(added, self.open_db()) + self.assertEqual(MAINTENANCE.plan_layers(self.open_db(), (spec,)), ()) + self.assertFalse(self.run_layer(spec).attempted) + self.assertEqual(self.state(spec).attempted_insert_count, 0) + self.add(1, self.open_db()) + self.assertEqual(len(MAINTENANCE.plan_layers(self.open_db(), (spec,))), 1) + self.assertTrue(self.run_layer(spec).attempted) + self.assertEqual(self.state(spec).attempted_insert_count, 25) + self.assertFalse(self.run_layer(spec).attempted) + + self.conn.execute("UPDATE messages SET content='synthetic correction' WHERE id=1") + self.conn.commit() + self.assertTrue(self.run_layer(spec).attempted) + self.assertFalse(self.run_layer(spec).attempted) + self.dependency = MAINTENANCE.make_layer_dependency( + 2, available=not unavailable, error_category="ImportError" if unavailable else None, + ) + self.assertTrue(self.run_layer(spec).attempted) + self.assertFalse(self.run_layer(spec).attempted) + self.assertTrue(self.run_layer(spec, force=True).attempted) + self.assertEqual(self.calls, [] if unavailable else ["summaries"] * 5) + + def test_initial_failure_waits_for_25_appends_but_corrections_dependency_and_force_wake(self) -> None: + self._assert_initial_terminal_attempt_respects_threshold(unavailable=False) + + def test_initial_unavailable_waits_for_25_appends_but_corrections_dependency_and_force_wake(self) -> None: + self._assert_initial_terminal_attempt_respects_threshold(unavailable=True) + + def test_unavailable_attempt_uses_captured_source_not_later_current(self) -> None: + self.dependency = MAINTENANCE.make_layer_dependency(1, available=False, error_category="ImportError") + writer = self.open_db() + original = MAINTENANCE._record_attempt + captured = [] + + def record(conn, state, *args): + captured.append(state.source) + self.add(1, writer) + return original(conn, state, *args) + + with patch.object(MAINTENANCE, "_record_attempt", record): + self.assertEqual(self.run_layer().outcome, "unavailable") + state = self.state() + self.assertEqual((state.attempted_revision, state.attempted_insert_count), (0, 0)) + self.assertEqual(state.source.revision, 1) + self.assertEqual(captured[0].revision, 0) + self.assertEqual(self.calls, []) + + def test_unchanged_unavailable_is_suppressed_and_restored_dependency_retries(self) -> None: + self.dependency = MAINTENANCE.make_layer_dependency(1, available=False, error_category="ImportError") + self.assertEqual(self.run_layer().outcome, "unavailable") + self.assertFalse(self.run_layer().attempted) + self.dependency = MAINTENANCE.make_layer_dependency(1) + self.assertEqual(self.run_layer().outcome, "success_empty") + self.assertEqual(self.calls, ["summaries"]) + + def test_new_source_epoch_invalidates_old_success_without_new_inserts(self) -> None: + self.add(1) + self.run_layer() + old = self.state() + self.conn.execute("DROP TRIGGER messages_maintenance_au") + STORAGE._initialize_maintenance_tracking(self.conn) + self.assertNotEqual(self.state().source.epoch, old.source.epoch) + self.assertEqual(self.state().source.insert_count, 0) + self.assertTrue(self.run_layer().attempted) + + def test_dependency_change_retries_and_failed_new_version_preserves_old_success(self) -> None: + self.add(1) + self.run_layer() + old = self.state() + self.dependency = MAINTENANCE.make_layer_dependency(2, {"algorithm": "synthetic"}) + + def fail(conn: sqlite3.Connection) -> None: + raise ValueError("synthetic failure") + + self.assertEqual(self.run_layer(self.spec(action=fail)).outcome, "failed") + state = self.state() + self.assertEqual(state.successful_dependency, old.successful_dependency) + self.assertEqual(state.attempted_dependency, self.dependency.key) + self.assertEqual(MAINTENANCE.layer_freshness(state.source, state, self.dependency), "dependency_pending") + self.assertFalse(self.run_layer().attempted) + + def test_forced_failure_does_not_hide_a_still_current_previous_success(self) -> None: + self.add(1) + self.run_layer() + previous = self.output() + + def fail(conn: sqlite3.Connection) -> None: + raise ValueError("synthetic failure") + + self.run_layer(self.spec(action=fail), force=True) + state = self.state() + self.assertEqual(state.outcome, "failed") + self.assertEqual(MAINTENANCE.layer_freshness(state.source, state, self.dependency), "current") + self.assertEqual(self.output(), previous) + + def test_failure_does_not_rerun_or_erase_successful_sibling(self) -> None: + self.add(1) + good = self.spec() + + def fail(conn: sqlite3.Connection) -> None: + raise ValueError("synthetic failure") + + bad = self.spec("landmarks", fail) + result = MAINTENANCE.run_layers(self.conn, (good, bad)) + self.assertEqual([item.outcome for item in result], ["success", "failed"]) + self.assertEqual(self.output(), [("summaries", 1)]) + self.assertTrue(all(not item.attempted for item in MAINTENANCE.run_layers(self.open_db(), (good, bad)))) + + +class TestRunnerTransactions(RunnerFixture): + def test_running_diagnostic_is_committed_without_writer_during_compute(self) -> None: + self.add(1) + writer = self.open_db() + spec = self.spec() + observed = [] + + def build(conn: sqlite3.Connection) -> None: + observed.append(writer.execute("SELECT outcome FROM maintenance_layers WHERE layer='summaries'").fetchone()[0]) + writer.execute("INSERT INTO metadata(key,value) VALUES ('synthetic_writer','committed')") + writer.commit() + spec.build(conn) + + result = self.run_layer(spec._replace(build=build)) + self.assertEqual(observed, ["running"]) + self.assertEqual(result.outcome, "failed") # Stale WAL snapshot cannot publish. + self.assertEqual(self.output(), []) + self.assertIsNone(self.state().successful_revision) + + def test_changed_source_is_pending_and_cannot_publish_stale_output(self) -> None: + self.add(1) + self.run_layer() + writer = self.open_db() + spec = self.spec() + + def build(conn: sqlite3.Connection) -> None: + writer.execute("UPDATE messages SET content='synthetic correction' WHERE id=1") + writer.commit() + spec.build(conn) + + result = self.run_layer(spec._replace(build=build), force=True) + state = self.state() + self.assertEqual(result.outcome, "failed") + self.assertEqual(state.attempted_revision, 1) + self.assertEqual(state.source.revision, 2) + self.assertEqual(state.successful_revision, 1) + self.assertEqual(len(MAINTENANCE.plan_layers(self.conn, (spec,))), 1) + + def test_dependency_change_after_output_write_rolls_back_output_and_checkpoint(self) -> None: + self.add(1) + self.run_layer() + old = self.state() + previous = self.output() + + def change(conn: sqlite3.Connection) -> None: + self.dependency = MAINTENANCE.make_layer_dependency(2) + + self.assertEqual(self.run_layer(self.spec(action=change), force=True).outcome, "failed") + self.assertEqual(self.output(), previous) + self.assertEqual(self.state().successful_dependency, old.successful_dependency) + + def test_dependency_is_rechecked_after_checkpoint_before_commit(self) -> None: + self.add(1) + original = MAINTENANCE.record_layer_success_in_transaction + + def publish(*args, **kwargs) -> None: + original(*args, **kwargs) + self.dependency = MAINTENANCE.make_layer_dependency(2) + + with patch.object(MAINTENANCE, "record_layer_success_in_transaction", publish): + self.assertEqual(self.run_layer().outcome, "failed") + self.assertEqual(self.output(), []) + self.assertIsNone(self.state().successful_revision) + + def test_successful_output_and_provenance_become_visible_together(self) -> None: + self.add(1) + self.run_layer() + self.add(1) + reader = self.open_db() + observed = [] + original = MAINTENANCE.record_layer_success_in_transaction + + def publish(*args, **kwargs) -> None: + original(*args, **kwargs) + observed.append((self.output(reader), self.state(conn=reader).successful_revision)) + + with patch.object(MAINTENANCE, "record_layer_success_in_transaction", publish): + self.run_layer(force=True) + self.assertEqual(observed, [([("summaries", 1)], 1)]) + self.assertEqual((self.output(reader), self.state(conn=reader).successful_revision), ([("summaries", 2)], 2)) + + def test_output_commit_failure_preserves_previous_success(self) -> None: + self.add(1) + self.run_layer() + old = self.state() + self.add(1) + denied = [] + + def authorizer(action: int, operation: str | None, *unused: str | None) -> int: + if action == sqlite3.SQLITE_TRANSACTION and operation == "COMMIT" and not denied: + denied.append(True) + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + + try: + # Install after the running diagnostic commits; this also + # invalidates SQLite's cached prepared COMMIT statement. + result = self.run_layer(self.spec(action=lambda conn: conn.set_authorizer(authorizer)), force=True) + finally: + self.conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.assertEqual(result.outcome, "failed") + self.assertEqual(denied, [True]) + self.assertEqual(self.output(), [("summaries", 1)]) + self.assertEqual(self.state().successful_revision, old.successful_revision) + self.assertFalse(self.conn.in_transaction) + + def test_checkpoint_write_failure_cannot_publish_output(self) -> None: + self.add(1) + self.conn.execute( + "CREATE TRIGGER synthetic_checkpoint_failure BEFORE UPDATE OF successful_revision ON maintenance_layers " + "BEGIN SELECT RAISE(ABORT,'synthetic checkpoint failure'); END" + ) + self.assertEqual(self.run_layer().outcome, "failed") + self.assertEqual(self.output(), []) + self.assertIsNone(self.state().successful_revision) + + def test_change_after_commit_remains_pending_without_an_immediate_loop(self) -> None: + self.add(1) + writer = self.open_db() + actual = self.conn + commits = [] + + class AfterCommit: + def __getattr__(self, name: str) -> object: + return getattr(actual, name) + + def execute(self, sql: str, parameters: tuple = ()): + result = actual.execute(sql, parameters) + if sql == "COMMIT": + commits.append(True) + if len(commits) == 2: + writer.execute("INSERT INTO messages(content) VALUES ('synthetic next generation')") + writer.commit() + return result + + result = MAINTENANCE.run_layers(AfterCommit(), (self.spec(),)) + self.assertEqual(result[0].outcome, "success") + self.assertEqual(self.calls, ["summaries"]) + state = self.state() + self.assertEqual((state.successful_revision, state.source.revision), (1, 2)) + self.assertEqual(MAINTENANCE.layer_freshness(state.source, state, self.dependency), "append_pending") + + def test_failure_status_commit_failure_is_surfaced(self) -> None: + denied = [] + + def authorizer(action: int, operation: str | None, *unused: str | None) -> int: + if action == sqlite3.SQLITE_TRANSACTION and operation == "COMMIT" and not denied: + denied.append(True) + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + + def fail(conn: sqlite3.Connection) -> None: + conn.set_authorizer(authorizer) + raise ValueError("synthetic failure") + + try: + with self.assertRaises(sqlite3.DatabaseError): + self.run_layer(self.spec(action=fail)) + finally: + self.conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.assertEqual(self.state().outcome, "running") + self.assertEqual(denied, [True]) + self.assertFalse(self.conn.in_transaction) + self.assertTrue(self.run_layer().attempted) + + def test_cancellation_rolls_back_and_leaves_retryable_abandoned_state(self) -> None: + self.add(1) + cancel = threading.Event() + result = self.run_layer(self.spec(action=lambda conn: cancel.set()), cancel=cancel) + self.assertEqual(result.outcome, "abandoned") + self.assertEqual(self.output(), []) + self.assertIsNone(self.state().successful_revision) + self.assertTrue(self.run_layer().attempted) + + def test_keyboard_interrupt_rolls_back_and_propagates(self) -> None: + self.add(1) + + def interrupt(conn: sqlite3.Connection) -> None: + raise KeyboardInterrupt() + + with self.assertRaises(KeyboardInterrupt): + self.run_layer(self.spec(action=interrupt)) + self.assertEqual(self.state().outcome, "abandoned") + self.assertEqual(self.output(), []) + + def test_active_caller_transaction_is_rejected_without_committing_it(self) -> None: + self.conn.execute("INSERT INTO messages(content) VALUES ('synthetic pending')") + with self.assertRaises(RuntimeError): + self.run_layer() + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + self.assertEqual(self.state().source.insert_count, 0) + + def test_nested_worker_owner_is_reused_and_other_thread_reports_busy(self) -> None: + errors = [] + + def contender() -> None: + try: + MAINTENANCE.run_layers(self.open_db(), (self.spec(),)) + except MAINTENANCE.MaintenanceBusyError: + errors.append("busy") + + with MAINTENANCE.maintenance_owner(self.path): + self.assertTrue(self.run_layer().attempted) + thread = threading.Thread(target=contender) + thread.start() + thread.join(3) + self.assertFalse(thread.is_alive()) + self.assertEqual(errors, ["busy"]) + + +class TestSharedCheckpointAndReaders(RunnerFixture): + def test_checkpoint_requires_transaction_and_does_not_commit_caller(self) -> None: + source = MAINTENANCE.read_source_revision(self.conn) + arguments = dict(layer="entity_profiles", dependency=self.dependency, source=source, output_count=0) + with self.assertRaises(RuntimeError): + MAINTENANCE.record_layer_success_in_transaction(self.conn, **arguments) + self.conn.execute("BEGIN") + MAINTENANCE.record_layer_success_in_transaction(self.conn, **arguments) + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.state(self.spec("entity_profiles")).successful_revision, 0) + self.conn.rollback() + self.assertIsNone(self.state(self.spec("entity_profiles")).successful_revision) + + def test_checkpoint_rejects_a_stale_captured_source(self) -> None: + source = MAINTENANCE.read_source_revision(self.conn) + self.add(1) + self.conn.execute("BEGIN") + with self.assertRaises(sqlite3.OperationalError): + MAINTENANCE.record_layer_success_in_transaction(self.conn, layer="summaries", + dependency=self.dependency, source=source, output_count=0) + self.conn.rollback() + self.assertIsNone(self.state().successful_revision) + + def test_reader_context_keeps_row_and_freshness_in_one_snapshot(self) -> None: + self.add(1) + self.run_layer() + writer = self.open_db() + with MAINTENANCE.layer_read_snapshot(self.conn, "summaries", self.dependency) as state: + self.add(1, writer) + MAINTENANCE.run_layers(writer, (self.spec(),), force=True) + self.assertEqual(MAINTENANCE.layer_freshness(state.source, state, self.dependency), "current") + self.assertEqual(self.output(), [("summaries", 1)]) + self.assertFalse(self.conn.in_transaction) + self.assertEqual(self.output(), [("summaries", 2)]) + + def test_read_context_preserves_caller_transaction(self) -> None: + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('synthetic_pending','retained')") + with MAINTENANCE.layer_read_snapshot(self.conn, "summaries", self.dependency): + self.assertTrue(self.conn.in_transaction) + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + self.assertIsNone(self.conn.execute("SELECT value FROM metadata WHERE key='synthetic_pending'").fetchone()) + + def test_failed_reader_releases_only_its_owned_snapshot(self) -> None: + with self.assertRaises(ValueError): + with MAINTENANCE.layer_read_snapshot(self.conn, "summaries", self.dependency): + raise ValueError("synthetic reader failure") + self.assertFalse(self.conn.in_transaction) + + def test_correction_pending_is_distinct_from_append_pending(self) -> None: + self.add(1) + self.run_layer() + self.add(1) + state = self.state() + self.assertEqual(MAINTENANCE.layer_freshness(state.source, state, self.dependency), "append_pending") + self.conn.execute("UPDATE messages SET sender='synthetic-other' WHERE id=1") + self.conn.commit() + state = self.state() + self.assertEqual(MAINTENANCE.layer_freshness(state.source, state, self.dependency), "correction_pending") + + +class TestRunnerMigrationAndAdapters(RunnerFixture): + def test_additive_migration_preserves_epoch_counters_and_existing_provenance(self) -> None: + self.add(1) + self.run_layer() + token = MAINTENANCE.read_source_revision(self.conn) + success = self.state().successful_dependency + self.conn.execute("ALTER TABLE maintenance_layers RENAME TO synthetic_previous_layers") + self.conn.execute( + "CREATE TABLE maintenance_layers AS SELECT layer,builder_version,outcome,successful_epoch," + "successful_revision,successful_dependency,attempted_epoch,attempted_revision,attempted_dependency," + "full_rebuild_required,output_count,run_generation,error_category FROM synthetic_previous_layers" + ) + self.conn.execute("CREATE UNIQUE INDEX synthetic_layer_key ON maintenance_layers(layer)") + self.conn.commit() + STORAGE._initialize_maintenance_tracking(self.conn) + self.assertEqual(MAINTENANCE.read_source_revision(self.conn), token) + self.assertEqual(self.state().successful_dependency, success) + self.assertIsNone(self.state().attempted_insert_count) + schema_version = self.conn.execute("PRAGMA schema_version").fetchone()[0] + STORAGE._initialize_maintenance_tracking(self.conn) + self.assertEqual(self.conn.execute("PRAGMA schema_version").fetchone()[0], schema_version) + + def test_six_real_nonvector_builders_compose_without_native_imports(self) -> None: + modules = {} + for name in ("consolidation", "predictive", "temporal"): + module = types.ModuleType("synthetic_runner_" + name) + path = ROOT / "truememory" / (name + ".py") + tree = ast.parse(path.read_text()) + tree.body = [node for node in tree.body if not ( + isinstance(node, ast.ImportFrom) and node.module in {"truememory.storage", "truememory.fts_search"} + )] + exec(compile(tree, str(path), "exec"), module.__dict__) + modules["truememory." + name] = module + self.conn.executemany( + "INSERT INTO messages(content,sender,recipient,timestamp) VALUES (?,?,?,?)", + [("synthetic team launched Cedar", "synthetic-sender", "synthetic-recipient", "2026-01-01"), + ("synthetic team switched from Cedar to Birch", "synthetic-sender", "synthetic-recipient", "2026-01-02")], + ) + self.conn.commit() + boundary = types.SimpleNamespace(import_module=lambda name: modules[name]) + with patch.object(MAINTENANCE, "importlib", boundary): + specs = MAINTENANCE.nonvector_layer_specs() + result = MAINTENANCE.run_layers(self.conn, specs) + self.assertEqual(len(result), 6) + self.assertTrue(all(item.outcome in {"success", "success_empty"} for item in result), result) + self.assertTrue(all(not item.attempted for item in MAINTENANCE.run_layers(self.open_db(), specs))) + self.assertFalse(self.conn.in_transaction) + self.assertEqual(self.conn.execute("SELECT outcome FROM maintenance_layers WHERE layer='clusters'").fetchone(), ("pending",)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-maintenance-source-revision-753.py b/tests/test-maintenance-source-revision-753.py new file mode 100644 index 00000000..d15e89cf --- /dev/null +++ b/tests/test-maintenance-source-revision-753.py @@ -0,0 +1,342 @@ +"""Transactional maintenance revisions on synthetic SQLite databases only.""" + +import builtins +import sys +import sqlite3 +import tempfile +import types +import unittest +from pathlib import Path +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] + + +def load_primitives() -> tuple[types.ModuleType, types.ModuleType]: + modules = {} + tier_switch = types.ModuleType("synthetic_maintenance_tier_switch") + + def unavailable_model(*args: object, **kwargs: object) -> object: + raise AssertionError("Maintenance fixtures must not construct or encode models") + + vector = types.SimpleNamespace( + EMBEDDING_MODEL="synthetic-model", _embedding_dim=2, _model=None, + _frozen_embedding_target=None, _runtime_policy_tier=None, + resolve_tier=lambda: "edge", _active_vec_table=lambda conn: "vec_messages", + _active_sep_table=lambda conn: "vec_messages_sep", get_model=unavailable_model, + ) + reranker = types.SimpleNamespace( + get_current_reranker_name=lambda: "synthetic/reranker", get_reranker=unavailable_model, + ) + + def safe_import(name: str, globals: dict | None = None, locals: dict | None = None, + fromlist: tuple[str, ...] = (), level: int = 0) -> object: + if name == "truememory": + return types.SimpleNamespace(vector_search=modules.get("vector_search", vector), + reranker=modules.get("reranker", reranker)) + if name == "truememory.tier_switch": + return tier_switch + if name.startswith("truememory."): + short = name.removeprefix("truememory.") + if short in modules: + return modules[short] + raise AssertionError("Unexpected application import: " + name) + if name == "psutil": + return types.SimpleNamespace() + if name.split(".")[0] in {"torch", "numpy", "sqlite_vec", "hdbscan", "model2vec", "sentence_transformers"}: + raise AssertionError("Unexpected native import: " + name) + return builtins.__import__(name, globals, locals, fromlist, level) + + for name in ("storage", "_platform", "maintenance", "embedding_target", "tier_config", + "tier_switch.cache", "rebuild_source", "tier_switch.job", "tier_switch.source", + "tier_switch.activation", "tier_switch.serving", "tier_switch.runtime", "tier_switch.writer"): + module = types.ModuleType("synthetic_maintenance_" + name.replace(".", "_")) + module.__dict__["__builtins__"] = dict(vars(builtins), __import__=safe_import) + path = ROOT / "truememory" / (name.replace(".", "/") + ".py") + # Dataclasses inspect only their own explicitly named synthetic module. + with patch.dict(sys.modules, {module.__name__: module}): + exec(compile(path.read_text(encoding="utf-8"), str(path), "exec"), module.__dict__) + modules[name] = module + if name.startswith("tier_switch."): + setattr(tier_switch, name.rsplit(".", 1)[1], module) + # Keep model identity injection local to this fixture graph, never sys.modules. + modules["vector_search"], modules["reranker"] = vector, reranker + modules["tier_switch.runtime"].sys = types.SimpleNamespace( + modules={"truememory." + name: module for name, module in modules.items()}) + modules["maintenance"]._fixture_modules = modules + modules["tier_switch.runtime"]._fixture_modules = modules + return modules["storage"], modules["maintenance"] + + +STORAGE, MAINTENANCE = load_primitives() + + +class TestSourceRevision(unittest.TestCase): + def setUp(self) -> None: + self.temp = tempfile.TemporaryDirectory(prefix="synthetic-revision-") + self.addCleanup(self.temp.cleanup) + self.path = Path(self.temp.name) / "synthetic.sqlite" + self.conn = STORAGE.create_db(self.path) + self.addCleanup(self.conn.close) + + def token(self, conn: sqlite3.Connection | None = None): + return MAINTENANCE.read_source_revision(conn or self.conn) + + def insert(self, mid: int, timestamp: str = "2026-01-01") -> None: + self.conn.execute("INSERT INTO messages(id, content, timestamp) VALUES (?, 'synthetic source', ?)", (mid, timestamp)) + + def test_insert_update_delete_are_transactional_and_visible_only_after_commit(self) -> None: + reader = STORAGE.create_db(self.path) + self.addCleanup(reader.close) + initial = self.token() + self.insert(1) + self.assertEqual(self.token().revision, 1) + self.assertEqual(self.token(reader), initial) + self.conn.rollback() + self.assertEqual(self.token(), initial) + self.insert(1) + self.conn.commit() + inserted = self.token(reader) + self.assertEqual((inserted.revision, inserted.insert_count, inserted.correction_count), (1, 1, 0)) + self.conn.execute("UPDATE messages SET content = 'synthetic changed' WHERE id = 1") + self.conn.commit() + self.assertEqual(self.token().correction_count, 1) + self.conn.execute("DELETE FROM messages WHERE id = 1") + self.conn.rollback() + self.assertEqual(self.token().revision, 2) + self.conn.execute("DELETE FROM messages WHERE id = 1") + self.conn.commit() + token = self.token(reader) + self.assertEqual((token.revision, token.insert_count, token.correction_count, token.nonappend_revision), (3, 1, 2, 3)) + self.assertEqual(token.max_seen_message_id, 1) + + def test_every_source_field_and_directive_change_invalidates_append_proof(self) -> None: + self.insert(1) + self.conn.commit() + fields = { + "content": "synthetic changed", "sender": "synthetic-sender", "recipient": "synthetic-recipient", + "timestamp": "2026-02-01", "category": "synthetic-category", "modality": "synthetic-modality", + "directive": 1, "metadata": '{"synthetic": true}', + } + for field, value in fields.items(): + with self.subTest(field=field): + before = self.token() + self.conn.execute(f"UPDATE messages SET {field} = ? WHERE id = 1", (value,)) + self.conn.commit() + after = self.token() + self.assertEqual(after.revision, before.revision + 1) + self.assertEqual(after.nonappend_revision, after.revision) + self.assertFalse(after.is_append_only_since(before)) + + def test_noop_null_and_derived_only_updates_do_not_self_dirty(self) -> None: + self.insert(1) + self.conn.commit() + before = self.token() + self.conn.execute("UPDATE messages SET content = content, id = id, directive = directive, metadata = metadata") + self.conn.execute("UPDATE messages SET episode_id = 42, emotional_valence = 0.25, embedding_separation = X'0102'") + self.conn.commit() + self.assertEqual(self.token(), before) + self.conn.execute("UPDATE messages SET sender = NULL") + self.conn.commit() + changed = self.token() + self.assertEqual(changed.revision, before.revision + 1) + self.conn.execute("UPDATE messages SET sender = NULL") + self.conn.commit() + self.assertEqual(self.token(), changed) + self.conn.execute("UPDATE messages SET sender = ''") + self.conn.commit() + self.assertEqual(self.token().revision, changed.revision + 1) + + def test_access_only_extension_columns_do_not_schedule_source_work(self) -> None: + self.insert(1) + self.conn.commit() + before = self.token() + self.conn.execute("ALTER TABLE messages ADD COLUMN access_count INTEGER DEFAULT 0") + self.conn.execute("ALTER TABLE messages ADD COLUMN last_accessed TEXT") + self.conn.execute("UPDATE messages SET access_count = 1, last_accessed = '2026-01-01'") + self.conn.commit() + self.assertEqual(self.token(), before) + + def test_rowid_alias_changes_are_single_corrections_and_advance_historical_max(self) -> None: + self.insert(1) + self.conn.commit() + for new_id, alias in enumerate(("id", "rowid", "_rowid_", "oid"), 101): + with self.subTest(alias=alias): + before = self.token() + self.conn.execute(f"UPDATE messages SET {alias} = ?", (new_id,)) + self.conn.commit() + after = self.token() + self.assertEqual(after.revision, before.revision + 1) + self.assertEqual(after.max_seen_message_id, new_id) + self.assertFalse(after.is_append_only_since(before)) + + def test_append_certificate_rejects_id_holes_deletes_and_rolled_back_tokens(self) -> None: + self.insert(10) + self.conn.commit() + before = self.token() + self.insert(20) + self.conn.commit() + appended = self.token() + self.assertTrue(appended.is_append_only_since(before)) + self.insert(15) + self.assertFalse(self.token().is_append_only_since(appended)) + uncommitted = self.token() + self.conn.rollback() + self.assertEqual(self.token(), appended) + self.assertFalse(self.token().is_append_only_since(uncommitted)) + self.insert(15) + self.conn.commit() + inserted_hole = self.token() + self.assertEqual(inserted_hole.max_seen_message_id, 20) + self.assertEqual(inserted_hole.nonappend_revision, inserted_hole.revision) + self.conn.execute("DELETE FROM messages WHERE id = 20") + self.conn.commit() + self.assertEqual(self.token().max_seen_message_id, 20) + self.insert(19) + self.conn.commit() + self.assertFalse(self.token().is_append_only_since(inserted_hole)) + + def test_backdated_and_tied_timestamps_do_not_change_id_append_semantics(self) -> None: + self.insert(1, "2026-02-01") + self.conn.commit() + before = self.token() + self.insert(2, "2026-01-01") + self.insert(3, "2026-02-01") + self.conn.commit() + self.assertTrue(self.token().is_append_only_since(before)) + self.conn.execute("UPDATE messages SET timestamp = '2026-03-01' WHERE id = 1") + self.conn.commit() + self.assertFalse(self.token().is_append_only_since(before)) + + def test_insert_replace_invalidates_even_without_recursive_delete_triggers(self) -> None: + self.insert(1) + self.conn.commit() + before = self.token() + self.conn.execute("PRAGMA recursive_triggers = OFF") + self.conn.execute("INSERT OR REPLACE INTO messages(id, content) VALUES (1, 'synthetic replacement')") + self.conn.commit() + self.assertGreater(self.token().revision, before.revision) + self.assertFalse(self.token().is_append_only_since(before)) + + def test_bulk_delete_and_insert_do_not_lose_mutation_counts(self) -> None: + for mid in (1, 2, 3): + self.insert(mid) + self.conn.commit() + self.conn.execute("DELETE FROM messages") + self.insert(4) + self.insert(5) + self.conn.commit() + token = self.token() + self.assertEqual((token.revision, token.insert_count, token.correction_count), (8, 5, 3)) + self.assertEqual(token.max_seen_message_id, 5) + + def test_read_helper_preserves_caller_snapshot_and_transaction(self) -> None: + writer = STORAGE.create_db(self.path) + self.addCleanup(writer.close) + self.conn.execute("BEGIN") + old = self.token() + writer.execute("INSERT INTO messages(content) VALUES ('synthetic new')") + writer.commit() + self.assertEqual(self.token(), old) + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + self.assertEqual(self.token().revision, old.revision + 1) + + def test_reopen_is_idempotent_and_empty_outputs_are_not_marked_success(self) -> None: + self.insert(7) + self.conn.commit() + before = self.token() + schema_version = self.conn.execute("PRAGMA schema_version").fetchone()[0] + reopened = STORAGE.create_db(self.path) + self.addCleanup(reopened.close) + self.assertEqual(self.token(reopened), before) + self.assertEqual(reopened.execute("PRAGMA schema_version").fetchone()[0], schema_version) + rows = reopened.execute("SELECT layer, outcome, successful_revision, full_rebuild_required FROM maintenance_layers").fetchall() + self.assertEqual(len(rows), 8) + self.assertTrue(all(row[1:] == ("pending", None, 1) for row in rows)) + + def test_missing_trigger_bootstraps_new_epoch_instead_of_certifying_untracked_history(self) -> None: + self.insert(10) + self.conn.commit() + before = self.token() + self.conn.execute("DROP TRIGGER messages_maintenance_au") + self.conn.execute("UPDATE messages SET content = 'synthetic untracked'") + self.conn.commit() + reopened = STORAGE.create_db(self.path) + self.addCleanup(reopened.close) + after = self.token(reopened) + self.assertNotEqual(after.epoch, before.epoch) + self.assertEqual(after.max_seen_message_id, 10) + self.assertFalse(after.is_append_only_since(before)) + + def test_missing_layer_registration_is_repaired_without_resetting_source_epoch(self) -> None: + self.insert(1) + self.conn.commit() + before = self.token() + self.conn.execute("DELETE FROM maintenance_layers WHERE layer = 'clusters'") + self.conn.commit() + STORAGE._initialize_maintenance_tracking(self.conn) + self.assertEqual(self.token(), before) + self.assertEqual(self.conn.execute("SELECT outcome, full_rebuild_required FROM maintenance_layers WHERE layer = 'clusters'").fetchone(), ("pending", 1)) + + def test_unmigrated_connection_cannot_issue_token(self) -> None: + conn = sqlite3.connect(":memory:") + self.addCleanup(conn.close) + with self.assertRaises(MAINTENANCE.MaintenanceUnavailableError): + MAINTENANCE.read_source_revision(conn) + + def test_existing_rows_bootstrap_without_fabricated_historical_event_counts(self) -> None: + path = Path(self.temp.name) / "synthetic-existing.sqlite" + original = sqlite3.connect(path) + original.executescript(STORAGE._SCHEMA_SQL) + original.execute("INSERT INTO messages(id, content) VALUES (91, 'synthetic historical')") + original.commit() + original.close() + reopened = STORAGE.create_db(path) + self.addCleanup(reopened.close) + token = self.token(reopened) + self.assertEqual((token.revision, token.insert_count, token.correction_count, token.max_seen_message_id), (0, 0, 0, 91)) + + def test_failed_trigger_installation_rolls_back_source_state_and_recovers(self) -> None: + self.conn.execute("DROP TRIGGER messages_maintenance_au") + self.conn.commit() + before = self.token() + + def deny_create(action: int, name: str, *unused: str | None) -> int: + return sqlite3.SQLITE_DENY if action == sqlite3.SQLITE_CREATE_TRIGGER and name == "messages_maintenance_au" else sqlite3.SQLITE_OK + + self.conn.set_authorizer(deny_create) + try: + with self.assertRaises(sqlite3.DatabaseError): + STORAGE._initialize_maintenance_tracking(self.conn) + finally: + self.conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.assertFalse(self.conn.in_transaction) + self.assertEqual(self.token(), before) + STORAGE._initialize_maintenance_tracking(self.conn) + self.assertNotEqual(self.token().epoch, before.epoch) + + def test_incomplete_legacy_schema_stays_open_but_cannot_issue_revision_token(self) -> None: + path = Path(self.temp.name) / "synthetic-incomplete.sqlite" + conn = sqlite3.connect(path) + conn.executescript(STORAGE._SCHEMA_SQL.replace(" directive INTEGER DEFAULT 0,\n", "")) + conn.close() + with patch.object(STORAGE, "_migrate_messages_schema", return_value=None), self.assertLogs(STORAGE.log, level="WARNING"): + incomplete = STORAGE.create_db(path) + self.addCleanup(incomplete.close) + incomplete.execute("INSERT INTO messages(content) VALUES ('synthetic legacy')") + incomplete.commit() + with self.assertRaises(MAINTENANCE.MaintenanceUnavailableError): + self.token(incomplete) + incomplete.execute("ALTER TABLE messages ADD COLUMN directive INTEGER DEFAULT 0") + incomplete.commit() + STORAGE._initialize_maintenance_tracking(incomplete) + self.assertEqual(self.token(incomplete).max_seen_message_id, 1) + incomplete.execute("UPDATE messages SET directive = 1") + incomplete.commit() + self.assertEqual(self.token(incomplete).correction_count, 1) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-model-admission-737.py b/tests/test-model-admission-737.py new file mode 100644 index 00000000..81fd0a5e --- /dev/null +++ b/tests/test-model-admission-737.py @@ -0,0 +1,503 @@ +"""Synthetic transport/admission regressions; no package or model imports.""" +from __future__ import annotations + +import ast +import json +import os +import socket +import struct +import sys +import threading +import time +import types +import unittest +from pathlib import Path +from unittest.mock import Mock, patch + + +SOURCE = Path(__file__).resolve().parents[1] / "truememory" + + +class SyntheticArray(list): + """Keep transport fixtures JSON-readable while exposing real array shape.""" + @property + def shape(self) -> tuple: + return (len(self), len(self[0])) if self and isinstance(self[0], list) else (len(self),) + + @property + def ndim(self) -> int: + return len(self.shape) + + +def load_source(name: str) -> types.ModuleType: + tree = ast.parse((SOURCE / (name + ".py")).read_text(encoding="utf-8")) + tree.body = [node for node in tree.body if not ( + isinstance(node, ast.Import) and any(alias.name in ("numpy", "psutil") for alias in node.names) + or isinstance(node, ast.ImportFrom) and (node.module or "").startswith("truememory.") + or isinstance(node, ast.Try) and any( + isinstance(child, ast.Import) and any(alias.name == "psutil" for alias in child.names) + for child in node.body + ) + or isinstance(node, ast.Expr) and isinstance(node.value, ast.Call) + and isinstance(node.value.func, ast.Name) and node.value.func.id == "_set_mps_memory_cap" + )] + module = types.ModuleType("synthetic_" + name) + module.__dict__.update({ + "np": types.SimpleNamespace(ndarray=SyntheticArray, float32="float32", + asarray=lambda value, **kwargs: SyntheticArray(value)), + "psutil": None, "_USE_UNIX": True, "_LOOPBACK_HOST": "127.0.0.1", + "_env_int": lambda name, default, **kwargs: default, + "pid_is_alive": lambda pid: False, "spawn_kwargs": lambda: {}, + }) + sys.modules[module.__name__] = module + exec(compile(ast.fix_missing_locations(tree), str(SOURCE / (name + ".py")), "exec"), module.__dict__) + return module + + +def frame(payload: bytes) -> bytes: + return struct.pack(">I", len(payload)) + payload + + +class TestModelAdmission(unittest.TestCase): + def setUp(self) -> None: + self.server_module = load_source("model_server") + self.client_module = load_source("model_client") + with patch.dict(os.environ, {"TRUEMEMORY_MODEL_SERVER_MAX_HANDLERS": "2"}): + self.server = self.server_module.ModelServer() + self.server._REJECT_TIMEOUT = 0.02 + self.clients: list[socket.socket] = [] + self.releases: list[threading.Event] = [] + self.futures = [] + self.server._get_embed_model = Mock(side_effect=AssertionError("Unexpected model load")) + self.server._get_reranker = Mock(side_effect=AssertionError("Unexpected model load")) + + def tearDown(self) -> None: + for event in self.releases: + event.set() + self.server._stop_clients() + self.server._workers.shutdown(wait=True, cancel_futures=True) + for client in self.clients: + client.close() + self.assertEqual(len(self.server._clients), 0) + self.assertEqual(self.server._reserved_request_bytes, 0) + for module in (self.server_module, self.client_module): + sys.modules.pop(module.__name__, None) + + def pair(self) -> tuple[socket.socket, socket.socket]: + client, server = socket.socketpair() + client.settimeout(2) + self.clients.append(client) + return client, server + + def send(self, payload: bytes, advertised: int | None = None) -> tuple[socket.socket, object]: + client, server = self.pair() + client.sendall(frame(payload) if advertised is None else struct.pack(">I", advertised) + payload) + future = self.server._dispatch_client(server) + if future is not None: + self.futures.append(future) + return client, future + + def response(self, client: socket.socket) -> dict: + header = self.server._recv_exact(client, 4, time.monotonic() + 2) + self.assertIsNotNone(header) + body = self.server._recv_exact(client, struct.unpack(">I", header)[0], time.monotonic() + 2) + return json.loads(body) + + def blocked_handler(self) -> tuple[threading.Event, threading.Event]: + entered, release = threading.Event(), threading.Event() + self.releases.append(release) + + def handle(request: dict) -> dict: + entered.set() + if not release.wait(2): + raise AssertionError("Synthetic request was not released") + return {"ok": True} + + self.server.handle_request = handle + return entered, release + + def assert_empty(self) -> None: + self.assertEqual(len(self.server._clients), 0) + self.assertEqual(self.server._reserved_request_bytes, 0) + + def test_strict_configuration_and_documented_default_arithmetic(self) -> None: + parse = self.server_module._admission_setting + for invalid in ("", "0", "-1", "1.5", "nan", "inf", " 2", "+2", "129", "٢"): + with self.subTest(invalid=invalid), patch.dict(os.environ, {"SYNTHETIC_LIMIT": invalid}): + with self.assertRaisesRegex(ValueError, "SYNTHETIC_LIMIT"): + parse("SYNTHETIC_LIMIT", 16, 1, 128) + with patch.dict(os.environ, {}, clear=True): + self.assertEqual(parse("SYNTHETIC_LIMIT", 16, 1, 128), 16) + self.assertEqual(self.server._max_request_bytes, 33554432) + self.assertLessEqual(3 * self.server_module._MAX_MESSAGE_SIZE, 33554432) + self.assertGreater(4 * self.server_module._MAX_MESSAGE_SIZE, 33554432) + + def test_handler_capacity_rejects_before_body_decode_or_submission(self) -> None: + entered, release = self.blocked_handler() + payload = b'{"op":"ping"}' + first, first_future = self.send(payload) + self.assertTrue(entered.wait(1)) + second, second_future = self.send(payload) + with patch.object(self.server._workers, "submit", side_effect=AssertionError("Unbounded submit")): + third, rejected = self.send(b"", advertised=len(payload)) + self.assertIsNone(rejected) + response = self.response(third) + self.assertEqual(response["error_code"], "server_busy") + self.assertEqual(response["retry_after_ms"], 250) + self.assertEqual(len(self.server._clients), 2) + self.assertEqual(self.server._reserved_request_bytes, 2 * len(payload)) + self.assertLessEqual(len(self.server._workers._threads), 2) + release.set() + for client, future in ((first, first_future), (second, second_future)): + self.assertTrue(self.response(client)["ok"]) + future.result(2) + self.assert_empty() + + def test_byte_credit_is_reserved_from_header_and_held_through_inference(self) -> None: + self.server._max_request_bytes = 20 + client, future = self.send(b"", advertised=20) + self.assertEqual(self.server._reserved_request_bytes, 20) + with patch.object(self.server_module.json, "loads", side_effect=AssertionError("Rejected body decoded")): + rejected, rejected_future = self.send(b"", advertised=1) + self.assertIsNone(rejected_future) + self.assertEqual(self.response(rejected)["error_code"], "server_busy") + client.shutdown(socket.SHUT_WR) + future.result(2) + self.assert_empty() + entered, release = self.blocked_handler() + client, future = self.send(b'{"op":"ping"}') + self.assertTrue(entered.wait(1)) + self.assertEqual(self.server._reserved_request_bytes, 13) + release.set() + self.response(client) + future.result(2) + self.assert_empty() + + def test_malformed_empty_oversized_and_truncated_frames_return_credits(self) -> None: + for payload in (b"{", b"[]", b"null", b"\xff"): + with self.subTest(payload=payload): + client, future = self.send(payload) + self.assertFalse(self.response(client)["ok"]) + future.result(2) + self.assert_empty() + for length in (0, self.server_module._MAX_MESSAGE_SIZE + 1): + client, future = self.send(b"", advertised=length) + self.assertIsNone(future) + self.assertEqual(client.recv(1), b"") + self.assert_empty() + client, future = self.send(b"{", advertised=20) + client.shutdown(socket.SHUT_WR) + future.result(2) + self.assert_empty() + + def test_authentication_stays_before_application_responses(self) -> None: + self.server._token = b"x" * 32 + with patch.object(self.server_module, "_USE_UNIX", False): + client, peer = self.pair() + client.sendall(b"y" * 32) + self.assertIsNone(self.server._dispatch_client(peer)) + self.assertEqual(client.recv(1), b"") + client, peer = self.pair() + client.sendall(b"x" * 32 + frame(b'{"op":"ping"}')) + future = self.server._dispatch_client(peer) + self.assertTrue(self.response(client)["ok"]) + future.result(2) + self.assert_empty() + + def test_header_and_body_timeouts_are_absolute_even_for_slow_drips(self) -> None: + class DrippingSocket: + def __init__(self) -> None: + self.timeouts: list[float] = [] + self.reads = 0 + + def settimeout(self, seconds: float) -> None: + self.timeouts.append(seconds) + + def recv(self, count: int) -> bytes: + self.reads += 1 + return b"x" + + drip = DrippingSocket() + with patch.object(self.server_module.time, "monotonic", side_effect=[0.0, 0.4, 0.8, 1.2]): + with self.assertRaises(TimeoutError): + self.server._recv_exact(drip, 4, 1.0) + self.assertEqual(drip.reads, 3) + self.assertAlmostEqual(drip.timeouts[-1], 0.1) + self.server._frame_timeout = 0.02 + client, future = self.send(b"", advertised=20) + self.assertIn("timed out", self.response(client)["error"]) + future.result(2) + self.assert_empty() + self.server._header_timeout = 0.02 + client, peer = self.pair() + client.sendall(b"\x00") + self.assertIsNone(self.server._dispatch_client(peer)) + self.assertEqual(client.recv(1), b"") + + def test_receive_observes_shutdown_without_a_socket_wakeup(self) -> None: + clock = [0.0] + server = self.server + + class NonWakingSocket: + timeout = 30.0 + + def settimeout(self, seconds: float) -> None: + self.timeout = seconds + + def recv(self, count: int) -> bytes: + clock[0] += self.timeout + # Model a platform whose blocking read only returns when its + # own timeout expires, even after another thread stops service. + server._stopped.set() + raise TimeoutError("timed out") + + with patch.object(self.server_module.time, "monotonic", side_effect=lambda: clock[0]): + self.assertIsNone(server._recv_exact(NonWakingSocket(), 20, 30.0)) + self.assertLessEqual(clock[0], 0.1) + + def test_receive_poll_timeouts_preserve_partial_data_and_absolute_expiry(self) -> None: + clock = [0.0] + + class DelayedSocket: + timeout = 30.0 + chunks = iter((b"a", None, b"b")) + + def settimeout(self, seconds: float) -> None: + self.timeout = seconds + + def recv(self, count: int) -> bytes: + chunk = next(self.chunks, None) + if chunk is not None: + return chunk + clock[0] += self.timeout + raise TimeoutError("timed out") + + peer = DelayedSocket() + with patch.object(self.server_module.time, "monotonic", side_effect=lambda: clock[0]): + self.assertEqual(self.server._recv_exact(peer, 2, 0.35), b"ab") + with self.assertRaisesRegex(TimeoutError, "deadline exceeded"): + self.server._recv_exact(peer, 1, 0.35) + self.assertAlmostEqual(clock[0], 0.35) + + def test_total_frame_budget_includes_header_time_and_caps_header_budget(self) -> None: + for header_timeout, frame_timeout, expected_header in ((10, 30, 110), (30, 10, 110)): + with self.subTest(header_timeout=header_timeout, frame_timeout=frame_timeout): + self.server._header_timeout = header_timeout + self.server._frame_timeout = frame_timeout + client, peer = self.pair() + with patch.object(self.server_module.time, "monotonic", side_effect=[100, 107]), \ + patch.object(self.server, "_recv_exact", return_value=struct.pack(">I", 13)) as receive: + admitted = self.server._admit_client(peer) + self.assertIsNotNone(admitted) + self.assertEqual(receive.call_args.args, (peer, 4, expected_header)) + self.assertEqual(admitted.frame_expires_at, 100 + frame_timeout) + self.server._release_client(peer) + self.assert_empty() + + def test_valid_caller_deadline_replaces_only_the_legacy_queue_ceiling(self) -> None: + admitted = self.server_module._TransportRequest(13, time.monotonic() - 1, self.server._stopped) + deadline = self.server_module._RequestDeadline(time.monotonic() + 300, admitted) + self.assertGreater(deadline.remaining(), 120) + with self.assertRaises(TimeoutError): + self.server_module._RequestDeadline(None, admitted).check() + with self.assertRaises(TimeoutError): + self.server_module._RequestDeadline(time.monotonic() - 1, admitted).check() + + def test_expired_caller_and_legacy_queue_wait_never_load_models(self) -> None: + self.server._inference_lock.acquire() + try: + for request in ( + {"op": "rerank", "pairs": [["synthetic", "document"]], "deadline": time.time() - 1}, + {"op": "rerank", "pairs": [["synthetic", "document"]], + "_transport_context": {"started": True}, "queue_expires_at": None}, + ): + self.server._queue_timeout = 0.03 + client, future = self.send(json.dumps(request).encode()) + self.assertIn("deadline", self.response(client)["error"]) + future.result(2) + self.server._get_reranker.assert_not_called() + self.assertTrue(self.server._inference_lock.locked()) + self.assert_empty() + finally: + self.server._inference_lock.release() + + def test_active_native_work_outlives_legacy_queue_ceiling_and_keeps_credit(self) -> None: + entered, release = threading.Event(), threading.Event() + self.releases.append(release) + + def predict(pairs: list, **kwargs: object) -> list: + entered.set() + self.assertTrue(release.wait(2)) + return [0.5] * len(pairs) + + self.server._get_reranker = lambda name, deadline=None: types.SimpleNamespace(predict=predict) + self.server_module._store_batch_result = lambda result, values, *args: values + self.server._queue_timeout = 0.05 + payload = b'{"op":"rerank","pairs":[["synthetic","document"]]}' + client, future = self.send(payload) + self.assertTrue(entered.wait(1)) + admitted = next(iter(self.server._clients.values())) + self.assertTrue(admitted.started) + admitted.queue_expires_at = time.monotonic() - 1 + self.assertEqual(self.server._reserved_request_bytes, len(payload)) + release.set() + self.assertTrue(self.response(client)["ok"]) + future.result(2) + self.assert_empty() + + def test_shutdown_wakes_queued_work_but_retains_active_native_credit(self) -> None: + entered, release = self.blocked_handler() + client, future = self.send(b'{"op":"ping"}') + self.assertTrue(entered.wait(1)) + self.server._stop_clients() + self.assertEqual(len(self.server._clients), 1) + self.assertEqual(self.server._reserved_request_bytes, 13) + self.assertFalse(future.done()) + release.set() + future.result(2) + self.assert_empty() + + def test_shutdown_cancels_lock_wait_and_incomplete_body(self) -> None: + waiting = threading.Event() + original_lock = self.server._inference_lock + + class ObservedLock: + def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: + if timeout >= 0: + waiting.set() + return original_lock.acquire(blocking, timeout) + + def release(self) -> None: + original_lock.release() + + self.server._inference_lock = ObservedLock() + self.server._inference_lock.acquire() + try: + queued, queued_future = self.send(b'{"op":"rerank","pairs":[]}') + self.assertTrue(waiting.wait(1), "Request did not reach the owned inference lock") + partial, partial_future = self.send(b"", advertised=20) + self.server._stop_clients() + for future in (queued_future, partial_future): + if not future.cancelled(): + future.result(2) + self.assert_empty() + self.server._get_reranker.assert_not_called() + finally: + self.server._inference_lock.release() + + def test_enqueued_work_is_withdrawn_when_executor_thread_start_fails(self) -> None: + self.server.handle_request = Mock(side_effect=AssertionError("Orphan request executed")) + with patch.object(self.server._workers, "_adjust_thread_count", side_effect=RuntimeError("synthetic start failure")): + with self.assertRaisesRegex(RuntimeError, "synthetic start failure"): + self.send(b'{"op":"ping"}') + self.assertTrue(self.server._stopped.is_set()) + self.assert_empty() + self.server.handle_request.assert_not_called() + client, future = self.send(b'{"op":"ping"}') + self.assertIsNone(future) + self.assert_empty() + + def test_submission_failure_retains_credit_if_existing_worker_claimed_work(self) -> None: + client, future = self.send(b'{"op":"ping"}') + self.response(client) + future.result(2) + entered, release = self.blocked_handler() + + def fail_after_claim() -> None: + self.assertTrue(entered.wait(1)) + raise RuntimeError("synthetic failure after worker claim") + + with patch.object(self.server._workers, "_adjust_thread_count", side_effect=fail_after_claim): + with self.assertRaisesRegex(RuntimeError, "after worker claim"): + self.send(b'{"op":"ping"}') + self.assertTrue(self.server._stopped.is_set()) + self.assertEqual(len(self.server._clients), 1) + self.assertEqual(self.server._reserved_request_bytes, 13) + release.set() + self.server._workers.shutdown(wait=True, cancel_futures=True) + self.assert_empty() + + def test_send_and_inference_errors_return_all_credits(self) -> None: + for failure in (ValueError("synthetic inference failure"), TypeError("synthetic serialization failure")): + self.server.handle_request = Mock(side_effect=failure) + client, future = self.send(b'{"op":"ping"}') + self.assertFalse(self.response(client)["ok"]) + future.result(2) + self.assert_empty() + self.server.handle_request = lambda request: {"ok": True, "value": object()} + client, future = self.send(b'{"op":"ping"}') + self.assertFalse(self.response(client)["ok"]) + future.result(2) + self.assert_empty() + self.server.handle_request = lambda request: {"ok": True} + with patch.object(self.server, "_send_response", side_effect=BrokenPipeError): + client, future = self.send(b'{"op":"ping"}') + future.result(2) + self.assert_empty() + + def test_new_client_busy_is_explicit_without_autostart(self) -> None: + self.server._max_request_bytes = 1 + for timeout in (None, 1): + with self.subTest(timeout=timeout): + client, peer = self.pair() + worker = threading.Thread(target=self.server.handle_client, args=(peer,)) + worker.start() + try: + with patch.object(self.client_module, "_connect", return_value=client), \ + patch.object(self.client_module, "_start_server", side_effect=AssertionError("Busy restarted server")): + with self.assertRaises(self.client_module.ModelServerBusyError) as caught: + self.client_module._request_with_autostart({"op": "ping"}, timeout=timeout) + self.assertEqual(caught.exception.retry_after_ms, 250) + finally: + worker.join(2) + self.assertFalse(worker.is_alive()) + self.assert_empty() + + def test_client_recovers_busy_frame_after_early_broken_pipe(self) -> None: + payload = json.dumps({"ok": False, "error_code": "server_busy", "protocol": 1}).encode() + + class EarlyRejection: + def __init__(self) -> None: + self.data = frame(payload) + + def settimeout(self, timeout: float) -> None: + self.timeout = timeout + + def sendall(self, data: bytes) -> None: + raise BrokenPipeError("synthetic early rejection") + + def recv(self, count: int) -> bytes: + result, self.data = self.data[:count], self.data[count:] + return result + + def close(self) -> None: + pass + + with patch.object(self.client_module, "_connect", return_value=EarlyRejection()), \ + patch.object(self.client_module, "_start_server", side_effect=AssertionError("Busy restarted server")): + with self.assertRaises(self.client_module.ModelServerBusyError): + self.client_module._request_with_autostart({"op": "ping"}, timeout=1) + + def test_admitted_fast_query_still_runs_while_main_ownership_is_busy(self) -> None: + self.server._inference_lock.acquire() + self.server._get_fast_encoder = lambda tier, deadline=None: types.SimpleNamespace(encode=lambda texts, **kwargs: [[0.25]]) + try: + client, future = self.send(b'{"op":"embed","texts":["synthetic"]}') + self.assertEqual(self.response(client)["vectors"], [[0.25]]) + future.result(2) + self.server._get_embed_model.assert_not_called() + finally: + self.server._inference_lock.release() + + def test_half_closed_valid_client_receives_legacy_protocol_response(self) -> None: + client, peer = self.pair() + client.sendall(frame(b'{"op":"ping"}')) + client.shutdown(socket.SHUT_WR) + future = self.server._dispatch_client(peer) + self.assertEqual(self.response(client), {"ok": True, "protocol": 1}) + future.result(2) + self.assert_empty() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-model-allocation-preflight-297.py b/tests/test-model-allocation-preflight-297.py new file mode 100644 index 00000000..a5a7ff38 --- /dev/null +++ b/tests/test-model-allocation-preflight-297.py @@ -0,0 +1,486 @@ +"""Exact wire arithmetic and recovery lifetimes, without native dependencies.""" +from __future__ import annotations + +import ast +import base64 +import json +import math +import os +import struct +import sys +import threading +import types +import unittest +import weakref +from pathlib import Path +from unittest.mock import Mock, patch + + +SOURCE = Path(__file__).resolve().parents[1] / "truememory" / "model_server.py" + + +class Array: + def __init__(self, shape: tuple, data: list | None = None, dtype: str = "float32") -> None: + self.shape = shape + self.ndim = len(shape) + self.data = data + self.dtype = dtype + + def __setitem__(self, index: slice, batch: Array) -> None: + stride = math.prod(self.shape[1:]) + self.data[index.start * stride:index.stop * stride] = batch.data + + def tobytes(self) -> bytes: + if self.data is None: + raise AssertionError("Shape-only output must not be materialized") + return struct.pack("=" + "f" * len(self.data), *self.data) + + +class Numpy: + ndarray = Array + float32 = "float32" + + def __init__(self) -> None: + self.allocations: list[tuple] = [] + self.conversions = 0 + + def asarray(self, values: object, dtype: str = "float32") -> Array: + self.conversions += 1 + if isinstance(values, Array): + return values if values.dtype == dtype else Array(values.shape, values.data, dtype) + + def unpack(value: object) -> tuple[tuple, list]: + if not isinstance(value, (list, tuple)): + return (), [float(value)] + if not value: + return (0,), [] + children = [unpack(child) for child in value] + if any(shape != children[0][0] for shape, _ in children): + raise ValueError("Ragged synthetic array") + return (len(value), *children[0][0]), [item for _, data in children for item in data] + + shape, data = unpack(values) + return Array(shape, data, dtype) + + def empty(self, shape: tuple, dtype: str) -> Array: + self.allocations.append(shape) + if math.prod(shape) > 4096: + raise AssertionError("Unexpected large full-result allocation") + return Array(shape, [None] * math.prod(shape), dtype) + + def ascontiguousarray(self, values: object, dtype: str) -> Array: + return self.asarray(values, dtype) + + +def load_source() -> types.ModuleType: + tree = ast.parse(SOURCE.read_text(encoding="utf-8")) + tree.body = [node for node in tree.body if not ( + isinstance(node, ast.Import) and any(alias.name == "numpy" for alias in node.names) + or isinstance(node, ast.ImportFrom) and (node.module or "").startswith("truememory.") + or isinstance(node, ast.Try) and any( + isinstance(child, ast.Import) and any(alias.name == "psutil" for alias in child.names) + for child in node.body + ) + or isinstance(node, ast.Expr) and isinstance(node.value, ast.Call) + and isinstance(node.value.func, ast.Name) and node.value.func.id == "_set_mps_memory_cap" + )] + module = types.ModuleType("synthetic_allocation_model_server") + module.__dict__.update({ + "np": Numpy(), "psutil": None, "_USE_UNIX": True, "_LOOPBACK_HOST": "127.0.0.1", + "_env_int": lambda name, default, **kwargs: default, "pid_is_alive": lambda pid: False, + }) + sys.modules[module.__name__] = module + exec(compile(ast.fix_missing_locations(tree), str(SOURCE), "exec"), module.__dict__) + return module + + +class CountOnlyInputs: + """Large cardinality without allocating a large fixture or doing inference.""" + def __init__(self, count: int) -> None: + self.count = count + + def __len__(self) -> int: + return self.count + + def __getitem__(self, key: object) -> object: + raise AssertionError("Oversized request reached a native input slice") + + +class Workspace: + pass + + +class TestAllocationPreflight(unittest.TestCase): + def setUp(self) -> None: + package = types.ModuleType("truememory") + package.__path__ = [] + self.mps = types.ModuleType("truememory.mps_utils") + self.mps.is_mps_oom = lambda error: "MPS backend out of memory" in str(error) + self.mps.flush_mps_cache = Mock() + reranker = types.ModuleType("truememory.reranker") + reranker.get_current_reranker_name = lambda: "cross-encoder/ms-marco-MiniLM-L-6-v2" + vector = types.ModuleType("truememory.vector_search") + vector.EMBEDDING_MODEL = "model2vec" + vector._TIER_ALIASES = {"edge": "model2vec", "base": "qwen3_256", "pro": "qwen3_256"} + self.modules = patch.dict(sys.modules, { + "truememory": package, "truememory.mps_utils": self.mps, + "truememory.reranker": reranker, "truememory.vector_search": vector, + }) + self.modules.start() + self.environment = patch.dict(os.environ, {"TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD": "1"}) + self.environment.start() + self.ms = load_source() + self.server = self.ms.ModelServer() + self.server._write_status_file = lambda: None + self.server._get_embed_model = Mock(side_effect=AssertionError("Unexpected model load")) + self.server._get_reranker = Mock(side_effect=AssertionError("Unexpected model load")) + self.threads: list[threading.Thread] = [] + self.releases: list[threading.Event] = [] + + def tearDown(self) -> None: + for event in self.releases: + event.set() + for thread in self.threads: + thread.join(2) + self.assertFalse(thread.is_alive()) + self.server._workers.shutdown(wait=True, cancel_futures=True) + self.environment.stop() + self.modules.stop() + + @staticmethod + def request(op: str, count: int = 5, **kwargs: object) -> dict: + return {"op": op, "texts": [str(i) for i in range(count)], + "pairs": [("synthetic", str(i)) for i in range(count)], + "tier": "synthetic/custom", "model_name": "synthetic/reranker", + "batch_size": 2, **kwargs} + + def test_wire_arithmetic_matches_real_base64_json_including_empty_and_multilabel(self) -> None: + for field in ("vectors", "scores"): + for shape in ((0,), (1,), (2,), (3,), (2, 3), (1, 2, 3)): + with self.subTest(field=field, shape=shape): + array = Array(shape, [0.25] * math.prod(shape)) + response = {"ok": True, field: array, "protocol": self.ms.PROTOCOL_VERSION} + encoded = json.dumps(response, default=self.ms._json_default).encode() + self.assertEqual(self.ms._result_wire_size(shape, field), len(encoded)) + self.assertEqual(self.ms._response_wire_size(response), len(encoded)) + self.assertEqual(self.ms._array_base64_size(shape), + len(base64.b64encode(array.tobytes()))) + + def test_exact_limit_is_accepted_and_one_byte_less_is_rejected(self) -> None: + size = self.ms._result_wire_size((3, 2), "vectors") + with patch.object(self.ms, "_MAX_MESSAGE_SIZE", size): + self.ms._check_result_size((3, 2), "vectors") + with patch.object(self.ms, "_MAX_MESSAGE_SIZE", size - 1): + with self.assertRaisesRegex(self.ms._ResultTooLarge, f"requires {size} bytes"): + self.ms._check_result_size((3, 2), "vectors") + self.assertEqual(self.ms._MAX_MESSAGE_SIZE, 10485760) + self.assertEqual(self.ms._result_wire_size((7679, 256), "vectors"), 10484497) + self.assertEqual(self.ms._result_wire_size((7680, 256), "vectors"), 10485861) + self.assertEqual(self.ms._result_wire_size((1966061,), "scores"), 10485758) + self.ms._check_result_size((1966061,), "scores") + with self.assertRaises(self.ms._ResultTooLarge): + self.ms._check_result_size((1966062,), "scores") + + def test_known_embedding_constructors_reject_before_loading_or_slicing(self) -> None: + for tier in ("edge", "base", "pro", "model2vec", "qwen3_256", "minilm", "bge-small"): + with self.subTest(tier=tier): + response = self.server.handle_request({"op": "embed", "tier": tier, + "texts": CountOnlyInputs(7680)}) + self.assertEqual(response["error_code"], "result_too_large") + self.assertIn("Split the request", response["error"]) + self.server._get_embed_model.assert_not_called() + self.assertEqual(self.ms.np.allocations, []) + + def test_unknown_model_without_opt_in_checks_actual_model2vec_fallback(self) -> None: + with patch.dict(os.environ, {"TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD": "0"}): + response = self.server.handle_request({"op": "embed", "tier": "synthetic/custom", + "texts": CountOnlyInputs(7680)}) + self.assertEqual(response["error_code"], "result_too_large") + self.server._get_embed_model.assert_not_called() + + def test_builtin_scalar_rerankers_reject_before_loading_or_slicing(self) -> None: + for name in (None, "cross-encoder/ms-marco-MiniLM-L-6-v2", + "Alibaba-NLP/gte-reranker-modernbert-base"): + with self.subTest(name=name): + response = self.server.handle_request({"op": "rerank", "model_name": name, + "pairs": CountOnlyInputs(1966062)}) + self.assertEqual(response["error_code"], "result_too_large") + self.server._get_reranker.assert_not_called() + + def test_custom_native_shape_is_checked_before_float32_conversion_or_full_allocation(self) -> None: + for op, field, method in (("embed", "vectors", "encode"), ("rerank", "scores", "predict")): + with self.subTest(op=op): + native = Array((2, 1000000), dtype="float64") + model = types.SimpleNamespace(**{method: Mock(return_value=native)}) + setattr(self.server, "_get_embed_model" if op == "embed" else "_get_reranker", + Mock(return_value=model)) + conversions = self.ms.np.conversions + response = self.server.handle_request(self.request(op)) + self.assertEqual(response["error_code"], "result_too_large") + self.assertEqual(self.ms.np.conversions, conversions) + self.assertEqual(self.ms.np.allocations, []) + self.assertEqual(getattr(model, method).call_count, 1) + + def test_custom_truncation_upper_bound_and_changed_opt_in_do_not_reject_valid_output(self) -> None: + model = types.SimpleNamespace(encode=lambda texts, **kwargs: [[float(t), 0.5] for t in texts], + get_embedding_dimension=Mock(return_value=4096)) + self.server._embed_state = self.ms._EmbedState(model, "synthetic/custom", "synthetic/custom") + self.server._get_embed_model = Mock(return_value=model) + # Even a getter can report truncate_dim when the native width is unknown. + with patch.object(self.ms, "_MAX_MESSAGE_SIZE", 200), \ + patch.dict(os.environ, {"TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD": "0", + "TRUEMEMORY_CUSTOM_EMBED_DIM": "4096"}): + response = self.server.handle_request(self.request("embed")) + self.assertTrue(response["ok"]) + self.assertEqual(response["vectors"].shape, (5, 2)) + model.get_embedding_dimension.assert_not_called() + + def test_valid_microbatches_keep_all_rows_order_dimensions_and_float32(self) -> None: + for op, field, method in (("embed", "vectors", "encode"), ("rerank", "scores", "predict")): + calls = [] + + def forward(items: list, **kwargs: object) -> Array: + ids = [int(item if op == "embed" else item[1]) for item in items] + calls.append(ids) + return Array((len(ids), 2), [value for i in ids for value in (float(i), 0.5)], "float64") + + model = types.SimpleNamespace(**{method: forward}) + setattr(self.server, "_get_embed_model" if op == "embed" else "_get_reranker", + lambda _name, deadline=None: model) + response = self.server.handle_request(self.request(op)) + self.assertTrue(response["ok"]) + self.assertEqual(calls, [[0, 1], [2, 3], [4]]) + self.assertEqual(response[field].shape, (5, 2)) + self.assertEqual(response[field].dtype, "float32") + self.assertEqual(response[field].data, [value for i in range(5) for value in (float(i), 0.5)]) + + def test_native_count_and_later_dimension_changes_remain_errors(self) -> None: + with self.assertRaisesRegex(ValueError, "count"): + self.ms._store_batch_result(None, Array((1, 2), [0, 0]), 0, 2, 3) + result = self.ms._store_batch_result(None, Array((1, 2), [0, 0]), 0, 1, 2) + with self.assertRaisesRegex(ValueError, "dimensions changed"): + self.ms._store_batch_result(result, Array((1, 3), [0, 0, 0]), 1, 1, 2) + self.assertEqual(self.ms.np.allocations, [(2, 2)]) + + def test_empty_native_results_keep_their_existing_rank(self) -> None: + model = types.SimpleNamespace(encode=lambda *args, **kwargs: Array((0,), []), + predict=lambda *args, **kwargs: Array((0,), [])) + self.server._get_embed_model = lambda tier, deadline=None: model + self.server._get_reranker = lambda name, deadline=None: model + for op, field in (("embed", "vectors"), ("rerank", "scores")): + response = self.server.handle_request(self.request(op, count=0)) + self.assertEqual(response[field].shape, (0,)) + + def test_serializer_rejects_unknown_shape_before_array_conversion_or_base64(self) -> None: + response = {"ok": True, "vectors": Array((2, 1000000), dtype="float64")} + connection = Mock() + with patch.object(self.ms.np, "ascontiguousarray", side_effect=AssertionError("Array copy")), \ + patch.object(self.ms.base64, "b64encode", side_effect=AssertionError("Base64 copy")): + self.server._send_response(connection, response) + wire = connection.sendall.call_args.args[0] + self.assertEqual(struct.unpack(">I", wire[:4])[0], len(wire) - 4) + self.assertLessEqual(len(wire) - 4, self.ms._MAX_MESSAGE_SIZE) + self.assertEqual(json.loads(wire[4:]), {"ok": False, "error": "Response too large", "protocol": 1}) + + def test_fast_native_output_rejection_does_not_fall_back_or_copy(self) -> None: + model = types.SimpleNamespace(encode=Mock(return_value=Array((1, 2000000)))) + self.server._get_fast_encoder = Mock(return_value=model) + self.server._inference_lock.acquire() + try: + response = self.server.handle_request(self.request("embed", count=1)) + finally: + self.server._inference_lock.release() + self.assertEqual(response["error_code"], "result_too_large") + self.server._get_embed_model.assert_not_called() + self.assertEqual(self.ms.np.conversions, 0) + + def test_expiry_during_preflight_still_prevents_model_loading(self) -> None: + clock = [0.0] + + def preflight(tier: str, count: int) -> None: + clock[0] = 2.0 + + with patch.object(self.ms.time, "time", side_effect=lambda: clock[0]), \ + patch.object(self.ms.time, "monotonic", side_effect=lambda: clock[0]), \ + patch.object(self.server, "_preflight_embed_result", side_effect=preflight): + response = self.server.handle_request(self.request("embed", deadline=1.0)) + self.assertIn("deadline", response["error"]) + self.server._get_embed_model.assert_not_called() + + def test_embed_failed_workspace_is_gone_before_recovery_in_single_and_batch_paths(self) -> None: + for count, fail_on in ((1, 1), (5, 2)): + with self.subTest(count=count): + refs, calls, moves = [], [], [] + recovered = threading.Event() + testcase = self + + class Model: + def encode(self, texts: list, **kwargs: object) -> list: + calls.append(list(texts)) + if len(calls) == fail_on: + workspace = Workspace() + refs.append(weakref.ref(workspace)) + raise RuntimeError("MPS backend out of memory") + return [[float(text), 0.5] for text in texts] + + def to(self, device: str) -> None: + testcase.assertIsNone(refs[-1]()) + testcase.assertTrue(testcase.server._inference_lock.locked()) + testcase.assertTrue(testcase.server._lock.locked()) + moves.append(device) + recovered.set() + + self.server._get_embed_model = lambda tier, deadline=None: Model() + self.mps.flush_mps_cache.side_effect = lambda: self.assertIsNone(refs[-1]()) + response = self.server.handle_request(self.request("embed", count=count)) + self.assertTrue(response["ok"]) + self.assertTrue(recovered.is_set()) + self.assertEqual(moves, ["cpu"]) + self.assertEqual(calls, [["0"], ["0"]] if count == 1 else + [["0", "1"], ["2", "3"], ["2", "3"], ["4"]]) + self.assertIn("embed", self.server._sticky_cpu) + + def rerank_recovery_fixture(self, on_replacement: object = None) -> tuple[list, list, list]: + workspaces, old_models, calls = [], [], [] + testcase = self + + class Model: + def __init__(self, fail: bool) -> None: + self.fail = fail + + def predict(self, pairs: list, **kwargs: object) -> list: + ids = [pair[1] for pair in pairs] + calls.append(ids) + if self.fail and ids[0] == "2": + workspace = Workspace() + workspaces.append(weakref.ref(workspace)) + raise RuntimeError("MPS backend out of memory") + return [float(value) for value in ids] + + def get_model(name: str, deadline: object = None) -> Model: + if self.server._reranker is None: + replacing = "rerank" in self.server._sticky_cpu + if replacing: + self.assertIsNone(workspaces[-1]()) + self.assertIsNone(old_models[-1]()) + self.assertTrue(self.server._inference_lock.locked()) + self.assertTrue(self.server._lock.locked()) + if on_replacement is not None: + on_replacement() + self.server._reranker = Model(fail=not replacing) + self.server._reranker_name = name + if not replacing: + old_models.append(weakref.ref(self.server._reranker)) + return self.server._reranker + + def flush() -> None: + testcase.assertIsNone(workspaces[-1]()) + testcase.assertIsNone(old_models[-1]()) + + self.server._get_reranker = get_model + self.mps.flush_mps_cache.side_effect = flush + return workspaces, old_models, calls + + def test_rerank_releases_failed_workspace_and_old_model_before_cpu_constructor(self) -> None: + workspaces, old_models, calls = self.rerank_recovery_fixture() + response = self.server.handle_request(self.request("rerank")) + self.assertTrue(response["ok"]) + self.assertIsNone(workspaces[0]()) + self.assertIsNone(old_models[0]()) + self.assertEqual(calls, [["0", "1"], ["2", "3"], ["2", "3"], ["4"]]) + self.assertEqual(response["scores"].data, [0.0, 1.0, 2.0, 3.0, 4.0]) + + def test_cpu_constructor_failure_releases_ownership_and_keeps_failed_model_dropped(self) -> None: + def fail() -> None: + raise RuntimeError("Synthetic constructor failure") + + self.rerank_recovery_fixture(fail) + with self.assertRaisesRegex(RuntimeError, "Synthetic constructor failure"): + self.server.handle_request(self.request("rerank")) + self.assertIsNone(self.server._reranker) + self.assertFalse(self.server._lock.locked()) + self.assertFalse(self.server._inference_lock.locked()) + self.assertIn("rerank", self.server._sticky_cpu) + + def test_replacement_retains_ownership_while_ping_remains_responsive(self) -> None: + entered, release, competitor_started, loaded = (threading.Event() for _ in range(4)) + self.releases.append(release) + + def replacement() -> None: + entered.set() + self.assertTrue(release.wait(2)) + + self.rerank_recovery_fixture(replacement) + outcomes = [] + + def run(request: dict, started: threading.Event | None = None) -> None: + if started is not None: + started.set() + try: + outcomes.append(self.server.handle_request(request)) + except Exception as error: + outcomes.append(error) + + def load_embed(tier: str, deadline: object = None) -> object: + loaded.set() + return types.SimpleNamespace(encode=lambda texts, **kwargs: [[0.5] for _ in texts]) + + self.server._get_embed_model = load_embed + owner = threading.Thread(target=run, args=(self.request("rerank"),)) + self.threads.append(owner) + owner.start() + self.assertTrue(entered.wait(1)) + competitor = threading.Thread(target=run, args=(self.request("embed", count=2), competitor_started)) + self.threads.append(competitor) + competitor.start() + self.assertTrue(competitor_started.wait(1)) + self.assertFalse(loaded.wait(0.05)) + self.assertTrue(self.server.handle_request({"op": "ping"})["ok"]) + release.set() + for thread in self.threads: + thread.join(2) + self.assertFalse(thread.is_alive()) + self.assertTrue(loaded.is_set()) + self.assertEqual(len(outcomes), 2) + self.assertTrue(all(isinstance(result, dict) and result["ok"] for result in outcomes), outcomes) + + def test_expired_oom_drops_failed_cache_without_recovery_or_retry(self) -> None: + for op in ("embed", "rerank"): + with self.subTest(op=op): + clock, workspaces, models = [0.0], [], [] + + def fail(items: list, **kwargs: object) -> list: + workspace = Workspace() + workspaces.append(weakref.ref(workspace)) + clock[0] = 2.0 + raise RuntimeError("MPS backend out of memory") + + class Model: + encode = staticmethod(fail) + predict = staticmethod(fail) + to = Mock(side_effect=AssertionError("Expired CPU transfer")) + + def load(name: str, deadline: object = None) -> Model: + model = Model() + models.append(weakref.ref(model)) + if op == "embed": + self.server._embed_state = self.ms._EmbedState(model, name, "synthetic/custom") + else: + self.server._reranker = model + return model + + setattr(self.server, "_get_embed_model" if op == "embed" else "_get_reranker", load) + self.mps.flush_mps_cache.reset_mock() + with patch.object(self.ms.time, "time", side_effect=lambda: clock[0]), \ + patch.object(self.ms.time, "monotonic", side_effect=lambda: clock[0]): + response = self.server.handle_request(self.request(op, deadline=1.0)) + self.assertIn("deadline", response["error"]) + self.assertEqual(len(models), 1) + self.assertIsNone(models[0]()) + self.assertIsNone(workspaces[0]()) + self.assertIn(op, self.server._sticky_cpu) + self.mps.flush_mps_cache.assert_not_called() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-model-daemon-admission-297.py b/tests/test-model-daemon-admission-297.py new file mode 100644 index 00000000..abd47447 --- /dev/null +++ b/tests/test-model-daemon-admission-297.py @@ -0,0 +1,208 @@ +"""Inert daemon policy controls using actual source and synthetic sensors only.""" + +from __future__ import annotations + +import ast +import hashlib +import inspect +import logging +from pathlib import Path +import threading +import types +import unittest +from unittest.mock import Mock, patch + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = ROOT / "truememory/model_server.py" +GIB = 1 << 30 + + +def load_source(environment: dict[str, str], sensor: object) -> types.ModuleType: + names = {"_admission_setting", "_automatic_rss_budget_bytes", "_ProcessMemoryRefused", + "_RequestDeadlineExceeded", "ModelServer", "main"} + selected = [node for node in ast.parse(SOURCE.read_text()).body + if isinstance(node, (ast.FunctionDef, ast.ClassDef)) and node.name in names] + tree = ast.fix_missing_locations(ast.Module(body=[ast.ImportFrom(module="__future__", + names=[ast.alias(name="annotations")], level=0), *selected], type_ignores=[])) + module = types.ModuleType("synthetic_daemon_admission_297") + module.created_workers = [] + + class Workers: + def __init__(self, **kwargs: object) -> None: + module.created_workers.append(kwargs) + + module.__dict__.update({"os": types.SimpleNamespace(environ=environment), "psutil": sensor, + "threading": threading, "ThreadPoolExecutor": Workers, + "time": types.SimpleNamespace(time=lambda: 0.0), + "_MAX_MESSAGE_SIZE": 10 * 1024 * 1024, "logging": logging, + "log": logging.getLogger(module.__name__)}) + exec(compile(tree, str(SOURCE), "exec"), module.__dict__) + return module + + +class DaemonAdmissionControls(unittest.TestCase): + def setUp(self) -> None: + self.environment: dict[str, str] = {} + self.physical = Mock(return_value=types.SimpleNamespace(total=16 * GIB)) + self.current = Mock(return_value=types.SimpleNamespace(rss=1 * GIB)) + self.sensor = types.SimpleNamespace(virtual_memory=self.physical, + Process=lambda: types.SimpleNamespace(memory_info=self.current)) + self.module = load_source(self.environment, self.sensor) + + def automatic(self, value: object = True) -> object: + self.assertIn("automatic_rss_budget", inspect.signature(self.module.ModelServer).parameters) + return self.module.ModelServer(automatic_rss_budget=value) + + def test_direct_constructor_legacy_default_has_no_sensor_reads(self) -> None: + server = self.module.ModelServer() + self.assertEqual(server._max_rss_bytes, 0) + server._check_process_memory("synthetic legacy") + self.physical.assert_not_called() + self.current.assert_not_called() + + def test_original_default_disabled_test_preserved_byte_exact(self) -> None: + path = ROOT / "tests/test-model-process-admission-297.py" + source = path.read_text() + node = next(node for node in ast.walk(ast.parse(source)) if isinstance(node, ast.FunctionDef) + and node.name == "test_default_budget_is_disabled") + segment = ast.get_source_segment(source, node) + self.assertEqual(hashlib.sha256(segment.encode()).hexdigest(), + "1f18c4eb6d3fc61a08b34cf75a79fdb5ec663570129dd587adc63acedee83082") + + def test_automatic_physical_arithmetic_and_eight_GiB_ceiling(self) -> None: + for physical_gib, expected_gib in [(8, 2), (16, 4), (32, 8), (64, 8)]: + with self.subTest(physical_gib=physical_gib): + self.physical.return_value = types.SimpleNamespace(total=physical_gib * GIB) + server = self.automatic() + self.assertEqual(server._max_rss_bytes, expected_gib * GIB) + self.assertEqual(self.physical.call_count, 4) + self.current.assert_not_called() + + def test_physical_bytes_floor_division_and_signed64_boundary(self) -> None: + for physical, expected in [(4 * GIB + 3, GIB), (2**63 - 1, 8 * GIB)]: + with self.subTest(physical=physical): + self.physical.return_value = types.SimpleNamespace(total=physical) + self.assertEqual(self.automatic()._max_rss_bytes, expected) + + def test_invalid_physical_values_fail_before_workers_or_models(self) -> None: + for value in [None, True, False, 0, -1, 1, 2, 3, "17179869184", 1.0, + float("nan"), float("inf"), 2**63, 2**100]: + with self.subTest(value=value): + self.physical.return_value = types.SimpleNamespace(total=value) + with self.assertRaises(ValueError): + self.automatic() + self.assertEqual(self.module.created_workers, []) + self.current.assert_not_called() + + def test_unavailable_or_malformed_sensor_fails_before_workers(self) -> None: + for sensor in [None, types.SimpleNamespace(), + types.SimpleNamespace(virtual_memory=lambda: None)]: + with self.subTest(sensor=sensor): + self.module.psutil = sensor + with self.assertRaises(ValueError): + self.automatic() + self.assertEqual(self.module.created_workers, []) + + def test_physical_sensor_failure_is_controlled_without_workers(self) -> None: + for error in [OSError("synthetic unavailable"), RuntimeError("synthetic sensor failure"), + AttributeError("synthetic missing total"), TypeError("synthetic invalid measurement")]: + with self.subTest(error=type(error).__name__): + self.physical.side_effect = error + with self.assertRaises(ValueError): + self.automatic() + self.assertEqual(self.module.created_workers, []) + + def test_explicit_zero_wins_and_reads_neither_sensor(self) -> None: + self.environment["TRUEMEMORY_MODEL_SERVER_MAX_RSS_MB"] = "0" + self.module.psutil = None + server = self.automatic() + self.assertEqual(server._max_rss_bytes, 0) + server._check_process_memory("synthetic disabled") + self.physical.assert_not_called() + self.current.assert_not_called() + + def test_explicit_finite_budget_wins_without_physical_read(self) -> None: + for value in [1, 512, 2**31 - 1]: + with self.subTest(value=value): + self.environment["TRUEMEMORY_MODEL_SERVER_MAX_RSS_MB"] = str(value) + self.assertEqual(self.automatic()._max_rss_bytes, value * 1048576) + self.physical.assert_not_called() + self.current.assert_not_called() + + def test_malformed_explicit_values_keep_original_rejection(self) -> None: + for value in ["-1", "1.0", "nan", "inf", "true", "", " 1", "١", str(2**31)]: + with self.subTest(value=value): + self.environment["TRUEMEMORY_MODEL_SERVER_MAX_RSS_MB"] = value + with self.assertRaises(ValueError): + self.automatic() + self.assertEqual(self.module.created_workers, []) + self.physical.assert_not_called() + + def test_boolean_keyword_only_policy_shape(self) -> None: + for value in [None, 0, 1, "true", [], {}]: + with self.subTest(value=value), self.assertRaises(ValueError): + self.automatic(value) + with self.assertRaises(TypeError): + self.module.ModelServer(True) + self.assertEqual(self.module.created_workers, []) + + def test_automatic_budget_keeps_exact_sampled_RSS_boundary(self) -> None: + server = self.automatic() + for usage in [server._max_rss_bytes - 1, server._max_rss_bytes, server._max_rss_bytes + 1]: + with self.subTest(usage=usage), server._inference_lock: + self.current.return_value = types.SimpleNamespace(rss=usage) + if usage < server._max_rss_bytes: + server._check_process_memory("synthetic main") + else: + with self.assertRaises(self.module._ProcessMemoryRefused): + server._check_process_memory("synthetic main") + self.assertEqual(self.current.call_count, 3) + + def test_missing_current_RSS_refuses_finite_work(self) -> None: + server = self.automatic() + self.module.psutil = None + with server._fast_lock, self.assertRaises(self.module._ProcessMemoryRefused): + server._check_process_memory("synthetic fast") + + def test_sampled_admission_deadline_priority_is_unchanged(self) -> None: + server = self.automatic() + deadline = Mock() + deadline.check.side_effect = self.module._RequestDeadlineExceeded("synthetic expiry") + with self.assertRaises(self.module._RequestDeadlineExceeded): + server._check_process_memory("synthetic expired", deadline) + self.current.assert_not_called() + deadline.check.side_effect = [None, self.module._RequestDeadlineExceeded("synthetic sensor consumed deadline")] + self.current.return_value = types.SimpleNamespace(rss=server._max_rss_bytes) + with self.assertRaises(self.module._RequestDeadlineExceeded): + server._check_process_memory("synthetic expiry after sensor", deadline) + self.assertEqual(self.current.call_count, 1) + + def test_installed_main_requests_automatic_policy_without_real_start(self) -> None: + policies: list[bool] = [] + ran: list[bool] = [] + class Server: + def __init__(self, *, automatic_rss_budget: bool = False) -> None: + policies.append(automatic_rss_budget) + def run(self) -> None: + ran.append(True) + def _cleanup(self) -> None: + pass + self.module.ModelServer = Server + self.module.signal = types.SimpleNamespace(SIGTERM=15, SIGINT=2, SIGHUP=1, signal=Mock()) + self.module._handle_signal = Mock() + self.module.atexit = types.SimpleNamespace(register=Mock()) + with patch.dict("sys.modules", {"setproctitle": None}), patch.object(logging, "basicConfig"): + self.module.main() + self.assertEqual(policies, [True]) + self.assertEqual(ran, [True]) + + def test_original_module_and_console_launch_contract_is_preserved(self) -> None: + client = (ROOT / "truememory/model_client.py").read_text() + self.assertIn('[executable, "-m", "truememory.model_server"]', client) + self.assertIn('truememory-model-server = "truememory.model_server:main"', (ROOT / "pyproject.toml").read_text()) + self.assertEqual(hashlib.sha256((ROOT / "truememory/mps_utils.py").read_bytes()).hexdigest(), + "7057b1e5cdbd23d4de625e6e0c999d2a71455df5fec421842dd9841dabe1b69b") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-model-microbatches-735.py b/tests/test-model-microbatches-735.py new file mode 100644 index 00000000..a241ab11 --- /dev/null +++ b/tests/test-model-microbatches-735.py @@ -0,0 +1,1408 @@ +"""Synthetic batch-control regressions, including real framed socket transport.""" +from __future__ import annotations + +import socket +import threading +import types +import unittest +from collections.abc import Callable, Iterator + +import numpy as np +import pytest + +from truememory import model_client as mc +from truememory import model_server as ms + + +class FakeClock: + def __init__(self) -> None: + self.now = 1000.0 + + def time(self) -> float: + return self.now + + def monotonic(self) -> float: + return self.now + + +def vectors(texts: list[str]) -> np.ndarray: + return np.asarray([[len(text), sum(map(ord, text)), 0.5] for text in texts], + dtype=np.float64).reshape(len(texts), 3) + + +class RecordingModel: + def __init__(self) -> None: + self.calls: list[tuple[str, list, int]] = [] + self.fail_calls: set[int] = set() + self.after_call: Callable[[], None] = lambda: None + self.moves: list[str] = [] + + def record(self, kind: str, items: list, batch_size: int) -> None: + self.calls.append((kind, list(items), batch_size)) + self.after_call() + if len(self.calls) in self.fail_calls: + raise RuntimeError("MPS backend out of memory") + + def encode(self, texts: list[str], *, batch_size: int = 32, + show_progress_bar: bool) -> np.ndarray: + assert show_progress_bar is False + self.record("embed", texts, batch_size) + return vectors(texts) + + def predict(self, pairs: list[tuple[str, str]], *, batch_size: int, + show_progress_bar: bool) -> np.ndarray: + assert show_progress_bar is False + self.record("rerank", pairs, batch_size) + return np.asarray([len(pair[1]) / 10 for pair in pairs], dtype=np.float64) + + def to(self, device: str) -> None: + self.moves.append(device) + + +class RecordingThrottler: + def __init__(self, limits: list[int]) -> None: + self.limits = limits + self.before_count = 0 + self.after_counts: list[int] = [] + + def before_batch(self) -> tuple[int, dict]: + limit = self.limits[min(self.before_count, len(self.limits) - 1)] + self.before_count += 1 + return limit, {} + + def after_batch(self, count: int, _seconds: float) -> None: + self.after_counts.append(count) + + def should_flush_cache(self) -> bool: + return False + + +@pytest.fixture +def runtime(monkeypatch: pytest.MonkeyPatch) -> tuple[ms.ModelServer, RecordingModel]: + from truememory import mps_utils + + server = ms.ModelServer() + model = RecordingModel() + monkeypatch.setattr(server, "_get_embed_model", lambda _tier, deadline=None: model) + monkeypatch.setattr(server, "_get_fast_encoder", lambda _tier, deadline=None: model) + monkeypatch.setattr(server, "_get_reranker", lambda _name, deadline=None: model) + monkeypatch.setattr(server, "_write_status_file", lambda: None) + monkeypatch.setattr(server, "_flush_mps_cache", lambda: None) + monkeypatch.setattr(mps_utils, "flush_mps_cache", lambda: None) + return server, model + + +def payload(op: str, count: int, **kwargs: object) -> dict: + texts = [f"synthetic-{i}" for i in range(count)] + return {"op": op, "texts": texts, + "pairs": [("synthetic-query", text) for text in texts], **kwargs} + + +def activate(server: ms.ModelServer, limits: list[int]) -> RecordingThrottler: + throttler = RecordingThrottler(limits) + server._throttler = throttler + server._throttler_active = True + return throttler + + +@pytest.mark.parametrize("platform", ["linux", "win32"]) +@pytest.mark.parametrize("device", ["cpu", "cuda:0"]) +@pytest.mark.parametrize("op", ["embed", "rerank"]) +def test_sustained_unsupported_adaptive_policy_keeps_requested_bound( + runtime: tuple, monkeypatch: pytest.MonkeyPatch, platform: str, device: str, op: str, +) -> None: + from truememory.tier_switch import throttler as tm + + server, model = runtime + clock = FakeClock() + sleeps: list[float] = [] + clock.sleep = sleeps.append + monkeypatch.setattr(tm, "time", clock) + monkeypatch.setattr(tm, "sys", types.SimpleNamespace(platform=platform)) + throttler = tm.DynamicThrottler(device) + monkeypatch.setattr(throttler, "_budget_snapshot", lambda: None) + monkeypatch.setattr(throttler, "_read_all_channels", lambda: { + name: {"status": "unsupported", "required": False} + for name in ("mps_level", "growth_rate", "thermal") + }) + server._throttler, server._throttler_active = throttler, True + server._embed_timestamps = [ms.time.time()] * server._SUSTAINED_THRESHOLD + for _ in range(300): + response = server.handle_request(payload(op, 17, batch_size=8)) + assert response["ok"] + assert [len(items) for _, items, _ in model.calls] == [8, 8, 1] * 300 + assert all(limit == 8 for _, _, limit in model.calls) + assert sleeps == [] + assert throttler.items_processed == 0 + assert throttler.state_machine.good_streak == 0 + + +@pytest.mark.parametrize("op,maximum", [("embed", 32), ("rerank", 64)]) +def test_unsupported_policy_retains_server_cap_and_rechecks_applicability( + runtime: tuple, op: str, maximum: int, +) -> None: + server, model = runtime + throttler = activate(server, [2]) + server._embed_timestamps = [ms.time.time()] * server._SUSTAINED_THRESHOLD + throttler.adaptive_applicable = False + assert server.handle_request(payload(op, maximum + 3, batch_size=maximum * 10))["ok"] + assert [len(items) for _, items, _ in model.calls] == [maximum, 3] + assert throttler.before_count == 0 + assert throttler.after_counts == [] + model.calls.clear() + throttler.adaptive_applicable = True + assert server.handle_request(payload(op, 5, batch_size=8))["ok"] + assert [len(items) for _, items, _ in model.calls] == [2, 2, 1] + assert throttler.before_count == 1 + assert throttler.after_counts == [5] + + +@pytest.mark.parametrize("op,key", [("embed", "vectors"), ("rerank", "scores")]) +@pytest.mark.parametrize("requested,limits,expected", [ + (1, [8], [1, 1, 1, 1, 1]), + (8, [2], [2, 2, 1]), + (8, [2, 1, 2], [2, 2, 1]), +]) +def test_each_actual_call_obeys_caller_and_captured_throttler_limit( + runtime: tuple, op: str, key: str, requested: int, + limits: list[int], expected: list[int], +) -> None: + server, model = runtime + throttler = activate(server, limits) + request = payload(op, 5, batch_size=requested) + response = server.handle_request(request) + assert response["ok"] + assert [len(items) for _, items, _ in model.calls] == expected + assert all(len(items) <= limit <= requested for _, items, limit in model.calls) + assert throttler.before_count == 1 + assert throttler.after_counts == [5] + expected_values = vectors(request["texts"]) if op == "embed" else [ + len(pair[1]) / 10 for pair in request["pairs"] + ] + np.testing.assert_array_equal(response[key], np.asarray(expected_values, dtype=np.float32)) + assert response[key].dtype == np.float32 + + +@pytest.mark.parametrize("op,key,maximum", [ + ("embed", "vectors", 32), + ("rerank", "scores", 64), +]) +@pytest.mark.parametrize("explicit", [False, True]) +def test_server_cap_also_bounds_legacy_and_oversized_caller_requests( + runtime: tuple, op: str, key: str, maximum: int, explicit: bool, +) -> None: + server, model = runtime + request = payload(op, maximum + 3) + if explicit: + request["batch_size"] = maximum * 10 + response = server.handle_request(request) + assert response["ok"] + assert [len(items) for _, items, _ in model.calls] == [maximum, 3] + assert response[key].shape[0] == maximum + 3 + + +@pytest.mark.parametrize("op,key", [("embed", "vectors"), ("rerank", "scores")]) +def test_duplicates_mixed_lengths_and_multidimensional_scores_keep_order( + runtime: tuple, monkeypatch: pytest.MonkeyPatch, op: str, key: str, +) -> None: + server, model = runtime + texts = ["fixture", "x" * 1000, "fixture", "", "longer fixture"] + request = {"op": op, "texts": texts, + "pairs": [("query", text) for text in texts], "batch_size": 2} + if op == "rerank": + def predict(pairs: list, **kwargs: object) -> np.ndarray: + model.record("rerank", pairs, kwargs["batch_size"]) + return vectors([pair[1] for pair in pairs]) + + monkeypatch.setattr(model, "predict", predict) + response = server.handle_request(request) + assert response["ok"] + assert response[key].shape == (5, 3) + np.testing.assert_array_equal(response[key], vectors(texts).astype(np.float32)) + + +@pytest.mark.parametrize("op,key", [("embed", "vectors"), ("rerank", "scores")]) +@pytest.mark.parametrize("count", [0, 1]) +def test_empty_and_single_inputs_preserve_model_output_shape( + runtime: tuple, op: str, key: str, count: int, +) -> None: + server, model = runtime + response = server.handle_request(payload(op, count, batch_size=1)) + assert response["ok"] + assert response[key].shape == ((count, 3) if op == "embed" else (count,)) + assert response[key].dtype == np.float32 + assert len(model.calls) == 1 + assert model.calls[0][2] == 1 + + +def test_contended_single_text_fast_lane_keeps_limit( + runtime: tuple, +) -> None: + server, model = runtime + with server._lock: + response = server.handle_request(payload("embed", 1, batch_size=1)) + assert response["ok"] + assert len(model.calls) == 1 + assert model.calls[0][2] == 1 + + +@pytest.mark.parametrize("op,key", [("embed", "vectors"), ("rerank", "scores")]) +def test_only_failed_microbatch_retries_and_completed_work_is_retained( + runtime: tuple, op: str, key: str, +) -> None: + server, model = runtime + model.fail_calls.add(2) + request = payload(op, 5, batch_size=2) + response = server.handle_request(request) + assert response["ok"] + source = request["texts" if op == "embed" else "pairs"] + assert [items for _, items, _ in model.calls] == [ + source[:2], source[2:4], source[2:4], source[4:], + ] + assert [limit for _, _, limit in model.calls] == [2, 2, 2, 2] + assert server._sticky_cpu == {op} + expected = vectors(source) if op == "embed" else [len(pair[1]) / 10 for pair in source] + np.testing.assert_array_equal(response[key], np.asarray(expected, dtype=np.float32)) + + +@pytest.mark.parametrize("op", ["embed", "rerank"]) +def test_expired_between_batches_does_not_start_another_call( + runtime: tuple, monkeypatch: pytest.MonkeyPatch, op: str, +) -> None: + server, model = runtime + clock = FakeClock() + monkeypatch.setattr(ms, "time", clock) + model.after_call = lambda: setattr(clock, "now", clock.now + 2) + response = server.handle_request(payload(op, 5, batch_size=2, deadline=clock.now + 1)) + assert not response["ok"] + assert "deadline" in response["error"] + assert len(model.calls) == 1 + assert server._inflight == 0 + assert not server._lock.locked() + + +@pytest.mark.parametrize("op", ["embed", "rerank"]) +def test_expiry_on_later_oom_skips_retry_and_remaining_inputs( + runtime: tuple, monkeypatch: pytest.MonkeyPatch, op: str, +) -> None: + server, model = runtime + clock = FakeClock() + monkeypatch.setattr(ms, "time", clock) + model.fail_calls.add(2) + + def advance_on_failure() -> None: + if len(model.calls) == 2: + clock.now += 2 + + model.after_call = advance_on_failure + response = server.handle_request(payload(op, 5, batch_size=2, deadline=clock.now + 1)) + assert not response["ok"] + assert "deadline" in response["error"] + assert len(model.calls) == 2 + assert server._sticky_cpu == {op} + assert model.moves == [] + + +@pytest.mark.parametrize("op", ["embed", "rerank"]) +def test_different_model_request_cannot_interleave_successful_microbatches( + runtime: tuple, monkeypatch: pytest.MonkeyPatch, op: str, +) -> None: + server, model = runtime + alternate = RecordingModel() + getter = "_get_embed_model" if op == "embed" else "_get_reranker" + loads: list[str] = [] + premature_releases: list[int] = [] + competitor_waiting = threading.Event() + primary_thread = threading.get_ident() + + class ObservingLock: + def __init__(self) -> None: + self.lock = threading.Lock() + + def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: + if threading.get_ident() != primary_thread: + competitor_waiting.set() + return self.lock.acquire(blocking, timeout) + + def release(self) -> None: + if threading.get_ident() == primary_thread and 0 < len(model.calls) < 3: + premature_releases.append(len(model.calls)) + self.lock.release() + + server._lock = ObservingLock() + + def load(identity: str, deadline: object = None) -> RecordingModel: + loads.append(identity) + return model if identity == "primary" else alternate + + monkeypatch.setattr(server, getter, load) + identity_field = "tier" if op == "embed" else "model_name" + responses: list[dict] = [] + errors: list[Exception] = [] + + def compete() -> None: + try: + responses.append(server.handle_request(payload( + op, 2, batch_size=2, **{identity_field: "alternate"}, + ))) + except Exception as error: + errors.append(error) + + competitor = threading.Thread(target=compete) + + def interleave() -> None: + if len(model.calls) == 1: + competitor.start() + assert competitor_waiting.wait(timeout=2) + assert loads == ["primary"] + + model.after_call = interleave + try: + response = server.handle_request(payload( + op, 5, batch_size=2, **{identity_field: "primary"}, + )) + finally: + if competitor.ident is not None: + competitor.join(timeout=3) + assert response["ok"] + assert not competitor.is_alive() + assert errors == [] + assert len(responses) == 1 and responses[0]["ok"] + assert loads == ["primary", "alternate"] + assert premature_releases == [] + assert len(model.calls) == 3 + assert len(alternate.calls) == 1 + + +@pytest.mark.parametrize("op", ["embed", "rerank"]) +def test_throttler_sampling_stays_outside_model_lock( + runtime: tuple, op: str, +) -> None: + server, _model = runtime + + class NonblockingThrottler(RecordingThrottler): + def before_batch(self) -> tuple[int, dict]: + acquired = server._lock.acquire(blocking=False) + assert acquired, "sensor sampling must not hold the model state lock" + server._lock.release() + return super().before_batch() + + throttler = NonblockingThrottler([2]) + server._throttler = throttler + server._throttler_active = True + assert server.handle_request(payload(op, 5, batch_size=8))["ok"] + assert throttler.before_count == 1 + + +@pytest.mark.parametrize("bad", [0, -1, True, False, 1.5, "2", None, [], {}]) +@pytest.mark.parametrize("op", ["embed", "rerank"]) +def test_invalid_batch_size_fails_before_client_transport_or_server_model_loading( + runtime: tuple, monkeypatch: pytest.MonkeyPatch, op: str, bad: object, +) -> None: + server, model = runtime + monkeypatch.setattr(mc, "_request_with_autostart", lambda *_a, **_kw: pytest.fail("transport")) + proxy = mc.EmbeddingProxy().encode if op == "embed" else mc.RerankerProxy().predict + with pytest.raises(ValueError, match="positive integer"): + proxy(["synthetic"], batch_size=bad) + response = server.handle_request(payload(op, 2, batch_size=bad)) + assert not response["ok"] + assert "positive integer" in response["error"] + assert model.calls == [] + + +@pytest.mark.parametrize("op", ["embed", "rerank"]) +def test_proxy_supports_numpy_integer_and_preserves_timeout_and_identity( + monkeypatch: pytest.MonkeyPatch, op: str, +) -> None: + requests: list[dict] = [] + + def record(request: dict, timeout: float | None = None) -> dict: + assert timeout == 0.75 + requests.append(request) + return {"ok": True, "vectors": np.zeros((1, 3)), "scores": np.zeros(1)} + + monkeypatch.setattr(mc, "_request_with_autostart", record) + if op == "embed": + mc.EmbeddingProxy("synthetic-tier").encode("synthetic", batch_size=np.int64(2), timeout=0.75) + assert requests[0]["texts"] == ["synthetic"] + assert requests[0]["tier"] == "synthetic-tier" + else: + mc.RerankerProxy("synthetic-model").predict([("q", "d")], batch_size=np.int64(2), timeout=0.75) + assert requests[0]["model_name"] == "synthetic-model" + assert requests[0]["batch_size"] == 2 + assert type(requests[0]["batch_size"]) is int + assert requests[0]["op"] == op + "_batched" + + +@pytest.mark.parametrize("op", ["embed", "rerank"]) +@pytest.mark.parametrize("legacy_daemon", [False, True]) +def test_framed_protocol_enforces_bounds_or_rejects_old_daemon_before_inference( + runtime: tuple, monkeypatch: pytest.MonkeyPatch, op: str, legacy_daemon: bool, +) -> None: + server, model = runtime + client_socket, server_socket = socket.socketpair() + monkeypatch.setattr(ms, "_USE_UNIX", True) + monkeypatch.setattr(mc, "_connect", lambda _deadline: client_socket) + monkeypatch.setattr(mc, "_start_server", lambda **_kw: pytest.fail("unexpected autostart")) + if legacy_daemon: + current_handler = server._handle_request_inner + + def legacy_handler(request: dict) -> dict: + if request["op"] not in ("embed", "rerank", "ping"): + return {"ok": False, "error": f"Unknown op: {request['op']}"} + return current_handler(request) + + monkeypatch.setattr(server, "_handle_request_inner", legacy_handler) + thread = threading.Thread(target=server.handle_client, args=(server_socket,)) + thread.start() + request = payload(op, 5) + method = mc.EmbeddingProxy().encode if op == "embed" else mc.RerankerProxy().predict + try: + inputs = request["texts" if op == "embed" else "pairs"] + if legacy_daemon: + with pytest.raises(mc.ProtocolMismatchError, match="restart it after upgrading"): + method(inputs, batch_size=2, timeout=2) + assert model.calls == [] + else: + output = method(inputs, batch_size=2, timeout=2) + assert output.shape == ((5, 3) if op == "embed" else (5,)) + assert output.dtype == np.float32 + assert [len(items) for _, items, _ in model.calls] == [2, 2, 1] + expected = vectors(inputs) if op == "embed" else [len(pair[1]) / 10 for pair in inputs] + np.testing.assert_array_equal(output, np.asarray(expected, dtype=np.float32)) + finally: + client_socket.close() + server_socket.close() + thread.join(timeout=3) + assert not thread.is_alive() + + +def test_output_count_or_dimension_mismatch_cannot_return_partial_success() -> None: + with pytest.raises(ValueError, match="count"): + ms._store_batch_result(None, [[1, 2]], 0, 2, 3) + result = ms._store_batch_result(None, [[1, 2]], 0, 1, 2) + with pytest.raises(ValueError, match="dimensions"): + ms._store_batch_result(result, [[1, 2, 3]], 1, 1, 2) + + +class OrderArray: + """Small indexing fake; these tests can run without importing NumPy.""" + + def __init__(self, values: list, shape: tuple | None = None) -> None: + self.values = list(values) + self.shape = shape or ((len(values), len(values[0])) if values and isinstance(values[0], list) + else (len(values),)) + self.ndim = len(self.shape) + + def __len__(self) -> int: + return len(self.values) + + def __iter__(self) -> Iterator[object]: + return iter(self.values) + + def __getitem__(self, key: object) -> object: + if isinstance(key, OrderArray): + return OrderArray([self.values[index] for index in key], (len(key), *self.shape[1:])) + if isinstance(key, slice): + values = self.values[key] + return OrderArray(values, (len(values), *self.shape[1:])) + return self.values[key] + + def __setitem__(self, key: object, value: "OrderArray") -> None: + indices = list(key) if isinstance(key, OrderArray) else list(range(len(self)))[key] + assert len(indices) == len(value) + for index, row in zip(indices, value): + self.values[index] = row + + +class OrderNP: + ndarray = OrderArray + float32 = "float32" + + def __init__(self) -> None: + self.sort_sizes: list[int] = [] + self.allocations: list[tuple] = [] + self.after_sort: Callable[[], None] = lambda: None + + def argsort(self, values: object) -> OrderArray: + keys = list(values) + self.sort_sizes.append(len(keys)) + # Deliberately unstable ties make a second local sort observable. + indices = sorted(range(len(keys)), key=lambda index: (keys[index], -index)) + self.after_sort() + return OrderArray(indices) + + def asarray(self, values: object, dtype: object = None) -> OrderArray: + return values if isinstance(values, OrderArray) else OrderArray(list(values)) + + def empty(self, shape: tuple, dtype: object = None) -> OrderArray: + self.allocations.append(shape) + return OrderArray([None] * shape[0], shape) + + +class OrderModel: + """Model rows identify their entire native batch and their position in it.""" + + def __init__(self, numpy: OrderNP) -> None: + self.numpy = numpy + self.calls: list[list] = [] + self.limits: list[int] = [] + self.native_batches: list[list[str]] = [] + self.fail_calls: set[int] = set() + self.after_call: Callable[[], None] = lambda: None + self.after_move: Callable[[], None] = lambda: None + self.moves: list[str] = [] + self.default_prompt = "Instruct: retain the source\nQuery: " + + @staticmethod + def _input_length(text: str) -> int: + return len(text) + + def _can_flatten_inputs(self) -> bool: + return False + + def encode(self, texts: list, *, batch_size: int, show_progress_bar: bool) -> OrderArray: + assert show_progress_bar is False + self.calls.append(list(texts)) + self.limits.append(batch_size) + self.after_call() + if len(self.calls) in self.fail_calls: + raise RuntimeError("MPS backend out of memory") + order = self.numpy.argsort([-len(text) for text in texts]) + rows = [None] * len(texts) + for start in range(0, len(texts), batch_size): + indices = order[start:start + batch_size] + features = [self.default_prompt + str(texts[index]) for index in indices] + self.native_batches.append(features) + fingerprint = sum((index + 1) * sum(map(ord, text)) for index, text in enumerate(features)) + for position, index in enumerate(indices): + rows[index] = [fingerprint, position, sum(map(ord, features[position]))] + return OrderArray(rows, (len(texts), 3)) + + def to(self, device: str) -> None: + self.moves.append(device) + self.after_move() + + +def load_order_runtime() -> dict: + """Execute actual control methods with stdlib dependencies and array fakes.""" + import ast + import builtins + import json + import logging + import math + from contextlib import contextmanager + from dataclasses import dataclass + from pathlib import Path + + path = Path(__file__).resolve().parents[1] / "truememory/model_server.py" + functions = {"_array_metadata", "_array_base64_size", "_result_wire_size", "_check_result_size", + "_batch_limit", "_checked_batch", "_store_batch_result"} + classes = {"_EmbedState", "_RequestDeadline", "_RequestDeadlineExceeded", "_ResultTooLarge", + "_ProcessMemoryRefused"} + methods = {"handle_request", "_handle_request_inner", "_handle_fast_embed", "_embed_global_order", + "_embed_slice_indices", "_preflight_embed_result", "_resolve_embed_cache", + "_request_batch_limit", "_after_request_batches", "_recover_embed_oom_locked", + "_check_embed_recovery_deadline_locked", "_rerank_global_order", "_rerank_slice_indices", + "_preflight_rerank_result", "_publish_embed_state", "_retire_stale_fast_encoder", + "_check_process_memory", "_check_serving_embed_model", "_serving_embed_response", + "_serving_reranker_name", "_check_serving_reranker"} + body = [ast.ImportFrom(module="__future__", names=[ast.alias(name="annotations")], level=0)] + for node in ast.parse(path.read_text(encoding="utf-8")).body: + if isinstance(node, ast.FunctionDef) and node.name in functions: + body.append(node) + elif isinstance(node, ast.ClassDef) and node.name in classes: + body.append(node) + elif isinstance(node, ast.ClassDef) and node.name == "ModelServer": + node.body = [part for part in node.body if isinstance(part, ast.Assign) + or isinstance(part, ast.FunctionDef) and part.name in methods] + body.append(node) + + mps = types.SimpleNamespace(is_mps_oom=lambda error: "out of memory" in str(error), + flush_mps_cache=lambda: None) + + def safe_import(name: str, *args: object, **kwargs: object) -> object: + if name == "__future__": + return builtins.__import__(name, *args, **kwargs) + if name == "truememory.mps_utils": + return mps + if name == "truememory.reranker": + return types.SimpleNamespace(get_current_reranker_name=lambda: "Alibaba-NLP/gte-reranker-modernbert-base") + raise AssertionError("Unexpected runtime import: " + name) + + numpy, clock = OrderNP(), FakeClock() + namespace = {"__name__": __name__, "np": numpy, "time": clock, "math": math, "json": json, + "threading": threading, "contextmanager": contextmanager, "dataclass": dataclass, + "log": logging.getLogger("synthetic-order"), "PROTOCOL_VERSION": 1, + "_MAX_MESSAGE_SIZE": 10 * 1024**2, "_EMBED_BATCH_LIMIT": 32, "_RERANK_BATCH_LIMIT": 64, + "__builtins__": dict(vars(builtins), __import__=safe_import), "mps": mps} + exec(compile(ast.fix_missing_locations(ast.Module(body=body, type_ignores=[])), str(path), "exec"), namespace) + return namespace + + +class TestQwenGlobalOrder(unittest.TestCase): + def runtime(self) -> tuple[dict, object, OrderModel]: + module = load_order_runtime() + server = object.__new__(module["ModelServer"]) + server._max_rss_bytes = 0 + model = OrderModel(module["np"]) + server._embed_state = module["_EmbedState"](model, "base", "qwen3_256") + server._lock = threading.Lock() + server._inference_lock = threading.Lock() + server._activity_lock = threading.Lock() + server._fast_lock = threading.Lock() + server._residency_lock = threading.Lock() + server._fast_encoder = None + server._fast_model_id = None + server._fast_generation = None + server._transport_context = types.SimpleNamespace() + server._embed_timestamps = [] + server._throttler_active = False + server._throttler = None + server._inflight = 0 + server._sticky_cpu = set() + server.loads = 0 + + def load(_tier: str, deadline: object = None) -> OrderModel: + server.loads += 1 + return model + + server._get_embed_model = load + server._get_fast_encoder = load + server._mark_sticky_cpu = server._sticky_cpu.add + server._deactivate_throttler = lambda: None + server._flush_mps_cache = lambda: None + return module, server, model + + @staticmethod + def texts(count: int) -> list[str]: + samples = ["", "same", "same", "aa", "bb", "é", "東京", "x" * 100, "tail"] + return [samples[(index * 5 + index // 9) % len(samples)] for index in range(count)] + + def request(self, server: object, texts: list, **kwargs: object) -> dict: + return server.handle_request({"op": "embed", "tier": "base", "texts": texts, **kwargs}) + + def reference(self, texts: list, limit: int) -> tuple[OrderArray, list[list[str]]]: + model = OrderModel(OrderNP()) + values = model.encode(texts, batch_size=limit, show_progress_bar=False) + return values, model.native_batches + + def contiguous_reference(self, texts: list, limit: int) -> tuple[OrderArray, list[list[str]]]: + model = OrderModel(OrderNP()) + values = [] + for start in range(0, max(1, len(texts)), limit): + values.extend(model.encode(texts[start:start + limit], batch_size=limit, + show_progress_bar=False).values) + return OrderArray(values, (len(texts), 3)), model.native_batches + + def test_native_membership_ties_duplicates_unicode_and_scatter(self) -> None: + for count in (0, 1, 2, 8, 31, 32, 33, 63, 64, 65, 100): + for requested in (1, 2, 8, 32, 100): + with self.subTest(count=count, requested=requested): + module, server, model = self.runtime() + texts = self.texts(count) + limit = min(requested, 32) + reference = self.reference if limit == 32 else self.contiguous_reference + expected, batches = reference(texts, limit) + response = self.request(server, texts, batch_size=requested) + self.assertTrue(response["ok"]) + self.assertEqual(response["vectors"].values, expected.values) + self.assertEqual(model.native_batches, batches) + self.assertTrue(all(len(call) <= bound <= limit for call, bound in zip(model.calls, model.limits))) + self.assertLessEqual(len(module["np"].allocations), 1) + if limit != 32: + self.assertEqual(model.calls, [texts[start:start + limit] + for start in range(0, max(1, count), limit)]) + self.assertEqual(module["np"].sort_sizes, [len(call) for call in model.calls]) + + def test_default_request_preserves_native_32_policy(self) -> None: + for count in (33, 65, 100): + with self.subTest(count=count): + _module, server, model = self.runtime() + texts = self.texts(count) + expected, batches = self.reference(texts, 32) + response = self.request(server, texts) + self.assertEqual(response["vectors"].values, expected.values) + self.assertEqual(model.native_batches, batches) + self.assertEqual(model.limits, [32] * len(model.calls)) + + def test_single_and_one_effective_batch_do_not_plan_or_reorder_inputs(self) -> None: + for count in (0, 1, 8, 32): + module, server, model = self.runtime() + texts = self.texts(count) + self.assertTrue(self.request(server, texts)["ok"]) + self.assertEqual(model.calls, [texts]) + self.assertEqual(module["np"].sort_sizes, [count]) + + def test_contended_single_input_retains_the_existing_fast_lane(self) -> None: + module, server, model = self.runtime() + model.after_call = lambda: self.assertTrue(server._fast_lock.locked() and not server._lock.locked()) + with server._inference_lock: + response = self.request(server, ["synthetic query"]) + self.assertTrue(response["ok"]) + self.assertEqual(model.calls, [["synthetic query"]]) + self.assertEqual(model.limits, [1]) + self.assertEqual(module["np"].sort_sizes, [1]) + + def test_unsupported_identity_interface_flattening_and_inputs_keep_bounded_slices(self) -> None: + for mode in ("other-model", "legacy", "no-flatten-probe", "flattened", "non-string"): + with self.subTest(mode=mode): + module, server, model = self.runtime() + texts = self.texts(33) + if mode == "other-model": + server._embed_state = module["_EmbedState"](model, "base", "model2vec") + elif mode == "legacy": + model._input_length = None + elif mode == "no-flatten-probe": + model._can_flatten_inputs = None + elif mode == "flattened": + model._can_flatten_inputs = lambda: True + else: + texts[5] = ["synthetic", "pair"] + self.assertTrue(self.request(server, texts, batch_size=32)["ok"]) + self.assertEqual(model.calls, [texts[start:start + 32] for start in range(0, len(texts), 32)]) + self.assertNotIn(33, module["np"].sort_sizes) + + def test_negative_controls_detect_missing_global_order_compensation_and_scatter(self) -> None: + for missing in ("global-order", "tie-compensation", "scatter"): + with self.subTest(missing=missing): + module, server, model = self.runtime() + texts = self.texts(65) + expected, batches = self.reference(texts, 32) + if missing == "global-order": + server._embed_global_order = lambda *_args: None + elif missing == "tie-compensation": + server._embed_slice_indices = lambda _model, _texts, order, offset, limit, _deadline: order[offset:offset + limit] + else: + store = module["_store_batch_result"] + module["_store_batch_result"] = lambda *args, **_kwargs: store(*args) + response = self.request(server, texts, batch_size=32) + self.assertNotEqual(response["vectors"].values, expected.values) + if missing != "scatter": + self.assertNotEqual(model.native_batches, batches) + + def test_negative_control_detects_missing_effective_limit_guard(self) -> None: + import ast + from pathlib import Path + from unittest.mock import patch + + path = Path(__file__).resolve().parents[1] / "truememory/model_server.py" + tree = ast.parse(path.read_text(encoding="utf-8")) + server_class = next(node for node in tree.body + if isinstance(node, ast.ClassDef) and node.name == "ModelServer") + method = next(node for node in server_class.body + if isinstance(node, ast.FunctionDef) and node.name == "_embed_global_order") + removed = 0 + for node in ast.walk(method): + if not isinstance(node, ast.BoolOp): + continue + retained = [] + for value in node.values: + if (isinstance(value, ast.Compare) and isinstance(value.left, ast.Name) + and value.left.id == "limit" and len(value.ops) == 1 + and isinstance(value.ops[0], ast.NotEq) + and isinstance(value.comparators[0], ast.Constant) + and value.comparators[0].value == 32): + removed += 1 + else: + retained.append(value) + node.values = retained + self.assertEqual(removed, 1) + with patch.object(Path, "read_text", return_value=ast.unparse(tree)): + for adaptive in (False, True): + with self.subTest(adaptive=adaptive): + _module, server, model = self.runtime() + if adaptive: + server._throttler = RecordingThrottler([8]) + server._throttler_active = True + texts = self.texts(65) + expected, batches = self.contiguous_reference(texts, 8) + response = self.request(server, texts, **({} if adaptive else {"batch_size": 8})) + self.assertTrue(response["ok"]) + self.assertNotEqual(response["vectors"].values, expected.values) + self.assertNotEqual(model.native_batches, batches) + self.assertNotEqual(model.calls, [texts[start:start + 8] for start in range(0, 65, 8)]) + + def test_cyclic_ties_require_the_inverse_and_place_each_occurrence_once(self) -> None: + from unittest.mock import patch + + placements: list[int] = [] + setitem = OrderArray.__setitem__ + + def record_placement(array: OrderArray, key: object, value: OrderArray) -> None: + indices = list(key) if isinstance(key, OrderArray) else list(range(len(array)))[key] + placements.extend(indices) + setitem(array, key, value) + + def cyclic_argsort(numpy: OrderNP, values: object) -> OrderArray: + keys = list(values) + numpy.sort_sizes.append(len(keys)) + groups: dict[int, list[int]] = {} + for index, key in enumerate(keys): + groups.setdefault(key, []).append(index) + order: list[int] = [] + for key in sorted(groups): + group = groups[key] + # Reversing ties is its own inverse; cycles expose direction errors. + order.extend(group[1:] + group[:1]) + numpy.after_sort() + return OrderArray(order) + + with patch.object(OrderNP, "argsort", cyclic_argsort), \ + patch.object(OrderArray, "__setitem__", record_placement): + for count in (31, 33, 63, 64, 65): + for limit in (8, 32): + with self.subTest(count=count, limit=limit): + texts = self.texts(count) + reference = self.reference if limit == 32 else self.contiguous_reference + expected, batches = reference(texts, limit) + _module, server, model = self.runtime() + placements.clear() + response = self.request(server, texts, batch_size=limit) + self.assertTrue(response["ok"]) + self.assertEqual(response["vectors"].values, expected.values) + self.assertEqual(model.native_batches, batches) + self.assertEqual(sorted(placements), list(range(count))) + if count <= limit or limit != 32: + continue + + module, server, model = self.runtime() + + def wrong_inverse( + model: object, texts: list, order: OrderArray, + offset: int, limit: int, deadline: object, + ) -> OrderArray: + deadline.check() + indices = order[offset:offset + limit] + inner = module["np"].argsort([-model._input_length(texts[index]) for index in indices]) + deadline.check() + return indices[inner] + + server._embed_slice_indices = wrong_inverse + placements.clear() + response = self.request(server, texts, batch_size=limit) + self.assertTrue(response["ok"]) + self.assertEqual(sorted(placements), list(range(count))) + self.assertNotEqual(model.native_batches, batches) + self.assertNotEqual(response["vectors"].values, expected.values) + + def test_adaptive_limit_is_captured_once_and_cannot_raise_caller_cap(self) -> None: + for limits, requested, effective in (([2, 1, 16], 8, 2), ([64], 8, 8), + ([8, 2, 32], None, 8), ([8], 32, 8), + ([8], 100, 8), ([64], 100, 32)): + module, server, model = self.runtime() + throttler = RecordingThrottler(limits) + server._throttler, server._throttler_active = throttler, True + texts = self.texts(65) + reference = self.reference if effective == 32 else self.contiguous_reference + expected, batches = reference(texts, effective) + response = self.request(server, texts, **({} if requested is None else {"batch_size": requested})) + self.assertEqual(response["vectors"].values, expected.values) + self.assertEqual(model.native_batches, batches) + self.assertEqual(model.limits, [effective] * len(model.calls)) + self.assertEqual(throttler.before_count, 1) + self.assertEqual(throttler.after_counts, [65]) + if effective != 32: + self.assertEqual(model.calls, [texts[start:start + effective] for start in range(0, 65, effective)]) + self.assertEqual(module["np"].sort_sizes, [len(call) for call in model.calls]) + + def test_result_preflight_precedes_model_load_and_planning(self) -> None: + module, server, model = self.runtime() + response = self.request(server, [""] * 7680) + self.assertEqual(response["error_code"], "result_too_large") + self.assertEqual(server.loads, 0) + self.assertEqual(model.calls, []) + self.assertEqual(module["np"].sort_sizes, []) + self.assertEqual(module["np"].allocations, []) + + def test_ordered_output_count_and_dimension_guards_remain_active(self) -> None: + for mismatch in ("count", "dimensions"): + _module, server, model = self.runtime() + encode = model.encode + + def invalid(texts: list, **kwargs: object) -> OrderArray: + values = encode(texts, **kwargs) + if mismatch == "count": + return OrderArray(values.values[:-1], (len(values) - 1, 3)) + if len(model.calls) == 2: + return OrderArray([row[:2] for row in values], (len(values), 2)) + return values + + model.encode = invalid + with self.assertRaisesRegex(ValueError, mismatch): + self.request(server, self.texts(65), batch_size=32) + self.assertFalse(server._inference_lock.locked()) + self.assertFalse(server._lock.locked()) + + def test_expiry_before_planning_and_after_each_sort_stops_encode(self) -> None: + for phase in ("entry", "model-load", "flatten-probe", "lengths", "global-sort", "inner-sort", "inverse-sort"): + with self.subTest(phase=phase): + module, server, model = self.runtime() + clock, numpy = module["time"], module["np"] + expires = clock.now + 1 + + def expire() -> None: + clock.now += 2 + + if phase == "entry": + expires = clock.now + elif phase == "model-load": + server._get_embed_model = lambda _tier, deadline=None: (expire(), model)[1] + elif phase == "flatten-probe": + model._can_flatten_inputs = lambda: (expire(), False)[1] + elif phase == "lengths": + model._input_length = lambda text: (expire(), len(text))[1] + else: + wanted = {"global-sort": 1, "inner-sort": 2, "inverse-sort": 3}[phase] + numpy.after_sort = lambda: expire() if len(numpy.sort_sizes) == wanted else None + response = self.request(server, self.texts(65), batch_size=32, deadline=expires) + self.assertFalse(response["ok"]) + self.assertIn("deadline", response["error"]) + self.assertEqual(model.calls, []) + self.assertEqual(numpy.sort_sizes, [] if phase in ("entry", "model-load", "flatten-probe", "lengths") + else [65, 32, 32][:wanted]) + self.assertFalse(server._inference_lock.locked()) + self.assertFalse(server._lock.locked()) + + def test_expiry_after_slice_does_not_plan_or_encode_another_slice(self) -> None: + module, server, model = self.runtime() + expires = module["time"].now + 1 + model.after_call = lambda: setattr(module["time"], "now", expires + 1) + response = self.request(server, self.texts(65), batch_size=32, deadline=expires) + self.assertFalse(response["ok"]) + self.assertEqual(len(model.calls), 1) + self.assertEqual(module["np"].sort_sizes, [65, 32, 32, 32]) + + def test_oom_retries_exact_slice_with_frozen_cursor_and_exclusive_owner(self) -> None: + for limit in (8, 32): + with self.subTest(limit=limit): + module, server, model = self.runtime() + texts = self.texts(65) + reference = self.reference if limit == 32 else self.contiguous_reference + expected, batches = reference(texts, limit) + model.fail_calls.add(2) + state_locks = [] + + def observe() -> None: + self.assertTrue(server._inference_lock.locked()) + state_locks.append(server._lock.locked()) + + model.after_call = observe + model.after_move = lambda: self.assertTrue(server._lock.locked() and server._inference_lock.locked()) + response = self.request(server, texts, batch_size=limit) + self.assertEqual(response["vectors"].values, expected.values) + self.assertEqual(model.native_batches, batches) + self.assertEqual(model.calls[1], model.calls[2]) + batches_count = (len(texts) + limit - 1) // limit + self.assertEqual(len(model.calls), batches_count + 1) + self.assertEqual(state_locks, [True, True, False] + [True] * (batches_count - 2)) + self.assertEqual(module["np"].sort_sizes.count(65), int(limit == 32)) + self.assertEqual(module["np"].allocations, [(65, 3)]) + self.assertEqual(model.moves, ["cpu"]) + self.assertEqual(server._sticky_cpu, {"embed"}) + + def test_expiry_on_oom_or_during_recovery_never_retries(self) -> None: + for limit in (8, 32): + for phase in ("oom", "flush", "move"): + with self.subTest(limit=limit, phase=phase): + module, server, model = self.runtime() + expires = module["time"].now + 1 + model.fail_calls.add(2) + + def expire() -> None: + module["time"].now = expires + 1 + + if phase == "oom": + model.after_call = lambda: expire() if len(model.calls) == 2 else None + elif phase == "flush": + module["mps"].flush_mps_cache = expire + else: + model.after_move = expire + response = self.request(server, self.texts(65), batch_size=limit, deadline=expires) + self.assertFalse(response["ok"]) + self.assertEqual(len(model.calls), 2) + self.assertEqual(model.moves, ["cpu"] if phase == "move" else []) + self.assertEqual(server._sticky_cpu, {"embed"}) + self.assertFalse(server._inference_lock.locked()) + self.assertFalse(server._lock.locked()) + + +class PairOrderNP(OrderNP): + def argsort(self, values: object) -> OrderArray: + keys = list(values) + self.sort_sizes.append(len(keys)) + groups: dict[int, list[int]] = {} + for index, key in enumerate(keys): + groups.setdefault(key, []).append(index) + order = [] + for key in sorted(groups): + group = groups[key] + order.extend(group[1:] + group[:1]) + self.after_sort() + return OrderArray(order) + + +class PairOrderModel: + """Predict's pinned ST 6.1 pair sorting and restoration, with synthetic scores. + + cross_encoder/model.py SHA256 c8688911838df2acd48550e80dce2743c1f31dd85187f98d688517af1ce575e8: + lines 695-736 resolve the prompt, sort raw pair lengths, preprocess batches, + and restore scores. Features and scores here expose membership and position. + """ + + def __init__(self, numpy: OrderNP) -> None: + self.numpy = numpy + self.calls: list[list] = [] + self.limits: list[int] = [] + self.native_batches: list[list[tuple[str, str]]] = [] + self.fail_calls: set[int] = set() + self.after_call: Callable[[], None] = lambda: None + self.default_prompt = "Synthetic instruction: " + + @staticmethod + def _input_length(pair: tuple | list) -> int: + return sum(len(str(text)) for text in pair) + + def _can_flatten_inputs(self) -> bool: + return False + + def predict(self, pairs: list, *, batch_size: int, show_progress_bar: bool) -> OrderArray: + assert show_progress_bar is False + self.calls.append(list(pairs)) + self.limits.append(batch_size) + self.after_call() + if len(self.calls) in self.fail_calls: + raise RuntimeError("MPS backend out of memory") + order = self.numpy.argsort([-PairOrderModel._input_length(pair) for pair in pairs]) + rows = [None] * len(pairs) + for start in range(0, len(pairs), batch_size): + indices = order[start:start + batch_size] + features = [(self.default_prompt + str(pairs[index][0]), str(pairs[index][1])) for index in indices] + self.native_batches.append(features) + fingerprint = sum((index + 1) * sum((offset + 1) * ord(char) for offset, char in enumerate(str(pair))) + for index, pair in enumerate(features)) + for position, index in enumerate(indices): + rows[index] = float(fingerprint * 1000 + position) + return OrderArray(rows, (len(pairs),)) + + +class TestModernBertGlobalOrder(unittest.TestCase): + identity = "Alibaba-NLP/gte-reranker-modernbert-base" + + def runtime(self) -> tuple[dict, object, PairOrderModel]: + module = load_order_runtime() + module["np"] = PairOrderNP() + server = object.__new__(module["ModelServer"]) + server._max_rss_bytes = 0 + model = PairOrderModel(module["np"]) + server._lock = threading.Lock() + server._inference_lock = threading.Lock() + server._activity_lock = threading.Lock() + server._inflight = 0 + server._transport_context = types.SimpleNamespace() + server._throttler_active, server._throttler = False, None + server._sticky_cpu = set() + server._reranker, server._reranker_name = None, None + server._mark_sticky_cpu = server._sticky_cpu.add + server.default_name = self.identity + server.loads, server.models = [], [] + server.after_load = lambda _model: None + + def load(name: str | None, deadline: object = None) -> PairOrderModel: + server.loads.append(name) + loaded = model if not server.models else PairOrderModel(module["np"]) + loaded.default_prompt = model.default_prompt + server.models.append(loaded) + server._reranker, server._reranker_name = loaded, name or server.default_name + server.after_load(loaded) + return loaded + + server._get_reranker = load + return module, server, model + + @staticmethod + def pairs(count: int) -> list[tuple[str, str]]: + samples = [("", ""), ("same", "same"), ("same", "same"), ("雪", "a"), + ("e\u0301", ""), ("q" * 100, "d"), ("q", "d" * 100), ("query", "doc")] + return [samples[(index * 3 + index // 8) % len(samples)] for index in range(count)] + + def request(self, server: object, pairs: list, **kwargs: object) -> dict: + return server.handle_request({"op": "rerank", "pairs": pairs, **kwargs}) + + def reference(self, pairs: list, *, prompt: str = "Synthetic instruction: ") -> PairOrderModel: + model = PairOrderModel(PairOrderNP()) + model.default_prompt = prompt + model.output = model.predict(pairs, batch_size=64, show_progress_bar=False) + return model + + def test_default_64_native_membership_cyclic_ties_duplicates_and_scatter(self) -> None: + for count in (63, 64, 65, 100, 127, 128, 129): + for prompt in ("", "Synthetic instruction: "): + with self.subTest(count=count, prompt=prompt): + module, server, model = self.runtime() + model.default_prompt = prompt + pairs = self.pairs(count) + expected = self.reference(pairs, prompt=prompt) + response = self.request(server, pairs) + self.assertTrue(response["ok"]) + self.assertEqual(response["scores"].values, expected.output.values) + self.assertEqual(model.native_batches, expected.native_batches) + self.assertEqual(model.limits, [64] * len(model.calls)) + self.assertEqual(module["np"].allocations, [(count,)]) + + def test_each_duplicate_occurrence_is_written_exactly_once(self) -> None: + from unittest.mock import patch + + for count in (65, 100, 129): + placements = [] + original = OrderArray.__setitem__ + + def store(array: OrderArray, key: object, values: OrderArray) -> None: + placements.extend(list(key) if isinstance(key, OrderArray) else list(range(len(array)))[key]) + original(array, key, values) + + _module, server, _model = self.runtime() + with patch.object(OrderArray, "__setitem__", store): + response = self.request(server, self.pairs(count)) + self.assertTrue(response["ok"]) + self.assertEqual(sorted(placements), list(range(count))) + + def test_negative_controls_reject_global_ties_inverse_scatter_and_document_only_lengths(self) -> None: + for mutation in ("global", "compensation", "inverse", "scatter", "document-length"): + with self.subTest(mutation=mutation): + module, server, model = self.runtime() + pairs = self.pairs(129) + expected = self.reference(pairs) + if mutation == "global": + server._rerank_global_order = lambda *_args: None + elif mutation == "compensation": + server._rerank_slice_indices = lambda _model, _pairs, order, offset, limit, _deadline: order[offset:offset + limit] + elif mutation == "inverse": + def wrong_inverse(model: object, pairs: list, order: OrderArray, offset: int, + limit: int, deadline: object) -> OrderArray: + deadline.check() + indices = order[offset:offset + limit] + inner = module["np"].argsort([-model._input_length(pairs[index]) for index in indices]) + return indices[inner] + server._rerank_slice_indices = wrong_inverse + elif mutation == "scatter": + store = module["_store_batch_result"] + module["_store_batch_result"] = lambda *args, **_kwargs: store(*args) + else: + model._input_length = lambda pair: len(pair[1]) + response = self.request(server, pairs) + self.assertTrue(response["ok"]) + self.assertNotEqual(response["scores"].values, expected.output.values) + if mutation != "scatter": + self.assertNotEqual(model.native_batches, expected.native_batches) + + def test_single_empty_one_batch_and_lower_caps_preserve_contiguous_calls(self) -> None: + for count, requested in ((0, 64), (1, 64), (8, 64), (64, 64), (129, 1), (129, 8), (129, 32)): + with self.subTest(count=count, requested=requested): + module, server, model = self.runtime() + pairs = self.pairs(count) + self.assertTrue(self.request(server, pairs, batch_size=requested)["ok"]) + self.assertEqual(model.calls, [pairs[i:i + requested] for i in range(0, count, requested)] or [[]]) + self.assertEqual(module["np"].sort_sizes, [len(call) for call in model.calls]) + + def test_unsupported_identity_interface_flattening_and_pairs_keep_old_path(self) -> None: + for mode in ("custom", "stale-cache", "legacy", "missing-flatten", "flattened", "three-strings", "non-string"): + with self.subTest(mode=mode): + module, server, model = self.runtime() + pairs = self.pairs(100) + if mode == "custom": + server.default_name = "synthetic-custom" + elif mode == "stale-cache": + server.after_load = lambda _model: setattr(server, "_reranker", object()) + elif mode == "legacy": + model._input_length = None + elif mode == "missing-flatten": + model._can_flatten_inputs = None + elif mode == "flattened": + model._can_flatten_inputs = lambda: True + elif mode == "three-strings": + pairs[5] = ("q", "d", "extra") + else: + pairs[5] = ("q", 17) + self.assertTrue(self.request(server, pairs)["ok"]) + self.assertEqual(model.calls, [pairs[:64], pairs[64:]]) + self.assertEqual(module["np"].sort_sizes, [64, 36]) + + def test_caller_server_and_once_per_request_adaptive_limits(self) -> None: + for requested, samples, effective in ((8, [128], 8), (128, [128], 64), (64, [8, 64], 8), (128, [64, 1], 64)): + module, server, model = self.runtime() + throttler = RecordingThrottler(samples) + server._throttler, server._throttler_active = throttler, True + pairs = self.pairs(129) + self.assertTrue(self.request(server, pairs, batch_size=requested)["ok"]) + self.assertEqual(model.limits, [effective] * len(model.calls)) + self.assertEqual(throttler.before_count, 1) + self.assertEqual(throttler.after_counts, [129]) + if effective == 64: + self.assertEqual(model.native_batches, self.reference(pairs).native_batches) + else: + self.assertEqual(model.calls, [pairs[i:i + effective] for i in range(0, len(pairs), effective)]) + + def test_result_preflight_precedes_loading_and_ordering(self) -> None: + module, server, model = self.runtime() + module["_MAX_MESSAGE_SIZE"] = 200 + response = self.request(server, self.pairs(129)) + self.assertEqual(response["error_code"], "result_too_large") + self.assertEqual(server.loads, []) + self.assertEqual(model.calls, []) + self.assertEqual(module["np"].sort_sizes, []) + self.assertEqual(module["np"].allocations, []) + + def test_deadlines_before_work_and_after_each_sort_stop_prediction(self) -> None: + for phase in ("entry", "load", "flatten", "length", "global", "inner", "inverse"): + with self.subTest(phase=phase): + module, server, model = self.runtime() + clock, numpy = module["time"], module["np"] + expires = clock.now + 1 + + def expire() -> None: + clock.now = expires + 1 + + if phase == "entry": + expires = clock.now + elif phase == "load": + server.after_load = lambda _model: expire() + elif phase == "flatten": + model._can_flatten_inputs = lambda: (expire(), False)[1] + elif phase == "length": + model._input_length = lambda pair: (expire(), PairOrderModel._input_length(pair))[1] + else: + wanted = {"global": 1, "inner": 2, "inverse": 3}[phase] + numpy.after_sort = lambda: expire() if len(numpy.sort_sizes) == wanted else None + response = self.request(server, self.pairs(129), deadline=expires) + self.assertFalse(response["ok"]) + self.assertIn("deadline", response["error"]) + self.assertEqual(model.calls, []) + self.assertEqual(numpy.sort_sizes, [] if phase in ("entry", "load", "flatten", "length") else [129, 64, 64][:wanted]) + self.assertFalse(server._inference_lock.locked()) + self.assertFalse(server._lock.locked()) + + def test_expiry_after_first_slice_stops_following_work(self) -> None: + module, server, model = self.runtime() + expires = module["time"].now + 1 + model.after_call = lambda: setattr(module["time"], "now", expires + 1) + response = self.request(server, self.pairs(129), deadline=expires) + self.assertFalse(response["ok"]) + self.assertEqual(len(model.calls), 1) + self.assertEqual(module["np"].sort_sizes, [129, 64, 64, 64]) + + def test_later_oom_retries_frozen_slice_with_captured_omitted_identity(self) -> None: + module, server, model = self.runtime() + pairs = self.pairs(129) + expected = self.reference(pairs) + model.fail_calls.add(2) + locks = [] + + def observe() -> None: + self.assertTrue(server._inference_lock.locked()) + locks.append(server._lock.locked()) + if len(model.calls) == 2: + server.default_name = "synthetic-different-reranker" + + model.after_call = observe + + def loaded(replacement: PairOrderModel) -> None: + self.assertTrue(server._inference_lock.locked() and server._lock.locked()) + if replacement is not model: + replacement.after_call = observe + + server.after_load = loaded + response = self.request(server, pairs) + replacement = server.models[1] + self.assertEqual(server.loads, [None, self.identity]) + self.assertEqual(server._reranker_name, self.identity) + self.assertEqual(model.calls[1], replacement.calls[0]) + self.assertEqual(model.native_batches + replacement.native_batches, expected.native_batches) + self.assertEqual(response["scores"].values, expected.output.values) + self.assertEqual(locks, [True, True, False, True]) + self.assertEqual(module["np"].sort_sizes.count(129), 1) + self.assertEqual(module["np"].allocations, [(129,)]) + self.assertEqual(server._sticky_cpu, {"rerank"}) + + def test_lower_limit_recovery_keeps_existing_resolution_behavior(self) -> None: + _module, server, model = self.runtime() + model.fail_calls.add(2) + model.after_call = lambda: setattr(server, "default_name", "synthetic-other") if len(model.calls) == 2 else None + self.assertTrue(self.request(server, self.pairs(17), batch_size=8)["ok"]) + self.assertEqual(server.loads, [None, None]) + self.assertEqual(server._reranker_name, "synthetic-other") + + def test_unplanned_success_and_retry_keep_positional_result_writer(self) -> None: + for count, requested, failure in ((17, 8, None), (17, 8, 2), (64, None, None), (64, None, 1)): + with self.subTest(count=count, requested=requested, failure=failure): + module, server, model = self.runtime() + pairs = self.pairs(count) + limit = requested or 64 + reference = PairOrderModel(PairOrderNP()) + expected = [] + for offset in range(0, count, limit): + expected.extend(reference.predict(pairs[offset:offset + limit], batch_size=limit, + show_progress_bar=False).values) + writes = [] + store = module["_store_batch_result"] + + def positional_store(result: object, values: object, *args: object) -> OrderArray: + writes.append((args[0], args[1])) + return store(result, values, *args) + + module["_store_batch_result"] = positional_store + if failure is not None: + model.fail_calls.add(failure) + response = self.request(server, pairs, **({} if requested is None else {"batch_size": requested})) + self.assertTrue(response["ok"]) + self.assertEqual(response["scores"].values, expected) + self.assertEqual(writes, [(offset, min(limit, count - offset)) for offset in range(0, count, limit)]) + self.assertEqual([batch for loaded in server.models for batch in loaded.native_batches], + reference.native_batches) + self.assertEqual(module["np"].allocations, [(count,)]) + if failure is not None: + self.assertEqual(model.calls[failure - 1], server.models[1].calls[0]) + self.assertFalse(server._inference_lock.locked()) + self.assertFalse(server._lock.locked()) + + def test_deadline_on_oom_or_during_replacement_prevents_retry(self) -> None: + for phase in ("oom", "flush", "replacement"): + module, server, model = self.runtime() + expires = module["time"].now + 1 + model.fail_calls.add(2) + + def expire() -> None: + module["time"].now = expires + 1 + + if phase == "oom": + model.after_call = lambda: expire() if len(model.calls) == 2 else None + elif phase == "flush": + module["mps"].flush_mps_cache = expire + else: + server.after_load = lambda replacement: expire() if replacement is not model else None + response = self.request(server, self.pairs(129), deadline=expires) + self.assertFalse(response["ok"]) + self.assertEqual(len(model.calls), 2) + self.assertTrue(all(not replacement.calls for replacement in server.models[1:])) + self.assertEqual(server.loads, [None, self.identity] if phase == "replacement" else [None]) + self.assertFalse(server._inference_lock.locked()) + self.assertFalse(server._lock.locked()) + + def test_ordered_score_count_and_shape_validation_remain_active(self) -> None: + for mismatch in ("count", "dimensions"): + _module, server, model = self.runtime() + predict = model.predict + + def invalid(pairs: list, **kwargs: object) -> OrderArray: + result = predict(pairs, **kwargs) + if mismatch == "count": + return OrderArray(result.values[:-1]) + if len(model.calls) == 2: + return OrderArray([[value, value] for value in result]) + return result + + model.predict = invalid + with self.assertRaisesRegex(ValueError, mismatch): + self.request(server, self.pairs(129)) + self.assertFalse(server._inference_lock.locked()) + self.assertFalse(server._lock.locked()) diff --git a/tests/test-model-process-admission-297.py b/tests/test-model-process-admission-297.py new file mode 100644 index 00000000..afe380d8 --- /dev/null +++ b/tests/test-model-process-admission-297.py @@ -0,0 +1,744 @@ +"""Current-RSS stage admission with synthetic models and stdlib-only sensors.""" +from __future__ import annotations + +import ast +import importlib.util +import os +import sys +import threading +import types +import unittest +import weakref +from collections.abc import Callable +from pathlib import Path +from unittest.mock import Mock, patch + + +SOURCE = Path(__file__).resolve().parents[1] / "truememory" / "model_server.py" +SOURCE_TEXT: str | None = None +SUPPORT_PATH = Path(__file__).with_name("test-model-allocation-preflight-297.py") +SPEC = importlib.util.spec_from_file_location("process_admission_array_support", SUPPORT_PATH) +SUPPORT = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(SUPPORT) +BUDGET = 1048576 + + +def load_server() -> types.ModuleType: + tree = ast.parse(SOURCE_TEXT if SOURCE_TEXT is not None else SOURCE.read_text(encoding="utf-8")) + tree.body = [node for node in tree.body if not ( + isinstance(node, ast.Import) and any(alias.name == "numpy" for alias in node.names) + or isinstance(node, ast.ImportFrom) and (node.module or "").startswith("truememory.") + or isinstance(node, ast.Try) and any( + isinstance(child, ast.Import) and any(alias.name == "psutil" for alias in child.names) + for child in node.body + ) + or isinstance(node, ast.Expr) and isinstance(node.value, ast.Call) + and isinstance(node.value.func, ast.Name) and node.value.func.id == "_set_mps_memory_cap" + )] + module = types.ModuleType("synthetic_process_admission_server") + module.__dict__.update({ + "np": SUPPORT.Numpy(), "psutil": None, "_USE_UNIX": True, "_LOOPBACK_HOST": "127.0.0.1", + "_env_int": lambda name, default, **kwargs: default, "pid_is_alive": lambda pid: False, + }) + with patch.dict(sys.modules, {module.__name__: module}): + exec(compile(ast.fix_missing_locations(tree), str(SOURCE), "exec"), module.__dict__) + module.log.disabled = True + return module + + +class Model: + def __init__(self, owner: TestProcessAdmission, identity: str, device: str) -> None: + self.owner, self.identity, self.device = owner, identity, device + + def encode(self, texts: list, **kwargs: object) -> list: + self.owner.calls.append(("embed", self.device, list(texts), kwargs)) + self.owner.inference_hook(self) + return [[float(text), 0.5] for text in texts] + + def predict(self, pairs: list, **kwargs: object) -> list: + self.owner.calls.append(("rerank", self.device, list(pairs), kwargs)) + self.owner.inference_hook(self) + return [float(pair[1]) for pair in pairs] + + def to(self, device: str) -> None: + self.owner.assertTrue(self.owner.server._inference_lock.locked()) + self.owner.assertTrue(self.owner.server._lock.locked()) + self.owner.moves.append(device) + self.device = device + self.owner.transfer_hook() + + +class TestProcessAdmission(unittest.TestCase): + def setUp(self) -> None: + package = types.ModuleType("truememory") + package.__path__ = [] + vector = types.ModuleType("truememory.vector_search") + vector.EMBEDDING_MODEL = "model2vec" + vector._TIER_ALIASES = {"edge": "model2vec", "base": "qwen3_256"} + reranker = types.ModuleType("truememory.reranker") + reranker.get_current_reranker_name = lambda: "rank-a" + self.mps = types.ModuleType("truememory.mps_utils") + self.mps.auto_detect_device = lambda: "mps" + self.mps.resolve_device = lambda device: device or "mps" + self.mps.ensure_mps_memory_budget = lambda device: None + self.mps.is_mps_oom = lambda error: "MPS backend out of memory" in str(error) + self.mps.flush_mps_cache = Mock() + transformers = types.ModuleType("sentence_transformers") + transformers.CrossEncoder = lambda identity, device: self.build(identity, device) + self.modules = patch.dict(sys.modules, { + "truememory": package, "truememory.vector_search": vector, + "truememory.reranker": reranker, "truememory.mps_utils": self.mps, + "sentence_transformers": transformers, + }) + self.modules.start() + self.addCleanup(self.modules.stop) + self.environment = patch.dict(os.environ, {"TRUEMEMORY_MODEL_SERVER_MAX_RSS_MB": "1"}) + self.environment.start() + self.addCleanup(self.environment.stop) + self.ms = load_server() + self.server = self.ms.ModelServer() + self.addCleanup(self.server._workers.shutdown, wait=True, cancel_futures=True) + self.server._write_status_file = lambda: None + self.server._build_embed_model = self.build + self.rss: object = BUDGET - 1 + self.readings: list[object] = [] + self.measurement_owners: list[tuple[bool, bool]] = [] + self.sensor = Mock(side_effect=self.measure) + self.ms.psutil = types.SimpleNamespace(Process=lambda: types.SimpleNamespace(memory_info=self.sensor)) + self.builds: list[tuple[str, str]] = [] + self.calls: list[tuple] = [] + self.moves: list[str] = [] + self.refs: list[weakref.ReferenceType] = [] + self.build_hook: Callable[[Model], None] = lambda model: None + self.inference_hook: Callable[[Model], None] = lambda model: None + self.transfer_hook: Callable[[], None] = lambda: None + + def tearDown(self) -> None: + self.assertFalse(self.server._lock.locked()) + self.assertFalse(self.server._inference_lock.locked()) + self.assertFalse(self.server._fast_lock.locked()) + self.assertEqual(self.server._inflight, 0) + self.assertTrue(all(owned and not metadata for owned, metadata in self.measurement_owners)) + + def measure(self) -> types.SimpleNamespace: + self.measurement_owners.append(( + self.server._inference_lock.locked() or self.server._fast_lock.locked(), + self.server._residency_lock.locked(), + )) + self.readings.append(self.rss) + return types.SimpleNamespace(rss=self.rss) + + def build(self, identity: str, device: str) -> Model: + self.assertTrue(self.server._inference_lock.locked() or self.server._fast_lock.locked()) + self.builds.append((identity, device)) + model = Model(self, identity, device) + self.refs.append(weakref.ref(model)) + self.build_hook(model) + return model + + def request(self, op: str = "embed", count: int = 3, **options: object) -> dict: + request = { + "op": op, "tier": "edge", "model_name": "rank-a", "batch_size": 2, + "texts": [str(index) for index in range(count)], + "pairs": [("query", str(index)) for index in range(count)], + } + request.update(options) + return self.server.handle_request(request) + + def assert_busy(self, response: dict) -> None: + self.assertFalse(response["ok"], response) + self.assertEqual(response["error_code"], "server_busy") + self.assertEqual(response["retry_after_ms"], 250) + self.assertNotIn("vectors", response) + self.assertNotIn("scores", response) + + def clear_models(self) -> None: + self.server._embed_state = None + self.server._reranker = None + self.server._reranker_name = None + self.server._fast_encoder = None + self.server._fast_model_id = None + self.server._fast_generation = None + self.builds.clear() + self.calls.clear() + self.server._embed_timestamps.clear() + + def test_setting_uses_mib_and_rejects_malformed_values(self) -> None: + self.assertEqual(self.server._max_rss_bytes, 1 * 1048576) + for raw in ("-1", "1.0", "nan", "inf", "true", "", " 1", "١", str(2**31)): + with self.subTest(raw=raw), patch.dict(os.environ, {"TRUEMEMORY_MODEL_SERVER_MAX_RSS_MB": raw}): + with self.assertRaises(ValueError): + self.ms.ModelServer() + + def test_cold_constructor_boundary_all_main_paths(self) -> None: + for op, count in (("embed", 1), ("embed_batched", 3), ("rerank", 3), ("rerank_batched", 3)): + for usage in (BUDGET - 1, BUDGET, BUDGET + 1): + with self.subTest(op=op, count=count, usage=usage): + self.clear_models() + self.rss = usage + response = self.request(op, count) + if usage < BUDGET: + self.assertTrue(response["ok"]) + self.assertEqual(len(self.builds), 1) + else: + self.assert_busy(response) + self.assertEqual(self.builds, []) + self.assertEqual(self.calls, []) + + def test_cached_slice_boundary_all_main_paths(self) -> None: + for op, count in (("embed", 1), ("embed", 3), ("rerank", 3)): + with self.subTest(op=op, count=count): + self.clear_models() + self.rss = BUDGET - 1 + self.assertTrue(self.request(op, count)["ok"]) + built = len(self.builds) + self.calls.clear() + for usage in (BUDGET, BUDGET + 1): + self.rss = usage + self.assert_busy(self.request(op, count)) + self.assertEqual(self.calls, []) + self.assertEqual(len(self.builds), built) + + def test_invalid_and_missing_measurements_refuse_before_construction(self) -> None: + for invalid in (None, True, False, 0, -1, "1048575", 1.0, float("nan"), float("inf")): + with self.subTest(invalid=invalid): + # Each sensor case is independent of sustained-workload policy. + self.server._embed_timestamps.clear() + self.rss = invalid + self.assert_busy(self.request()) + self.assertEqual(self.builds, []) + for error in (OSError("synthetic sensor failure"), RuntimeError("synthetic sensor failure")): + with self.subTest(error=type(error).__name__): + self.server._embed_timestamps.clear() + self.sensor.side_effect = error + self.assert_busy(self.request()) + self.assertEqual(self.builds, []) + self.ms.psutil = None + self.assert_busy(self.request()) + self.assertEqual(self.builds, []) + + def test_constructor_crossing_refuses_before_publication_or_inference(self) -> None: + for op, count in (("embed", 1), ("embed", 3), ("rerank", 3)): + with self.subTest(op=op, count=count): + self.clear_models() + self.rss = BUDGET - 1 + self.build_hook = lambda model: setattr(self, "rss", BUDGET) + self.assert_busy(self.request(op, count)) + self.assertEqual(len(self.builds), 1) + self.assertEqual(self.calls, []) + self.assertIsNone(self.server._embed_state) + self.assertIsNone(self.server._reranker) + self.assertIsNone(self.refs[-1]()) + + def test_slice_crossing_never_returns_partial_output(self) -> None: + for op in ("embed", "rerank"): + with self.subTest(op=op): + self.clear_models() + self.rss = BUDGET - 1 + self.inference_hook = lambda model: setattr(self, "rss", BUDGET) + self.assert_busy(self.request(op, count=5)) + self.assertEqual(len(self.calls), 1) + self.assertEqual(len(self.calls[0][2]), 2) + + def test_fast_constructor_and_cache_refusal_never_fall_through_to_main(self) -> None: + for cached in (False, True): + with self.subTest(cached=cached): + self.clear_models() + self.rss = BUDGET - 1 + self.assertTrue(self.request()["ok"]) + with self.server._inference_lock: + if cached: + self.assertTrue(self.request(count=1)["ok"]) + self.rss = BUDGET + before_builds, before_calls = len(self.builds), len(self.calls) + with patch.object(self.server, "_request_batch_limit", side_effect=AssertionError("Main fallback")): + self.assert_busy(self.request(count=1)) + self.assertEqual(len(self.builds), before_builds) + self.assertEqual(len(self.calls), before_calls) + + def test_fast_constructor_crossing_drops_unpublished_clone(self) -> None: + self.assertTrue(self.request()["ok"]) + self.build_hook = lambda model: setattr(self, "rss", BUDGET) + with self.server._inference_lock: + with patch.object(self.server, "_request_batch_limit", side_effect=AssertionError("Main fallback")): + self.assert_busy(self.request(count=1)) + self.assertIsNone(self.server._fast_encoder) + self.assertIsNone(self.refs[-1]()) + self.assertEqual(len(self.calls), 2) + + def fail_mps(self, model: Model) -> None: + if model.device == "mps": + raise RuntimeError("MPS backend out of memory") + + def test_embed_refused_transfer_is_sticky_and_invalidates_failed_cache(self) -> None: + for count in (1, 3): + with self.subTest(count=count): + self.clear_models() + self.server._sticky_cpu.clear() + self.rss = BUDGET - 1 + + def fail(model: Model) -> None: + if model.device == "mps": + self.rss = BUDGET + self.fail_mps(model) + + self.inference_hook = fail + self.assert_busy(self.request(count=count)) + self.assertIn("embed", self.server._sticky_cpu) + self.assertIsNone(self.server._embed_state) + self.assertIsNone(self.refs[-1]()) + self.assertEqual(self.moves, []) + self.rss = BUDGET - 1 + self.assertTrue(self.request(count=count)["ok"]) + self.assertEqual(self.builds[-1], ("model2vec", "cpu")) + + def test_embed_transfer_crossing_refuses_cpu_retry(self) -> None: + for count in (1, 3): + with self.subTest(count=count): + self.clear_models() + self.server._sticky_cpu.clear() + self.moves.clear() + self.rss = BUDGET - 1 + self.inference_hook = self.fail_mps + self.transfer_hook = lambda: setattr(self, "rss", BUDGET) + self.assert_busy(self.request(count=count)) + self.assertIn("embed", self.server._sticky_cpu) + self.assertEqual(self.moves, ["cpu"]) + self.assertEqual(len(self.calls), 1) + self.assertEqual(self.server._embed_state.model.device, "cpu") + + def test_rerank_refused_replacement_is_sticky_and_releases_failed_model(self) -> None: + def fail(model: Model) -> None: + if model.device == "mps": + self.rss = BUDGET + self.fail_mps(model) + + self.inference_hook = fail + self.assert_busy(self.request("rerank")) + self.assertIn("rerank", self.server._sticky_cpu) + self.assertEqual(len(self.builds), 1) + self.assertIsNone(self.server._reranker) + self.assertIsNone(self.server._reranker_name) + self.assertIsNone(self.refs[-1]()) + self.rss = BUDGET - 1 + self.assertTrue(self.request("rerank")["ok"]) + self.assertEqual(self.builds[-1], ("rank-a", "cpu")) + + def test_rerank_cpu_constructor_crossing_refuses_retry(self) -> None: + self.inference_hook = self.fail_mps + + def constructed(model: Model) -> None: + if model.device == "cpu": + self.rss = BUDGET + + self.build_hook = constructed + self.assert_busy(self.request("rerank")) + self.assertEqual(self.builds, [("rank-a", "mps"), ("rank-a", "cpu")]) + self.assertEqual(len(self.calls), 1) + self.assertIsNone(self.server._reranker) + self.assertTrue(all(ref() is None for ref in self.refs)) + + def test_rerank_retry_samples_after_constructor_admission(self) -> None: + self.inference_hook = self.fail_mps + original = self.server._get_reranker + + def loaded(name: str, deadline: object = None) -> Model: + model = original(name, deadline=deadline) + if model.device == "cpu": + self.rss = BUDGET + return model + + self.server._get_reranker = loaded + self.assert_busy(self.request("rerank")) + self.assertEqual(len(self.builds), 2) + self.assertEqual(len(self.calls), 1) + + def test_admitted_recovery_retains_complete_order_and_batch_controls(self) -> None: + for op in ("embed", "rerank"): + with self.subTest(op=op): + self.clear_models() + self.server._sticky_cpu.clear() + self.inference_hook = self.fail_mps + response = self.request(op, count=5) + self.assertTrue(response["ok"]) + self.assertEqual([len(call[2]) for call in self.calls], [2, 2, 2, 1]) + self.assertTrue(all(call[3] == {"batch_size": 2, "show_progress_bar": False} + for call in self.calls)) + actual = response["vectors" if op == "embed" else "scores"] + self.assertEqual(actual.dtype, "float32") + self.assertEqual(actual.data, [0.0, 0.5, 1.0, 0.5, 2.0, 0.5, 3.0, 0.5, 4.0, 0.5] + if op == "embed" else [0.0, 1.0, 2.0, 3.0, 4.0]) + + def test_current_usage_can_fall_after_historical_peak(self) -> None: + resource = types.ModuleType("resource") + resource.getrusage = Mock(return_value=types.SimpleNamespace(ru_maxrss=10 * BUDGET)) + with patch.dict(sys.modules, {"resource": resource}): + self.rss = BUDGET + 1 + self.assert_busy(self.request()) + self.rss = BUDGET - 1 + self.assertTrue(self.request()["ok"]) + resource.getrusage.assert_not_called() + self.assertIn(BUDGET + 1, self.readings) + self.assertIn(BUDGET - 1, self.readings) + + def test_disabled_budget_performs_zero_sensor_reads_including_recovery(self) -> None: + self.server._max_rss_bytes = 0 + self.ms.psutil = types.SimpleNamespace(Process=Mock(side_effect=AssertionError("Disabled sensor"))) + self.inference_hook = self.fail_mps + self.assertTrue(self.request()["ok"]) + self.assertTrue(self.request("rerank")["ok"]) + with self.server._inference_lock: + self.assertTrue(self.request(count=1)["ok"]) + self.assertEqual(self.readings, []) + self.ms.psutil.Process.assert_not_called() + + def test_default_budget_is_disabled(self) -> None: + with patch.dict(os.environ): + os.environ.pop("TRUEMEMORY_MODEL_SERVER_MAX_RSS_MB", None) + server = self.ms.ModelServer() + try: + self.assertEqual(server._max_rss_bytes, 0) + finally: + server._workers.shutdown(wait=True, cancel_futures=True) + + def test_replacement_releases_obsolete_model_before_measuring(self) -> None: + self.assertTrue(self.request()["ok"]) + old = self.refs[-1] + + def measurement() -> types.SimpleNamespace: + self.assertIsNone(old(), "Old residency retained at admission") + return types.SimpleNamespace(rss=BUDGET - 1) + + self.sensor.side_effect = measurement + self.assertTrue(self.request(tier="base")["ok"]) + + def test_expired_request_never_samples_or_loads(self) -> None: + response = self.request(deadline=1) + self.assertFalse(response["ok"]) + self.assertIn("deadline", response["error"]) + self.assertEqual(self.readings, []) + self.assertEqual(self.builds, []) + + def test_sensor_time_expiry_prevents_all_five_constructor_routes(self) -> None: + routes = (("embed", 1, "cold"), ("embed", 3, "cold"), ("rerank", 3, "cold"), + ("embed", 1, "fast"), ("rerank", 3, "recovery")) + for op, count, route in routes: + for outcome in ("under", "equal", "over", "invalid", "error"): + with self.subTest(op=op, count=count, route=route, outcome=outcome): + self.clear_models() + self.server._sticky_cpu.clear() + self.rss = BUDGET - 1 + self.sensor.side_effect = self.measure + self.inference_hook = self.fail_mps if route == "recovery" else lambda model: None + if route == "fast": + self.assertTrue(self.request()["ok"]) + self.calls.clear() + self.builds.clear() + tick, expired_samples = [1000.0], [] + clock = types.SimpleNamespace(time=lambda: tick[0], monotonic=lambda: tick[0]) + + def measure() -> types.SimpleNamespace: + value = self.measure() + if route != "recovery" or "rerank" in self.server._sticky_cpu: + expired_samples.append(value) + tick[0] = 1002.0 + if outcome == "error": + raise OSError("synthetic expired RSS read") + value.rss = {"under": BUDGET - 1, "equal": BUDGET, + "over": BUDGET + 1, "invalid": None}[outcome] + return value + + self.sensor.side_effect = measure + with patch.object(self.ms, "time", clock): + if route == "fast": + with self.server._inference_lock, patch.object( + self.server, "_request_batch_limit", side_effect=AssertionError("Main fallback"), + ): + response = self.request(op, count, deadline=1001.0) + else: + response = self.request(op, count, deadline=1001.0) + self.assertFalse(response["ok"]) + self.assertIn("deadline", response["error"]) + self.assertNotIn("error_code", response) + self.assertEqual(len(expired_samples), 1) + self.assertEqual(self.builds, [("rank-a", "mps")] if route == "recovery" else []) + self.assertEqual(len(self.calls), 1 if route == "recovery" else 0) + if route == "recovery": + self.assertIn("rerank", self.server._sticky_cpu) + self.assertIsNone(self.server._reranker) + self.assertIsNone(self.server._reranker_name) + self.assertIsNone(self.refs[-1]()) + if route == "fast": + self.assertIsNone(self.server._fast_encoder) + self.assertFalse(self.server._lock.locked()) + self.assertFalse(self.server._inference_lock.locked()) + self.assertFalse(self.server._fast_lock.locked()) + self.assertEqual(self.server._inflight, 0) + + def test_sensor_expiry_after_construction_cannot_publish_or_infer(self) -> None: + for op, count, fast in (("embed", 1, False), ("embed", 3, False), + ("rerank", 3, False), ("embed", 1, True)): + with self.subTest(op=op, count=count, fast=fast): + self.clear_models() + self.sensor.side_effect = self.measure + if fast: + self.assertTrue(self.request()["ok"]) + self.calls.clear() + self.builds.clear() + tick = [1000.0] + clock = types.SimpleNamespace(time=lambda: tick[0], monotonic=lambda: tick[0]) + + def measure() -> types.SimpleNamespace: + value = self.measure() + if self.builds: + tick[0] = 1002.0 + return value + + self.sensor.side_effect = measure + with patch.object(self.ms, "time", clock): + if fast: + with self.server._inference_lock: + response = self.request(op, count, deadline=1001.0) + else: + response = self.request(op, count, deadline=1001.0) + self.assertIn("deadline", response["error"]) + self.assertEqual(len(self.builds), 1) + self.assertEqual(self.calls, []) + self.assertIsNone(self.refs[-1]()) + self.assertIsNone(self.server._fast_encoder) + if not fast: + self.assertIsNone(self.server._embed_state) + self.assertIsNone(self.server._reranker) + + def test_embed_transfer_sensor_expiry_invalidates_failed_accelerator(self) -> None: + for count in (1, 3): + with self.subTest(count=count): + self.clear_models() + self.server._sticky_cpu.clear() + tick = [1000.0] + clock = types.SimpleNamespace(time=lambda: tick[0], monotonic=lambda: tick[0]) + + def measure() -> types.SimpleNamespace: + value = self.measure() + if "embed" in self.server._sticky_cpu: + tick[0] = 1002.0 + return value + + self.sensor.side_effect = measure + self.inference_hook = self.fail_mps + with patch.object(self.ms, "time", clock): + response = self.request(count=count, deadline=1001.0) + self.assertIn("deadline", response["error"]) + self.assertIn("embed", self.server._sticky_cpu) + self.assertEqual(len(self.calls), 1) + self.assertEqual(self.moves, []) + self.assertIsNone(self.server._embed_state) + self.assertIsNone(self.refs[-1]()) + + def test_disabled_admission_retains_deadline_checks_without_sensor_reads(self) -> None: + self.server._max_rss_bytes = 0 + self.ms.psutil = types.SimpleNamespace(Process=Mock(side_effect=AssertionError("Disabled sensor"))) + for op, count in (("embed", 1), ("embed", 3), ("rerank", 3)): + with self.subTest(op=op, count=count): + self.clear_models() + tick = [1000.0] + clock = types.SimpleNamespace(time=lambda: tick[0], monotonic=lambda: tick[0]) + + def expire_device(device: str | None) -> str: + tick[0] = 1002.0 + return "mps" + + with patch.object(self.ms, "time", clock), patch.object(self.mps, "resolve_device", expire_device): + response = self.request(op, count, deadline=1001.0) + self.assertIn("deadline", response["error"]) + self.assertEqual(self.builds, []) + self.assertEqual(self.calls, []) + self.ms.psutil.Process.assert_not_called() + self.assertEqual(self.readings, []) + + def test_invalid_sensor_cases_do_not_activate_unrelated_throttler(self) -> None: + with patch.object(self.server, "_activate_throttler", side_effect=AssertionError("Unrelated throttler")): + self.test_invalid_and_missing_measurements_refuse_before_construction() + + def assert_failed_transfer(self, failure: BaseException, count: int) -> None: + responses = [] + try: + responses.append(self.request(count=count, tier="base")) + except BaseException as caught: + self.assertIs(caught, failure) + else: + self.fail("A failed transfer returned a response") + self.assertEqual(responses, []) + self.assertIn("embed", self.server._sticky_cpu) + self.assertEqual(self.moves, ["cpu"]) + self.assertFalse(self.server._lock.locked()) + self.assertFalse(self.server._inference_lock.locked()) + + def test_failed_cpu_transfer_drops_cache_and_next_request_rebuilds_same_identity(self) -> None: + for count, fail_at in ((1, 1), (5, 2)): + for error_type in (RuntimeError, MemoryError, ValueError, KeyboardInterrupt, SystemExit): + with self.subTest(count=count, error=error_type.__name__): + self.clear_models() + self.server._sticky_cpu.clear() + self.moves.clear() + failure = error_type("synthetic failed CPU transfer") + workspaces = [] + + def fail_inference(model: Model) -> None: + if len(self.calls) == fail_at: + self.fail_mps(model) + + def fail_transfer(model: Model, device: str) -> None: + self.assertEqual(model.identity, "qwen3_256") + self.assertEqual(model.device, "mps") + self.assertTrue(self.server._inference_lock.locked()) + self.assertTrue(self.server._lock.locked()) + self.moves.append(device) + workspace = SUPPORT.Workspace() + workspaces.append(weakref.ref(workspace)) + raise failure + + self.inference_hook = fail_inference + with patch.object(Model, "to", fail_transfer): + self.assert_failed_transfer(failure, count) + self.assertIsNone(self.server._embed_state) + self.assertEqual(len(self.calls), fail_at) + self.assertEqual(self.builds, [("qwen3_256", "mps")]) + self.assertEqual(self.server._inflight, 0) + self.assertFalse(self.server._fast_lock.locked()) + self.assertIsNotNone(workspaces[0]()) + self.assertIsNotNone(self.refs[-1]()) + failure.__traceback__ = None + self.assertIsNone(workspaces[0]()) + self.assertIsNone(self.refs[-1]()) + + self.inference_hook = self.fail_mps + response = self.request(count=count, tier="base") + self.assertTrue(response["ok"]) + self.assertEqual(self.builds, [("qwen3_256", "mps"), ("qwen3_256", "cpu")]) + self.assertEqual(self.moves, ["cpu"]) + self.assertEqual(response["vectors"].data, + [value for index in range(count) for value in (float(index), 0.5)]) + self.assertEqual(response["vectors"].dtype, "float32") + + def test_failed_transfer_preserves_an_unrelated_replacement_snapshot(self) -> None: + for count in (1, 3): + with self.subTest(count=count): + self.clear_models() + self.server._sticky_cpu.clear() + self.moves.clear() + failure = ValueError("synthetic transfer failure after replacement") + replacement = Model(self, "qwen3_256", "cpu") + replacement_state = self.ms._EmbedState(replacement, "base", "qwen3_256") + + def fail_transfer(model: Model, device: str) -> None: + self.moves.append(device) + self.server._publish_embed_state(replacement_state) + raise failure + + self.inference_hook = self.fail_mps + with patch.object(Model, "to", fail_transfer): + self.assert_failed_transfer(failure, count) + self.assertIs(self.server._embed_state, replacement_state) + failed = self.refs[-1] + failure.__traceback__ = None + self.assertIsNone(failed()) + self.assertTrue(self.request(count=count, tier="base")["ok"]) + self.assertEqual(self.builds, [("qwen3_256", "mps")]) + self.assertIs(self.server._embed_state.model, replacement) + + def test_failed_transfer_retires_idle_fast_clone(self) -> None: + self.assertTrue(self.request(tier="base")["ok"]) + main = self.refs[-1] + with self.server._inference_lock: + self.assertTrue(self.request(count=1, tier="base")["ok"]) + clone = self.refs[-1] + self.assertEqual(self.builds, [("qwen3_256", "mps"), ("qwen3_256", "cpu")]) + failure = MemoryError("synthetic transfer allocation failure") + + def fail_transfer(model: Model, device: str) -> None: + self.moves.append(device) + raise failure + + self.inference_hook = self.fail_mps + with patch.object(Model, "to", fail_transfer): + self.assert_failed_transfer(failure, 3) + self.assertIsNone(self.server._embed_state) + self.assertIsNone(self.server._fast_encoder) + self.assertIsNone(self.server._fast_model_id) + self.assertIsNone(self.server._fast_generation) + self.assertIsNone(clone()) + failure.__traceback__ = None + self.assertIsNone(main()) + + def test_failed_transfer_allows_active_fast_work_to_finish_without_stale_publication(self) -> None: + for phase in ("construction", "inference"): + with self.subTest(phase=phase): + self.clear_models() + self.server._sticky_cpu.clear() + self.moves.clear() + self.inference_hook = lambda model: None + self.build_hook = lambda model: None + self.assertTrue(self.request(tier="base")["ok"]) + main = self.refs[-1] + entered, release = threading.Event(), threading.Event() + outcomes, errors = [], [] + failure = ValueError("synthetic transfer failure beside active fast work") + + def wait_for_release() -> None: + entered.set() + self.assertTrue(release.wait(2), "Synthetic fast work was not released") + + def on_build(model: Model) -> None: + if model.device == "cpu" and phase == "construction": + wait_for_release() + + def on_inference(model: Model) -> None: + if model.device == "cpu" and phase == "inference": + wait_for_release() + elif model.device == "mps": + self.fail_mps(model) + + def run_fast() -> None: + try: + outcomes.append(self.request(count=1, tier="base")) + except BaseException as error: + errors.append(error) + + def fail_transfer(model: Model, device: str) -> None: + self.moves.append(device) + raise failure + + self.build_hook = on_build + self.inference_hook = on_inference + worker = threading.Thread(target=run_fast, daemon=True) + try: + with self.server._inference_lock: + worker.start() + self.assertTrue(entered.wait(1), "Fast owner never entered its native stage") + clone = self.refs[-1] + with patch.object(Model, "to", fail_transfer): + self.assert_failed_transfer(failure, 3) + self.assertIsNone(self.server._embed_state) + self.assertTrue(self.server._fast_lock.locked()) + self.assertTrue(worker.is_alive()) + failure.__traceback__ = None + self.assertIsNone(main()) + self.assertIsNotNone(clone()) + finally: + release.set() + if worker.ident is not None: + worker.join(2) + self.assertFalse(worker.is_alive(), "Fast owner remained blocked") + self.assertEqual(errors, []) + self.assertEqual(len(outcomes), 1) + self.assertTrue(outcomes[0]["ok"]) + self.assertEqual(outcomes[0]["vectors"].data, [0.0, 0.5]) + self.assertIsNone(self.server._fast_encoder) + self.assertIsNone(self.server._fast_model_id) + self.assertIsNone(self.server._fast_generation) + self.assertIsNone(clone()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-model-request-deadlines-736.py b/tests/test-model-request-deadlines-736.py new file mode 100644 index 00000000..415ce62f --- /dev/null +++ b/tests/test-model-request-deadlines-736.py @@ -0,0 +1,419 @@ +"""Synthetic deadline regressions; no models, sockets, or real clock waits.""" +from __future__ import annotations + +import sys +import types +import unittest +from unittest.mock import patch + +from truememory import model_server as ms + + +class FakeClock: + def __init__(self) -> None: + self.wall = 1000.0 + self.tick = 100.0 + + def time(self) -> float: + return self.wall + + def monotonic(self) -> float: + return self.tick + + def advance(self, seconds: float) -> None: + self.wall += seconds + self.tick += seconds + + +class FakeLock: + """Inject a wait at one acquisition, including a successful late wakeup.""" + + def __init__( + self, clock: FakeClock, *, wait_at: int = 0, wait: float = 2.0, + contended: bool = False, time_out: bool = False, + ) -> None: + self.clock = clock + self.wait_at = wait_at + self.wait = wait + self.contended = contended + self.time_out = time_out + self.calls = 0 + self.timeouts: list[float] = [] + self.held = False + + def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: + if not blocking and self.contended: + return False + self.calls += 1 + self.timeouts.append(timeout) + if self.calls == self.wait_at: + self.clock.advance(self.wait) + if self.time_out and timeout >= 0: + return False + self.held = True + return True + + def release(self) -> None: + assert self.held, "release without acquisition" + self.held = False + + def __enter__(self) -> FakeLock: + self.acquire() + return self + + def __exit__(self, *_args: object) -> None: + self.release() + + +class FakeModel: + def __init__(self, clock: FakeClock, *, oom: bool = False, delay: float = 0) -> None: + self.clock = clock + self.oom = oom + self.delay = delay + self.calls = 0 + + def encode(self, texts: list[str], **_kwargs: object) -> list[list[float]]: + self.calls += 1 + self.clock.advance(self.delay) + if self.oom and self.calls == 1: + raise RuntimeError("MPS backend out of memory") + return [[1.0, 0.0] for _ in texts] + + def predict(self, pairs: list[tuple[str, str]], **_kwargs: object) -> list[float]: + self.calls += 1 + self.clock.advance(self.delay) + if self.oom and self.calls == 1: + raise RuntimeError("MPS backend out of memory") + return [0.5 for _ in pairs] + + +class TestIssue736RequestDeadlines(unittest.TestCase): + def setUp(self) -> None: + self.clock = FakeClock() + clock_patch = patch.object(ms, "time", self.clock) + clock_patch.start() + self.addCleanup(clock_patch.stop) + self.server = ms.ModelServer() + self.server._lock = FakeLock(self.clock) + self.server._fast_lock = FakeLock(self.clock) + self.model = FakeModel(self.clock) + self.loads: list[str] = [] + self.recoveries: list[str] = [] + + def load(tier: str | None, deadline: object = None) -> FakeModel: + self.loads.append(tier or "fixture") + return self.model + + self.server._get_embed_model = load + self.server._get_fast_encoder = load + self.server._get_reranker = load + self.server._recover_embed_oom_locked = lambda model, deadline=None: self.recoveries.append("embed") + self.server._mark_sticky_cpu = lambda kind: self.recoveries.append(kind) + self.server._flush_mps_cache = lambda: None + mps = types.ModuleType("truememory.mps_utils") + mps.is_mps_oom = lambda error: "MPS backend out of memory" in str(error) + mps.flush_mps_cache = lambda: None + mps.ensure_mps_memory_budget = lambda device: None + mps_patch = patch.dict("sys.modules", {"truememory.mps_utils": mps}) + mps_patch.start() + self.addCleanup(mps_patch.stop) + + def request(self, op: str = "embed", *, single: bool = False) -> dict: + payload = {"op": op, "deadline": self.clock.wall + 1.0} + if op == "embed": + payload["texts"] = ["fixture-a"] if single else ["fixture-a", "fixture-b"] + else: + payload["pairs"] = [("fixture-query", "fixture-document")] + return payload + + def assert_expired(self, response: dict) -> None: + self.assertFalse(response["ok"]) + self.assertIn("deadline", response["error"]) + self.assertEqual(self.server._inflight, 0) + self.assertFalse(self.server._lock.held) + self.assertFalse(self.server._fast_lock.held) + + def test_embed_expires_during_each_pre_inference_lock_wait(self) -> None: + for acquisition in (1, 2, 3): + with self.subTest(acquisition=acquisition): + self.server._lock = FakeLock(self.clock, wait_at=acquisition) + response = self.server.handle_request(self.request()) + self.assert_expired(response) + self.assertEqual(self.loads, []) + self.assertEqual(self.model.calls, 0) + self.assertTrue(all(0 < value <= 1.0 for value in self.server._lock.timeouts)) + + def test_rerank_expires_during_lock_wait(self) -> None: + self.server._lock = FakeLock(self.clock, wait_at=1) + self.assert_expired(self.server.handle_request(self.request("rerank"))) + self.assertEqual(self.loads, []) + self.assertEqual(self.model.calls, 0) + + def test_timed_out_lock_is_not_released_or_followed_by_model_work(self) -> None: + self.server._lock = FakeLock(self.clock, wait_at=1, time_out=True) + self.assert_expired(self.server.handle_request(self.request())) + self.assertEqual(self.loads, []) + self.assertEqual(self.server._lock.timeouts, [1.0]) + + def test_fast_lock_expiry_does_not_fall_back_to_main(self) -> None: + self.server._lock = FakeLock(self.clock, contended=True) + self.server._fast_lock = FakeLock(self.clock, wait_at=1) + self.assert_expired(self.server.handle_request(self.request(single=True))) + self.assertEqual(self.loads, []) + self.assertEqual(self.server._lock.calls, 0) + + def test_fast_lock_timeout_does_not_release_an_unowned_lock(self) -> None: + self.server._lock = FakeLock(self.clock, contended=True) + self.server._fast_lock = FakeLock(self.clock, wait_at=1, time_out=True) + self.assert_expired(self.server.handle_request(self.request(single=True))) + self.assertEqual(self.loads, []) + self.assertEqual(self.server._lock.calls, 0) + self.assertEqual(self.server._fast_lock.timeouts, [1.0]) + + def test_lock_waits_spend_one_budget_without_renewal(self) -> None: + class ElapsingLock(FakeLock): + def acquire(inner_self, blocking: bool = True, timeout: float = -1) -> bool: + result = super().acquire(blocking, timeout) + self.clock.advance(0.4) + return result + + self.server._lock = ElapsingLock(self.clock) + self.assert_expired(self.server.handle_request(self.request())) + self.assertEqual(self.loads, []) + self.assertEqual(len(self.server._lock.timeouts), 3) + for actual, expected in zip(self.server._lock.timeouts, [1.0, 0.6, 0.2]): + self.assertAlmostEqual(actual, expected) + + def test_expiry_while_waiting_to_activate_throttler_skips_activation(self) -> None: + self.server._SUSTAINED_THRESHOLD = 1 + activations: list[bool] = [] + self.server._activate_throttler = lambda: activations.append(True) + self.server._lock = FakeLock(self.clock, wait_at=2) + self.assert_expired(self.server.handle_request(self.request())) + self.assertEqual(activations, []) + self.assertEqual(self.loads, []) + + def test_throttle_delay_cannot_start_model_loading(self) -> None: + class Throttler: + def before_batch(inner_self) -> tuple[int, dict]: + self.clock.advance(2.0) + return 1, {} + + def after_batch(inner_self, *_args: object) -> None: + pass + + def should_flush_cache(inner_self) -> bool: + return False + + self.server._throttler = Throttler() + self.server._throttler_active = True + self.assert_expired(self.server.handle_request(self.request())) + self.assertEqual(self.loads, []) + self.assertEqual(self.model.calls, 0) + + def test_model_loading_consumes_the_same_budget(self) -> None: + for op, single, contended in (("embed", False, False), ("embed", True, False), + ("embed", True, True), ("rerank", False, False)): + with self.subTest(op=op, single=single, contended=contended): + self.server._lock = FakeLock(self.clock, contended=contended) + + def slow_load(_identity: str | None, deadline: object = None) -> FakeModel: + self.clock.advance(2.0) + return self.model + + self.server._get_embed_model = slow_load + self.server._get_fast_encoder = slow_load + self.server._get_reranker = slow_load + self.assert_expired(self.server.handle_request(self.request(op, single=single))) + self.assertEqual(self.model.calls, 0) + + def test_expired_oom_records_fault_and_next_request_loads_cpu(self) -> None: + for op, single in (("embed", False), ("embed", True), ("rerank", False)): + with self.subTest(op=op, single=single): + self.server = ms.ModelServer() + self.server._lock = FakeLock(self.clock) + self.server._fast_lock = FakeLock(self.clock) + self.server._write_status_file = lambda: None + failed_model = FakeModel(self.clock, oom=True, delay=2.0) + cpu_model = FakeModel(self.clock) + devices: list[str | None] = [] + expensive_recovery: list[str] = [] + + def build(_identity: str, device: str | None) -> FakeModel: + self.assertTrue(self.server._lock.held) + devices.append(device) + return cpu_model + + failed_model.to = lambda device: expensive_recovery.append("move") + self.server._embed_state = ms._EmbedState( + model=failed_model, tier="", model_id="qwen3_256", + ) if op == "embed" else None + self.server._resolve_embed_model_id = lambda tier: "qwen3_256" + self.server._build_embed_model = build + self.server._reranker = failed_model if op == "rerank" else None + self.server._reranker_name = "fixture" if op == "rerank" else None + mps = sys.modules["truememory.mps_utils"] + mps.flush_mps_cache = lambda: expensive_recovery.append("flush") + mps.auto_detect_device = lambda: "mps" + mps.resolve_device = lambda device: device + reranker_module = types.ModuleType("truememory.reranker") + reranker_module.get_current_reranker_name = lambda: "fixture" + transformers = types.ModuleType("sentence_transformers") + transformers.CrossEncoder = build + + with patch.dict("sys.modules", { + "truememory.reranker": reranker_module, + "sentence_transformers": transformers, + }): + self.assert_expired(self.server.handle_request(self.request(op, single=single))) + self.assertEqual(failed_model.calls, 1) + self.assertEqual(self.server._sticky_cpu, {op}) + self.assertIsNone(self.server._embed_state) + self.assertIsNone(self.server._reranker) + self.assertIsNone(self.server._reranker_name) + self.assertEqual(devices, []) + self.assertEqual(expensive_recovery, []) + + # A fresh budget reloads on CPU, then reuses that instance. + for _ in range(2): + response = self.server.handle_request(self.request(op, single=single)) + self.assertTrue(response["ok"]) + self.assertEqual(devices, ["cpu"]) + self.assertEqual(failed_model.calls, 1) + self.assertEqual(cpu_model.calls, 2) + self.assertEqual(expensive_recovery, []) + if op == "embed": + self.assertIs(self.server._embed_state.model, cpu_model) + else: + self.assertIs(self.server._reranker, cpu_model) + + def test_unexpired_embed_recovery_keeps_cached_cpu_instance(self) -> None: + for single in (False, True): + with self.subTest(single=single): + self.server = ms.ModelServer() + self.server._lock = FakeLock(self.clock) + self.server._fast_lock = FakeLock(self.clock) + self.server._write_status_file = lambda: None + model = FakeModel(self.clock, oom=True) + movements: list[str] = [] + model.to = lambda device: movements.append(device) + self.server._embed_state = ms._EmbedState( + model=model, tier="", model_id="qwen3_256", + ) + self.assertTrue(self.server.handle_request(self.request(single=single))["ok"]) + self.assertTrue(self.server.handle_request(self.request(single=single))["ok"]) + self.assertEqual(self.server._sticky_cpu, {"embed"}) + self.assertEqual(movements, ["cpu"]) + self.assertIs(self.server._embed_state.model, model) + self.assertEqual(model.calls, 3) + + def test_embed_cache_flush_expiry_skips_native_move_and_invalidates_cache(self) -> None: + for single in (False, True): + with self.subTest(single=single): + self.server = ms.ModelServer() + self.server._lock = FakeLock(self.clock) + self.server._fast_lock = FakeLock(self.clock) + self.server._write_status_file = lambda: None + model = FakeModel(self.clock, oom=True) + movements: list[str] = [] + model.to = lambda device: movements.append(device) + self.server._embed_state = ms._EmbedState( + model=model, tier="", model_id="qwen3_256", + ) + sys.modules["truememory.mps_utils"].flush_mps_cache = lambda: self.clock.advance(2.0) + self.assert_expired(self.server.handle_request(self.request(single=single))) + self.assertEqual(self.server._sticky_cpu, {"embed"}) + self.assertIsNone(self.server._embed_state) + self.assertEqual(movements, []) + self.assertEqual(model.calls, 1) + + def test_embed_recovery_consumes_budget_before_retry(self) -> None: + for single in (False, True): + with self.subTest(single=single): + self.model = FakeModel(self.clock, oom=True) + self.server._recover_embed_oom_locked = lambda model, deadline=None: self.clock.advance(2.0) + self.assert_expired(self.server.handle_request(self.request(single=single))) + self.assertEqual(self.model.calls, 1) + + def test_rerank_reload_consumes_budget_before_retry(self) -> None: + self.model = FakeModel(self.clock, oom=True) + cpu_model = FakeModel(self.clock) + + def load(_name: str | None, deadline: object = None) -> FakeModel: + if self.recoveries: + self.clock.advance(2.0) + return cpu_model + return self.model + + self.server._get_reranker = load + self.assert_expired(self.server.handle_request(self.request("rerank"))) + self.assertEqual(self.model.calls, 1) + self.assertEqual(cpu_model.calls, 0) + + def test_rerank_cache_flush_expiry_prevents_cpu_model_load(self) -> None: + self.model = FakeModel(self.clock, oom=True) + sys.modules["truememory.mps_utils"].flush_mps_cache = lambda: self.clock.advance(2.0) + self.assert_expired(self.server.handle_request(self.request("rerank"))) + self.assertEqual(self.loads, ["fixture"]) + self.assertEqual(self.model.calls, 1) + self.assertEqual(self.recoveries, ["rerank"]) + self.assertIsNone(self.server._reranker) + self.assertIsNone(self.server._reranker_name) + + def test_wall_clock_rollback_cannot_extend_accepted_budget(self) -> None: + def slow_load(_tier: str, deadline: object = None) -> FakeModel: + self.clock.tick += 2.0 + self.clock.wall -= 100.0 + return self.model + + self.server._get_embed_model = slow_load + self.assert_expired(self.server.handle_request(self.request())) + self.assertEqual(self.model.calls, 0) + + def test_wall_clock_jump_cannot_expire_unspent_monotonic_budget(self) -> None: + def load(_tier: str, deadline: object = None) -> FakeModel: + self.clock.wall += 100.0 + return self.model + + self.server._get_embed_model = load + self.assertTrue(self.server.handle_request(self.request())["ok"]) + self.assertEqual(self.model.calls, 1) + + def test_legacy_request_without_deadline_keeps_blocking_semantics(self) -> None: + request = self.request() + del request["deadline"] + self.server._lock = FakeLock(self.clock, wait_at=1) + self.assertTrue(self.server.handle_request(request)["ok"]) + self.assertEqual(self.model.calls, 1) + self.assertTrue(all(value == -1 for value in self.server._lock.timeouts)) + + def test_legacy_malformed_or_unbounded_deadlines_remain_compatible(self) -> None: + for value in (None, "invalid", {}, float("nan"), float("inf")): + with self.subTest(value=value): + request = self.request(single=True) + request["deadline"] = value + self.assertTrue(self.server.handle_request(request)["ok"]) + + def test_distant_valid_deadline_cannot_overflow_native_lock_timeout(self) -> None: + request = self.request() + request["deadline"] = 1e100 + self.assertTrue(self.server.handle_request(request)["ok"]) + self.assertTrue(all(0 < value <= ms.threading.TIMEOUT_MAX + for value in self.server._lock.timeouts)) + + def test_fast_model_failure_before_expiry_still_uses_main_fallback(self) -> None: + self.server._lock = FakeLock(self.clock, contended=True) + + def fail(_tier: str, deadline: object = None) -> FakeModel: + raise ValueError("synthetic model failure") + + self.server._get_fast_encoder = fail + self.assertTrue(self.server.handle_request(self.request(single=True))["ok"]) + self.assertEqual(self.model.calls, 1) + + def test_successful_requests_keep_response_fields(self) -> None: + for op, key in (("embed", "vectors"), ("rerank", "scores")): + with self.subTest(op=op): + response = self.server.handle_request(self.request(op)) + self.assertEqual(set(response), {"ok", key}) + self.assertTrue(response["ok"]) diff --git a/tests/test-model-residency-297.py b/tests/test-model-residency-297.py new file mode 100644 index 00000000..7b31a9d7 --- /dev/null +++ b/tests/test-model-residency-297.py @@ -0,0 +1,504 @@ +"""Actual model loaders and handlers with stdlib lifetimes and event barriers.""" +from __future__ import annotations + +import ast +import importlib.util +import sys +import threading +import types +import unittest +import weakref +from collections.abc import Callable +from pathlib import Path +from unittest.mock import patch + + +SOURCE = Path(__file__).resolve().parents[1] / "truememory" / "model_server.py" +SOURCE_TEXT: str | None = None +SUPPORT_PATH = Path(__file__).with_name("test-model-allocation-preflight-297.py") +SPEC = importlib.util.spec_from_file_location("residency_array_support", SUPPORT_PATH) +SUPPORT = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(SUPPORT) + + +def load_server() -> types.ModuleType: + tree = ast.parse(SOURCE_TEXT if SOURCE_TEXT is not None else SOURCE.read_text(encoding="utf-8")) + tree.body = [node for node in tree.body if not ( + isinstance(node, ast.Import) and any(alias.name == "numpy" for alias in node.names) + or isinstance(node, ast.ImportFrom) and (node.module or "").startswith("truememory.") + or isinstance(node, ast.Try) and any( + isinstance(child, ast.Import) and any(alias.name == "psutil" for alias in child.names) + for child in node.body + ) + or isinstance(node, ast.Expr) and isinstance(node.value, ast.Call) + and isinstance(node.value.func, ast.Name) and node.value.func.id == "_set_mps_memory_cap" + )] + module = types.ModuleType("synthetic_residency_server") + module.__dict__.update({ + "np": SUPPORT.Numpy(), "psutil": None, "_USE_UNIX": True, "_LOOPBACK_HOST": "127.0.0.1", + "_env_int": lambda name, default, **kwargs: default, "pid_is_alive": lambda pid: False, + }) + with patch.dict(sys.modules, {module.__name__: module}): + exec(compile(ast.fix_missing_locations(tree), str(SOURCE), "exec"), module.__dict__) + module.log.disabled = True + return module + + +class MetadataLock: + """Observe ownership without mistaking another thread's scalar work for ours.""" + def __init__(self) -> None: + self.lock = threading.Lock() + self.owner: int | None = None + + def acquire(self, blocking: bool = True) -> bool: + acquired = self.lock.acquire(blocking=blocking) + if acquired: + self.owner = threading.get_ident() + return acquired + + def release(self) -> None: + self.owner = None + self.lock.release() + + def locked(self) -> bool: + return self.lock.locked() + + def owned(self) -> bool: + return self.owner == threading.get_ident() + + def __enter__(self) -> MetadataLock: + self.acquire() + return self + + def __exit__(self, *_args: object) -> None: + self.release() + + +class Model: + def __init__(self, owner: TestModelResidency, identity: str, device: str) -> None: + self.owner, self.identity, self.device = owner, identity, device + self.value = {"model2vec": 11.0, "qwen3_256": 22.0, "rank-a": 31.0, "rank-b": 32.0}[identity] + + def encode(self, texts: list, **kwargs: object) -> list: + self.owner.encodes.append((self.identity, self.device, list(texts))) + self.owner.encode_hook(self) + return [[self.value] for _ in texts] + + def predict(self, pairs: list, **kwargs: object) -> list: + self.owner.encodes.append((self.identity, self.device, list(pairs))) + return [self.value for _ in pairs] + + def to(self, device: str) -> None: + raise AssertionError("Residency retirement must not move an active model") + + def __del__(self) -> None: + lock = getattr(self.owner.server, "_residency_lock", None) + self.owner.destructions.append((self.identity, self.device, lock is not None and lock.owned())) + + +class TestModelResidency(unittest.TestCase): + def setUp(self) -> None: + package = types.ModuleType("truememory") + package.__path__ = [] + vector = types.ModuleType("truememory.vector_search") + vector.EMBEDDING_MODEL = "model2vec" + vector._TIER_ALIASES = {"edge": "model2vec", "base": "qwen3_256", "pro": "qwen3_256"} + reranker = types.ModuleType("truememory.reranker") + reranker.get_current_reranker_name = lambda: "rank-a" + mps = types.ModuleType("truememory.mps_utils") + mps.auto_detect_device = lambda: "cpu" + mps.resolve_device = lambda device: device + mps.ensure_mps_memory_budget = lambda device: None + mps.is_mps_oom = lambda error: False + transformers = types.ModuleType("sentence_transformers") + transformers.CrossEncoder = lambda identity, device: self.build(identity, device) + self.modules = patch.dict(sys.modules, { + "truememory": package, "truememory.vector_search": vector, + "truememory.reranker": reranker, "truememory.mps_utils": mps, + "sentence_transformers": transformers, + }) + self.modules.start() + self.ms = load_server() + self.server = self.ms.ModelServer() + if hasattr(self.server, "_residency_lock"): + self.server._residency_lock = MetadataLock() + self.server._write_status_file = lambda: None + self.server._embed_device = lambda: "main" + self.server._build_embed_model = self.build + self.refs: list[weakref.ReferenceType] = [] + self.builds: list[tuple[str, str]] = [] + self.encodes: list[tuple] = [] + self.destructions: list[tuple] = [] + self.build_hook: Callable[[str, str], None] = lambda identity, device: None + self.ready_hook: Callable[[Model], None] = lambda model: None + self.encode_hook: Callable[[Model], None] = lambda model: None + self.events: list[threading.Event] = [] + self.threads: list[threading.Thread] = [] + self.failures: list[str] = [] + + def tearDown(self) -> None: + for event in self.events: + event.set() + for thread in self.threads: + thread.join(2) + self.assertFalse(thread.is_alive(), "Synthetic owner did not finish") + self.server._workers.shutdown(wait=True, cancel_futures=True) + self.server._embed_state = None + self.server._reranker = None + self.server._fast_encoder = None + self.modules.stop() + self.assertEqual(self.failures, []) + self.assertFalse(any(locked for _, _, locked in self.destructions), self.destructions) + + def build(self, identity: str, device: str) -> Model: + lock = getattr(self.server, "_residency_lock", None) + self.assertFalse(lock is not None and lock.owned(), "Constructor owns residency metadata") + self.build_hook(identity, device) + model = Model(self, identity, device) + self.refs.append(weakref.ref(model)) + self.builds.append((identity, device)) + self.ready_hook(model) + return model + + def request(self, op: str = "embed", tier: str = "edge", name: str = "rank-a") -> dict: + return self.server.handle_request({ + "op": op, "tier": tier, "texts": ["one", "two"], + "pairs": [("query", "one"), ("query", "two")], "model_name": name, + }) + + def fast(self, tier: str = "edge", deadline: object = None) -> dict | None: + return self.server._handle_fast_embed(["query"], tier, deadline) + + def main_while_owned(self, tier: str) -> None: + with self.server._lock: + self.server._get_embed_model(tier) + + def event(self) -> threading.Event: + event = threading.Event() + self.events.append(event) + return event + + def start(self, action: Callable[[], object]) -> tuple[threading.Thread, list]: + results: list = [] + + def run() -> None: + try: + results.append(action()) + except Exception as exc: + self.failures.append(f"{type(exc).__name__}: {exc}") + + thread = threading.Thread(target=run, daemon=True) + self.threads.append(thread) + thread.start() + return thread, results + + def finish(self, thread: threading.Thread) -> None: + thread.join(2) + self.assertFalse(thread.is_alive(), "Owner waited for another lane") + self.assertEqual(self.failures, []) + + def test_main_replacement_releases_old_before_actual_handler_constructor(self) -> None: + self.assertEqual(self.request()["vectors"].data, [11.0, 11.0]) + old = self.refs[-1] + + def check(identity: str, device: str) -> None: + self.assertIsNone(old(), "Obsolete main remains live during replacement") + self.assertTrue(self.server._lock.locked()) + self.assertTrue(self.server._inference_lock.locked()) + + self.build_hook = check + self.assertEqual(self.request(tier="base")["vectors"].data, [22.0, 22.0]) + + def test_reranker_replacement_releases_old_before_actual_handler_constructor(self) -> None: + self.assertEqual(self.request("rerank")["scores"].data, [31.0, 31.0]) + old = self.refs[-1] + self.build_hook = lambda identity, device: self.assertIsNone( + old(), "Obsolete reranker remains live during replacement", + ) + self.assertEqual(self.request("rerank", name="rank-b")["scores"].data, [32.0, 32.0]) + + def test_failed_replacements_leave_empty_cache_and_later_reload_old_identity(self) -> None: + for op in ("embed", "rerank"): + with self.subTest(op=op): + self.build_hook = lambda identity, device: None + self.request(op) + old = self.refs[-1] + + def fail(identity: str, device: str) -> None: + self.assertIsNone(old()) + raise RuntimeError("synthetic construction failure") + + self.build_hook = fail + with self.assertRaisesRegex(RuntimeError, "synthetic construction failure"): + self.request(op, tier="base", name="rank-b") + self.assertIsNone(self.server._embed_state if op == "embed" else self.server._reranker) + if op == "rerank": + self.assertIsNone(self.server._reranker_name) + self.assertFalse(self.server._lock.locked()) + self.assertFalse(self.server._inference_lock.locked()) + self.build_hook = lambda identity, device: None + self.assertTrue(self.request(op)["ok"]) + self.assertIsNotNone(self.refs[-1]()) + + def test_idle_fast_clone_retires_before_distinct_main_constructor(self) -> None: + self.request() + main = self.refs[-1] + with self.server._inference_lock: + self.assertEqual(self.fast()["vectors"].data, [11.0]) + fast = self.refs[-1] + + def check(identity: str, device: str) -> None: + self.assertIsNone(fast(), "Idle obsolete fast clone remains resident") + self.assertIsNone(main()) + + self.build_hook = check + self.main_while_owned("base") + self.assertIsNone(self.server._fast_encoder) + self.assertIsNone(self.server._fast_model_id) + + def test_alias_retag_preserves_main_generation_and_fast_cache(self) -> None: + self.request(tier="base") + generation = self.server._embed_state.generation + with self.server._inference_lock: + self.assertTrue(self.fast("base")["ok"]) + clone = self.refs[-1] + self.main_while_owned("pro") + self.assertIs(self.server._embed_state.generation, generation) + self.assertIs(self.server._fast_encoder, clone()) + self.assertEqual(self.fast("pro")["vectors"].data, [22.0]) + self.assertEqual(self.builds, [("qwen3_256", "main"), ("qwen3_256", "cpu")]) + + def test_fast_construction_releases_snapshot_and_cannot_publish_after_a_b_a(self) -> None: + for return_to_a in (False, True): + with self.subTest(return_to_a=return_to_a): + self.request() + old = weakref.ref(self.server._embed_state.model) + started, release = self.event(), self.event() + + def block(model: Model) -> None: + if model.device == "cpu": + started.set() + if not release.wait(2): + raise AssertionError("Constructor barrier timed out") + + self.ready_hook = block + with self.server._inference_lock: + thread, results = self.start(self.fast) + self.assertTrue(started.wait(2)) + clone = self.refs[-1] + self.main_while_owned("base") + self.assertIsNone(old(), "Fast constructor retained main snapshot") + if return_to_a: + self.main_while_owned("edge") + self.encode_hook = lambda model: self.assertIsNone( + self.server._fast_encoder, "Stale construction published as current cache", + ) + release.set() + self.finish(thread) + self.assertIsNotNone(results[0], "Captured fast request lost its correct output") + self.assertEqual(results[0]["vectors"].data, [11.0]) + self.assertIsNone(clone()) + self.assertIsNone(self.server._fast_encoder) + self.encode_hook = lambda model: None + self.ready_hook = lambda model: None + + def test_active_stale_fast_request_finishes_or_fails_before_clone_retirement(self) -> None: + for fail in (False, True): + with self.subTest(fail=fail): + self.request() + started, release = self.event(), self.event() + + def block(model: Model) -> None: + if model.device == "cpu": + started.set() + if not release.wait(2): + raise AssertionError("Inference barrier timed out") + if fail: + raise RuntimeError("synthetic encode failure") + + self.encode_hook = block + with self.server._inference_lock: + thread, results = self.start(self.fast) + self.assertTrue(started.wait(2)) + clone = self.refs[-1] + self.main_while_owned("base") + self.assertIs(self.server._fast_encoder, clone()) + self.assertIsNotNone(clone(), "Active clone evicted") + release.set() + self.finish(thread) + if fail: + self.assertEqual(results, [None]) + else: + self.assertEqual(results[0]["vectors"].data, [11.0]) + self.assertIsNone(self.server._fast_encoder) + self.assertIsNone(clone()) + self.encode_hook = lambda model: None + + def test_fast_replacement_drops_old_clone_before_constructor_even_on_failure(self) -> None: + self.request() + with self.server._inference_lock: + self.assertTrue(self.fast()["ok"]) + old = self.refs[-1] + # Hold the owner slot without inference while the main lane changes. + with self.server._fast_lock: + self.main_while_owned("base") + self.assertIsNotNone(old()) + + def fail(identity: str, device: str) -> None: + self.assertIsNone(old(), "Old CPU clone overlaps replacement constructor") + raise RuntimeError("synthetic CPU construction failure") + + self.build_hook = fail + with self.assertRaisesRegex(RuntimeError, "synthetic CPU construction failure"): + self.server._get_fast_encoder("base") + self.assertIsNone(self.server._fast_encoder) + self.assertIsNone(self.server._fast_model_id) + + def test_fast_publication_contention_returns_uncached_correct_model(self) -> None: + self.request() + with self.server._fast_lock: + # Contend only at publication; the constructor must run lock-free. + self.ready_hook = lambda model: self.server._residency_lock.acquire() + try: + model = self.server._get_fast_encoder("edge") + clone = weakref.ref(model) + self.assertIsNone(self.server._fast_encoder) + self.assertEqual(model.encode(["query"]), [[11.0]]) + finally: + self.server._residency_lock.release() + model = None + self.assertIsNone(clone()) + + def test_fast_constructor_and_inference_finish_while_both_main_locks_are_owned(self) -> None: + self.request() + with self.server._inference_lock, self.server._lock: + thread, results = self.start(self.fast) + self.finish(thread) + self.assertEqual(results[0]["vectors"].data, [11.0]) + self.assertIs(self.server._fast_encoder, self.refs[-1]()) + self.assertEqual(self.builds, [("model2vec", "main"), ("model2vec", "cpu")]) + + def test_generation_validation_and_cache_publication_exclude_main_invalidation(self) -> None: + self.request() + publishing, release_publication = self.event(), self.event() + main_attempted, encoding, release_encode = self.event(), self.event(), self.event() + reads, guarded = [], [] + testcase = self + + class ObservedServer(self.ms.ModelServer): + def __getattribute__(inner_self, name: str) -> object: + value = super().__getattribute__(name) + if name == "_embed_state" and threading.current_thread().name == "residency-fast-publication": + reads.append(True) + if len(reads) == 2: + guarded.append(inner_self._residency_lock.owned()) + publishing.set() + if not release_publication.wait(2): + raise AssertionError("Publication barrier timed out") + return value + + self.server.__class__ = ObservedServer + acquire = self.server._residency_lock.acquire + + def observe_acquire(blocking: bool = True) -> bool: + if threading.current_thread().name == "residency-main-replacement": + main_attempted.set() + return acquire(blocking) + + self.server._residency_lock.acquire = observe_acquire + + def encode(model: Model) -> None: + if model.device == "cpu": + encoding.set() + if not release_encode.wait(2): + raise AssertionError("Publication inference barrier timed out") + + self.encode_hook = encode + + def fast() -> dict | None: + threading.current_thread().name = "residency-fast-publication" + return testcase.fast() + + def main() -> dict: + threading.current_thread().name = "residency-main-replacement" + return testcase.request(tier="base") + + with self.server._inference_lock: + fast_thread, fast_results = self.start(fast) + self.assertTrue(publishing.wait(2)) + self.assertEqual(guarded, [True]) + main_thread, main_results = self.start(main) + self.assertTrue(main_attempted.wait(2)) + self.assertEqual(self.server._embed_state.model_id, "model2vec") + release_publication.set() + self.assertTrue(encoding.wait(2)) + self.finish(main_thread) + self.assertEqual(main_results[0]["vectors"].data, [22.0, 22.0]) + release_encode.set() + self.finish(fast_thread) + self.assertEqual(fast_results[0]["vectors"].data, [11.0]) + self.assertIsNone(self.server._fast_encoder) + + def test_fast_deadline_after_stale_construction_skips_encode_and_releases_clone(self) -> None: + self.request() + started, release = self.event(), self.event() + clock = [1.0] + deadline = self.ms._RequestDeadline(expires_at=2.0) + + def block(model: Model) -> None: + if model.device == "cpu": + started.set() + if not release.wait(2): + raise AssertionError("Deadline barrier timed out") + + self.ready_hook = block + + def expire() -> str: + try: + self.fast(deadline=deadline) + except self.ms._RequestDeadlineExceeded: + return "expired" + raise AssertionError("Expired fast request completed") + + with patch.object(self.ms.time, "monotonic", side_effect=lambda: clock[0]): + with self.server._inference_lock: + thread, results = self.start(expire) + self.assertTrue(started.wait(2)) + clone = self.refs[-1] + self.main_while_owned("base") + clock[0] = 3.0 + release.set() + self.finish(thread) + self.assertEqual(results, ["expired"]) + self.assertIsNone(clone()) + self.assertIsNone(self.server._fast_encoder) + self.assertEqual(len(self.encodes), 1) + self.assertFalse(self.server._fast_lock.locked()) + + def test_idle_retirement_unlocks_owner_before_allowing_new_publication(self) -> None: + self.request() + with self.server._inference_lock: + self.assertTrue(self.fast()["ok"]) + real = self.server._fast_lock + testcase = self + + class CheckedRelease: + def acquire(self, blocking: bool = True) -> bool: + testcase.assertFalse(testcase.server._residency_lock.locked()) + testcase.assertFalse(blocking) + return real.acquire(blocking=blocking) + + def release(self) -> None: + testcase.assertTrue(testcase.server._residency_lock.locked()) + real.release() + + self.server._fast_lock = CheckedRelease() + self.server._retire_stale_fast_encoder() + self.assertFalse(real.locked()) + + +if __name__ == "__main__": + if "--source-stdin" in sys.argv: + sys.argv.remove("--source-stdin") + SOURCE_TEXT = sys.stdin.read() + unittest.main() diff --git a/tests/test-model-server-sharing-741.py b/tests/test-model-server-sharing-741.py new file mode 100644 index 00000000..35513415 --- /dev/null +++ b/tests/test-model-server-sharing-741.py @@ -0,0 +1,510 @@ +"""Shared ownership tests using production definitions and stdlib-only fakes. + +This file can run directly without importing TrueMemory, numpy or torch. +Temporary endpoints and synthetic child identities never reference live state. +""" +from __future__ import annotations + +import ast +import atexit +import base64 +from collections.abc import Iterator +from concurrent.futures import Future, ThreadPoolExecutor +from contextlib import contextmanager +from dataclasses import dataclass +import gc +import hmac +import json +import logging +import math +import os +from pathlib import Path +import platform +import plistlib +import secrets +import shutil +import signal +import socket +import stat +import struct +import subprocess +import sys +import tempfile +import threading +import time +import types +import unittest +import weakref +from unittest.mock import Mock, patch + +SOURCE = Path(__file__).resolve().parents[1] / "truememory" + + +def load_platform() -> types.ModuleType: + module = types.ModuleType("synthetic_platform_741") + exec(compile((SOURCE / "_platform.py").read_text(), "synthetic-platform", "exec"), module.__dict__) + return module + + +def load_definitions(filename: str, namespace: dict, names: set[str] | None = None) -> types.ModuleType: + parsed = ast.parse((SOURCE / filename).read_text()) + selected = [node for node in parsed.body if isinstance(node, (ast.FunctionDef, ast.ClassDef)) + and (names is None or node.name in names)] + tree = ast.fix_missing_locations(ast.Module(body=[ + ast.ImportFrom(module="__future__", names=[ast.alias(name="annotations")], level=0), *selected, + ], type_ignores=[])) + module = types.ModuleType("synthetic_" + filename.replace(".", "_")) + module.__dict__.update(namespace) + with patch.dict(sys.modules, {module.__name__: module}): + exec(compile(tree, filename, "exec"), module.__dict__) + return module + + +def shared_namespace(root: Path) -> dict: + helpers = load_platform() + result = {"os": os, "sys": sys, "time": time, "json": json, "socket": socket, + "struct": struct, "secrets": secrets, "logging": logging, "Path": Path, + "log": logging.getLogger("synthetic-sharing-741"), "_TRUEMEMORY_DIR": root, + "_USE_UNIX": sys.platform != "win32", "_LOOPBACK_HOST": "127.0.0.1", + "_MODEL_SERVER_GENERATION_ENV": helpers._MODEL_SERVER_GENERATION_ENV, + "try_file_lock": helpers.try_file_lock, "read_start_claim": helpers.read_start_claim, + "process_birth": lambda pid: float(pid), "spawn_kwargs": helpers.spawn_kwargs, + "pid_is_alive": lambda pid: pid == os.getpid(), + "_HEADER_FMT": ">I", "_HEADER_SIZE": 4, "_MAX_MESSAGE_SIZE": 10 * 1024 * 1024, + "PROTOCOL_VERSION": 1, "_SERVER_START_TIMEOUT": .5, "_REQUEST_TIMEOUT": 120, + "_default_request_timeout": None, "base64": base64, + "np": types.SimpleNamespace(ndarray=list, integer=int)} + for name, filename in (("SOCK_PATH", "model.sock"), ("PID_PATH", "model_server.pid"), + ("PORT_PATH", "model_server.port"), ("TOKEN_PATH", "model_server.token"), + ("LOCK_PATH", "model_server.lock"), ("START_LOCK_PATH", "model_server.start.lock"), + ("START_STATE_PATH", "model_server.start.json")): + result[name] = root / filename + return result + + +def client_module(root: Path) -> types.ModuleType: + namespace = shared_namespace(root) + namespace.update(subprocess=subprocess, platform=platform, plistlib=plistlib, shutil=shutil, + _APP_BUNDLE_PATH=root / "TrueMemory.app", + _APP_EXECUTABLE=root / "TrueMemory.app" / "Contents" / "MacOS" / "TrueMemory", + _LSREGISTER=str(root / "synthetic-lsregister")) + module = load_definitions("model_client.py", namespace) + module._ensure_app_bundle = lambda _deadline=None: None + return module + + +def server_module(root: Path) -> types.ModuleType: + namespace = shared_namespace(root) + namespace.update(dataclass=dataclass, contextmanager=contextmanager, ThreadPoolExecutor=ThreadPoolExecutor, + Future=Future, threading=threading, math=math, gc=gc, hmac=hmac, + stat=stat, atexit=atexit, signal=signal, IDLE_TIMEOUT=300, + _HMAC_TOKEN_BYTES=32, _EMBED_BATCH_LIMIT=32, _RERANK_BATCH_LIMIT=64) + return load_definitions("model_server.py", namespace) + + +def racing_client(root: Path) -> None: + module = client_module(root) + module._SERVER_START_TIMEOUT = 3 + module._probe_server = lambda _deadline: (root / "synthetic-ready").exists() + def spawn(*_args: object, **_kwargs: object) -> types.SimpleNamespace: + with (root / "synthetic-launches").open("a") as stream: + stream.write("launch\n") + (root / "synthetic-ready").touch() + return types.SimpleNamespace(pid=7654321) + module.subprocess = types.SimpleNamespace(Popen=spawn, DEVNULL=subprocess.DEVNULL) + if not module._start_server(wait_timeout=2): + raise RuntimeError("synthetic concurrent starter failed") + + +class SharedModelOwnership(unittest.TestCase): + def setUp(self) -> None: + temporary = tempfile.TemporaryDirectory(prefix="tm-sharing-741-") + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name) + self.client = client_module(self.root) + env = patch.dict(os.environ, {}, clear=False) + env.start() + self.addCleanup(env.stop) + os.environ.pop("TRUEMEMORY_NO_MODEL_SERVER", None) + os.environ.pop("TRUEMEMORY_MODEL_SERVER_GENERATION", None) + + def make_server(self) -> tuple[types.ModuleType, object]: + module = server_module(self.root) + server = module.ModelServer() + self.addCleanup(server._cleanup) + return module, server + + @contextmanager + def loaders(self) -> Iterator[tuple[types.ModuleType, types.ModuleType, Mock]]: + factories = Mock(side_effect=AssertionError("unexpected local model construction")) + transformers = types.ModuleType("sentence_transformers") + transformers.SentenceTransformer = factories + transformers.CrossEncoder = factories + mps = types.ModuleType("truememory.mps_utils") + mps.resolve_device = lambda _value=None: "cpu" + mps.auto_detect_device = lambda: "cpu" + mps.ensure_mps_memory_budget = lambda _device: None + namespace = {"_model": None, "_lock": threading.Lock(), "_embedding_dim": 256, + "EMBEDDING_MODEL": "qwen3_256", "os": os, "sys": sys, + "_model_name": None, "_model_certified": False, "_frozen_embedding_target": None, + "contextmanager": contextmanager, + "get_current_reranker_name": lambda: "synthetic-reranker"} + vectors = load_definitions("vector_search.py", dict(namespace), {"get_model"}) + reranker = load_definitions("reranker.py", dict(namespace), {"get_reranker", "_reranker_load_lock"}) + serving = types.ModuleType("synthetic_serving_741") + with patch.dict(sys.modules, {serving.__name__: serving}): + exec(compile((SOURCE / "tier_switch/serving.py").read_text(), "synthetic-serving", "exec"), serving.__dict__) + runtime = types.SimpleNamespace(current_runtime_operation=lambda: None, + require_model_load_allowed=lambda: None, TierRuntimeError=RuntimeError) + package = types.ModuleType("truememory") + package.__path__ = [] + with patch.dict(sys.modules, {"truememory": package, "truememory.model_client": self.client, + "truememory.mps_utils": mps, "sentence_transformers": transformers, + "truememory.tier_switch.serving": serving, + "truememory.tier_switch.runtime": runtime}): + yield vectors, reranker, factories + + def test_missing_endpoints_return_proxies_in_independent_clients(self) -> None: + self.client._server_ready = Mock(side_effect=AssertionError("policy read endpoint")) + self.client._server_is_alive = Mock(side_effect=AssertionError("policy read PID")) + for _ in range(2): + with self.loaders() as (vectors, reranker, factories): + embedding = vectors.get_model() + cross_encoder = reranker.get_reranker() + self.assertIsInstance(embedding, self.client.EmbeddingProxy) + self.assertIsInstance(cross_encoder, self.client.RerankerProxy) + self.client._request_with_autostart = Mock(side_effect=ConnectionError("synthetic absent")) + with self.assertRaises(ConnectionError): + embedding.encode(["synthetic"]) + with self.assertRaises(ConnectionError): + cross_encoder.predict([("synthetic", "synthetic")]) + factories.assert_not_called() + + def test_proxy_constructor_failure_never_falls_into_local_factories(self) -> None: + for name in ("get_embedding_proxy", "get_reranker_proxy"): + with patch.object(self.client, name, side_effect=RuntimeError("synthetic proxy failure")): + with self.loaders() as (vectors, reranker, factories): + getter = vectors.get_model if name == "get_embedding_proxy" else reranker.get_reranker + with self.assertRaisesRegex(RuntimeError, "synthetic proxy failure"): + getter() + factories.assert_not_called() + + def test_explicit_local_mode_preserves_models_and_device(self) -> None: + os.environ["TRUEMEMORY_NO_MODEL_SERVER"] = "1" + self.client._server_ready = Mock(side_effect=AssertionError("local policy probed endpoint")) + with self.loaders() as (vectors, reranker, factories): + factories.side_effect = None + factories.return_value = object() + self.assertIs(vectors.get_model(), factories.return_value) + self.assertEqual(factories.call_args.args, ("Qwen/Qwen3-Embedding-0.6B",)) + self.assertEqual(factories.call_args.kwargs["truncate_dim"], 256) + self.assertEqual(factories.call_args.kwargs["device"], "cpu") + reranker.get_reranker(device="synthetic-explicit-device") + self.assertEqual(factories.call_args.args, ("synthetic-reranker",)) + self.assertEqual(factories.call_args.kwargs["device"], "synthetic-explicit-device") + with patch.object(self.client, "_spawn_server", side_effect=AssertionError("local mode spawned")): + self.assertFalse(self.client._start_server()) + + def test_sixteen_cross_process_cold_starters_launch_once(self) -> None: + clients = [subprocess.Popen([sys.executable, str(Path(__file__).resolve()), "--startup-racer", str(self.root)], + stdout=subprocess.PIPE, stderr=subprocess.PIPE) + for _ in range(16)] + try: + for child in clients: + _stdout, stderr = child.communicate(timeout=15) + self.assertEqual(child.returncode, 0, stderr.decode()) + finally: + for child in clients: + if child.poll() is None: + child.terminate() + child.wait(timeout=3) + self.assertEqual((self.root / "synthetic-launches").read_text().splitlines(), ["launch"]) + + def test_pending_child_survives_waiter_timeout_and_recovers_after_death(self) -> None: + live = {os.getpid(): 1.0, 7654321: 2.0} + ready = threading.Event() + launches = [] + self.client.process_birth = lambda pid: live.get(pid) + self.client._probe_server = lambda _deadline: ready.is_set() + def spawn(*_args: object) -> types.SimpleNamespace: + launches.append(True) + live[7654321] = float(len(launches) + 1) + return types.SimpleNamespace(pid=7654321) + self.client._spawn_server = spawn + self.assertFalse(self.client._start_server(.02)) + self.assertFalse(self.client._start_server(.02)) + self.assertEqual(len(launches), 1) + ready.set() + self.assertTrue(self.client._start_server(.02)) + ready.clear() # Idle exit/crash leaves no usable endpoint. + live.pop(7654321) + self.assertFalse(self.client._start_server(.02)) + self.assertEqual(len(launches), 2) + + def test_crashed_launcher_or_reused_child_identity_allows_one_replacement(self) -> None: + for child in (None, {"pid": 7654321, "born": 1.0}): + with self.subTest(child=child): + self.client._write_start_claim({"generation": "a" * 32, + "launcher": {"pid": 7654320, "born": 1.0}, "child": child}) + self.client.process_birth = lambda pid: {os.getpid(): 2.0, 7654321: 2.0}.get(pid) + self.client._spawn_server = Mock(return_value=types.SimpleNamespace(pid=7654321)) + self.assertFalse(self.client._start_server(.02)) + self.client._spawn_server.assert_called_once() + self.assertNotEqual(self.client.read_start_claim(self.client.START_STATE_PATH)["generation"], "a" * 32) + + def test_stale_pid_and_artifacts_do_not_imply_readiness_or_get_deleted(self) -> None: + self.client.PID_PATH.write_text(str(os.getpid())) + for name in ("SOCK_PATH", "PORT_PATH", "TOKEN_PATH"): + getattr(self.client, name).write_text("synthetic sentinel") + self.client._probe_server = lambda _deadline: False + self.client._spawn_server = Mock(return_value=types.SimpleNamespace(pid=7654321)) + self.assertFalse(self.client._start_server(.02)) + self.client._spawn_server.assert_called_once() + self.assertEqual(self.client.PID_PATH.read_text(), str(os.getpid())) + for name in ("SOCK_PATH", "PORT_PATH", "TOKEN_PATH"): + self.assertEqual(getattr(self.client, name).read_text(), "synthetic sentinel") + + def test_held_bind_lock_prevents_launch_even_without_endpoint_or_pid(self) -> None: + owner = self.client.try_file_lock(self.client.LOCK_PATH) + self.assertIsNotNone(owner) + try: + self.client._spawn_server = Mock(side_effect=AssertionError("existing owner duplicated")) + self.assertFalse(self.client._start_server(.02)) + self.client._spawn_server.assert_not_called() + finally: + os.close(owner) + + def test_busy_and_foreign_protocol_never_spawn(self) -> None: + self.client._server_ready = lambda: True + self.client._spawn_server = Mock(side_effect=AssertionError("responding endpoint restarted")) + self.client._send_request = Mock(side_effect=self.client.ModelServerBusyError("synthetic busy")) + self.assertTrue(self.client._start_server(.1)) + with self.assertRaises(self.client.ModelServerBusyError): + self.client.EmbeddingProxy().encode(["synthetic"]) + self.client._send_request.side_effect = self.client.ProtocolMismatchError("synthetic foreign") + with self.assertRaises(self.client.ProtocolMismatchError): + self.client._start_server(.1) + with self.assertRaises(self.client.ProtocolMismatchError): + self.client.EmbeddingProxy().encode(["synthetic"]) + self.client._spawn_server.assert_not_called() + + def test_deadline_bounds_gate_wait_and_prevents_late_spawn(self) -> None: + class Clock: + value = 0.0 + def monotonic(self) -> float: + return self.value + def sleep(self, amount: float) -> None: + self.value += amount + clock = Clock() + self.client.time = clock + self.client.try_file_lock = Mock(return_value=None) + self.client._spawn_server = Mock(side_effect=AssertionError("expired caller spawned")) + self.assertFalse(self.client._start_server(0)) + self.client.try_file_lock.assert_not_called() + self.assertFalse(self.client._start_server(.012)) + self.assertAlmostEqual(clock.value, .012) + self.client._spawn_server.assert_not_called() + + def test_app_setup_consumes_same_budget_and_expiry_stops_popen(self) -> None: + clock = types.SimpleNamespace(value=0.0) + self.client.time = types.SimpleNamespace(monotonic=lambda: clock.value) + def app_setup(deadline: float) -> None: + self.assertEqual(deadline, .1) + clock.value = .11 + return None + self.client._ensure_app_bundle = app_setup + with patch.object(subprocess, "Popen", side_effect=AssertionError("expired caller spawned")): + with self.assertRaises(TimeoutError): + self.client._spawn_server("a" * 32, .1) + + def test_expired_start_returns_timeout_and_legacy_retry_contract_survives(self) -> None: + clock = types.SimpleNamespace(value=0.0) + self.client.time = types.SimpleNamespace(monotonic=lambda: clock.value) + self.client._send_request = Mock(side_effect=FileNotFoundError("synthetic absent")) + def expire(wait_timeout: float | None = None) -> bool: + assert wait_timeout is not None + clock.value += wait_timeout + return False + self.client._start_server = expire + with self.assertRaises(TimeoutError): + self.client.EmbeddingProxy().encode(["synthetic"], timeout=.1) + self.client._send_request = Mock(side_effect=[socket.timeout("synthetic slow"), {"ok": True, "vectors": [[1]]}]) + self.client._start_server = Mock(return_value=True) + self.assertEqual(self.client.EmbeddingProxy().encode(["synthetic"]), [[1]]) + self.assertEqual(self.client._REQUEST_TIMEOUT, 120) + self.assertEqual(self.client._send_request.call_count, 2) + + def test_non_owner_cleanup_preserves_winner_between_bind_and_pid(self) -> None: + module, winner = self.make_server() + winner._lock_fd = winner._acquire_bind_lock() + module.SOCK_PATH.write_text("synthetic winning socket") + _module, loser = self.make_server() + with self.assertRaises(RuntimeError): + loser._lock_fd = loser._acquire_bind_lock() + loser._cleanup() + self.assertEqual(module.SOCK_PATH.read_text(), "synthetic winning socket") + winner._cleanup() + self.assertFalse(module.SOCK_PATH.exists()) + + def test_superseded_generation_releases_lock_without_touching_artifacts(self) -> None: + module, server = self.make_server() + self.client._write_start_claim({"generation": "b" * 32, + "launcher": {"pid": os.getpid(), "born": float(os.getpid())}, "child": None}) + os.environ[module._MODEL_SERVER_GENERATION_ENV] = "a" * 32 + server._lock_fd = server._acquire_bind_lock() + module.SOCK_PATH.write_text("synthetic untouched socket") + with self.assertRaisesRegex(RuntimeError, "superseded"): + server._validate_launch_generation_locked() + self.assertIsNone(server._lock_fd) + server._cleanup() + self.assertTrue(module.SOCK_PATH.exists()) + fd = self.client.try_file_lock(self.client.LOCK_PATH) + self.assertIsNotNone(fd) + os.close(fd) + + def test_shutdown_retains_bind_owner_while_admitted_native_work_drains(self) -> None: + module, server = self.make_server() + module._USE_UNIX = True # socketpair is the synthetic authenticated transport. + server._lock_fd = server._acquire_bind_lock() + retained_fd = server._lock_fd + module.PID_PATH.write_text(str(os.getpid())) + module.SOCK_PATH.write_text("synthetic owned socket") + model = object() + server._reranker = model + entered, release = threading.Event(), threading.Event() + def native_boundary(_request: dict) -> dict: + entered.set() + if not release.wait(2): + raise RuntimeError("synthetic native watchdog") + return {"ok": True} + server.handle_request = native_boundary + peer, connection = socket.socketpair() + data = json.dumps({"op": "synthetic-native"}).encode() + peer.sendall(struct.pack(">I", len(data)) + data) + future = server._dispatch_client(connection) + try: + self.assertTrue(entered.wait(1)) + started = time.monotonic() + server._cleanup() + self.assertLess(time.monotonic() - started, .5) + self.assertIs(server._reranker, model) + self.assertEqual(server._lock_fd, retained_fd) + self.assertTrue(module.SOCK_PATH.exists()) + self.assertIsNone(self.client.try_file_lock(self.client.LOCK_PATH)) + server._cleanup() + with self.assertRaisesRegex(RuntimeError, "cannot be restarted"): + server.run() + finally: + release.set() + future.result(timeout=2) + peer.close() + server._workers.shutdown(wait=True) + # The test process continues; emulate OS teardown of this owned + # synthetic fd without signaling any other process. + os.close(retained_fd) + server._lock_fd = None + + def test_shutdown_releases_model_references_before_bind_ownership(self) -> None: + for cyclic in (False, True): + with self.subTest(cyclic=cyclic): + module, server = self.make_server() + server._lock_fd = server._acquire_bind_lock() + module.PID_PATH.write_text(str(os.getpid())) + observed, references = [], [] + lock_path = self.client.LOCK_PATH + acquire = self.client.try_file_lock + + class Model: + def __init__(self, name: str) -> None: + self.name = name + if cyclic: + self.cycle = self + + def __del__(self) -> None: + fd = acquire(lock_path) + observed.append((self.name, fd is None)) + if fd is not None: + os.close(fd) + + for name in ("embed", "rerank", "fast"): + model = Model(name) + references.append(weakref.ref(model)) + if name == "embed": + server._embed_state = module._EmbedState(model, "base", "qwen3_256") + elif name == "rerank": + server._reranker = model + else: + server._fast_encoder = model + del model + server._cleanup() + self.assertEqual(sorted(observed), [("embed", True), ("fast", True), ("rerank", True)]) + self.assertTrue(all(reference() is None for reference in references)) + self.assertIsNone(server._lock_fd) + successor = acquire(lock_path) + self.assertIsNotNone(successor) + os.close(successor) + + def test_shutdown_collection_failure_retains_bind_ownership(self) -> None: + module, server = self.make_server() + server._lock_fd = server._acquire_bind_lock() + retained_fd = server._lock_fd + module.PID_PATH.write_text(str(os.getpid())) + module.SOCK_PATH.write_text("synthetic retained socket") + try: + with patch.object(module.gc, "collect", side_effect=RuntimeError("synthetic collection failure")): + with self.assertRaisesRegex(RuntimeError, "synthetic collection failure"): + server._cleanup() + self.assertEqual(server._lock_fd, retained_fd) + self.assertTrue(module.SOCK_PATH.exists()) + self.assertIsNone(self.client.try_file_lock(self.client.LOCK_PATH)) + server._cleanup() + self.assertEqual(server._lock_fd, retained_fd) + finally: + # Emulate OS exit for the synthetic server in this continuing test process. + if server._lock_fd is not None: + os.close(server._lock_fd) + server._lock_fd = None + + def test_current_generation_and_direct_launch_remain_valid(self) -> None: + module, server = self.make_server() + server._lock_fd = server._acquire_bind_lock() + server._validate_launch_generation_locked() + self.client._write_start_claim({"generation": "a" * 32, + "launcher": {"pid": os.getpid(), "born": float(os.getpid())}, "child": None}) + os.environ[module._MODEL_SERVER_GENERATION_ENV] = "a" * 32 + server._validate_launch_generation_locked() + + def test_windows_readiness_uses_real_token_connection_and_protocol(self) -> None: + self.client._USE_UNIX = False + token = bytes(range(32)) + self.client.PORT_PATH.write_text("12345") + self.client.TOKEN_PATH.write_text(token.hex()) + response = json.dumps({"ok": True, "protocol": 1}).encode() + incoming = bytearray(struct.pack(">I", len(response)) + response) + sent = [] + class FakeSocket: + def settimeout(self, _timeout: float) -> None: + pass + def connect(self, address: tuple[str, int]) -> None: + self.address = address + def sendall(self, data: bytes) -> None: + sent.append(data) + def recv(self, size: int) -> bytes: + value = bytes(incoming[:size]) + del incoming[:size] + return value + def close(self) -> None: + pass + with patch.object(socket, "socket", return_value=FakeSocket()): + self.assertTrue(self.client._probe_server(time.monotonic() + 1)) + self.assertEqual(sent[0], token) + self.client.TOKEN_PATH.write_text("invalid-token") + with patch.object(socket, "socket", side_effect=AssertionError("invalid token connected")): + self.assertFalse(self.client._probe_server(time.monotonic() + 1)) + + +if __name__ == "__main__": + if len(sys.argv) == 3 and sys.argv[1] == "--startup-racer": + racing_client(Path(sys.argv[2])) + else: + unittest.main() diff --git a/tests/test-mps-budget-743.py b/tests/test-mps-budget-743.py new file mode 100644 index 00000000..e658b306 --- /dev/null +++ b/tests/test-mps-budget-743.py @@ -0,0 +1,630 @@ +"""Real control flow with synthetic allocator APIs; no torch or model imports.""" +from __future__ import annotations + +import ast +from contextlib import contextmanager +from collections.abc import Iterator +import dataclasses +import logging +import re +import sys +import threading +import types +import unittest +from pathlib import Path +from unittest.mock import Mock, patch + +from packaging.requirements import Requirement + + +SOURCE = Path(__file__).resolve().parents[1] / "truememory" +GIB = 1024**3 +HIGH = "PYTORCH_MPS_HIGH_WATERMARK_RATIO" +LOW = "PYTORCH_MPS_LOW_WATERMARK_RATIO" + + +def source_module(path: str, dependencies: dict | None = None, + functions: tuple[str, ...] | None = None) -> types.ModuleType: + tree = ast.parse((SOURCE / path).read_text(encoding="utf-8")) + if functions is not None: + tree.body = [node for node in tree.body + if isinstance(node, ast.FunctionDef) and node.name in functions] + module = types.ModuleType("synthetic_743_" + path.replace("/", "_").replace(".", "_")) + module.__dict__.update(dependencies or {}) + with patch.dict(sys.modules, {module.__name__: module}): + exec(compile(ast.fix_missing_locations(tree), path, "exec"), module.__dict__) + return module + + +class BudgetTestCase(unittest.TestCase): + def setUp(self) -> None: + self.fresh() + + def fresh(self, env: dict[str, str] | None = None) -> None: + self.events: list[object] = [] + self.env = dict(env or {}) + self.budget = source_module("mps_utils.py") + self.budget.os = types.SimpleNamespace(environ=self.env) + self.recommended = Mock(side_effect=self.recommended_bytes) + self.setter = Mock(side_effect=lambda ratio: self.events.append(("set", ratio))) + self.physical = Mock(return_value=types.SimpleNamespace(total=32 * GIB)) + self.torch = types.ModuleType("torch") + self.torch.cuda = types.SimpleNamespace(is_available=lambda: False) + self.torch.backends = types.SimpleNamespace(mps=types.SimpleNamespace(is_available=lambda: True)) + self.torch.mps = types.SimpleNamespace( + recommended_max_memory=self.recommended, + set_per_process_memory_fraction=self.setter, + driver_allocated_memory=Mock(return_value=0), + ) + self.package = types.ModuleType("truememory") + self.package.__path__ = [] + self.package.mps_utils = self.budget + modules = { + "torch": self.torch, + "psutil": types.SimpleNamespace(virtual_memory=self.physical), + "truememory": self.package, + "truememory.mps_utils": self.budget, + } + patcher = patch.dict(sys.modules, modules) + patcher.start() + self.addCleanup(patcher.stop) + + def recommended_bytes(self) -> int: + self.assertIn(LOW, self.env, "LOW must be bootstrapped before allocator initialization") + self.events.append("recommended") + return 20 * GIB + + +class TestMPSBudget(BudgetTestCase): + def test_preserved_intended_byte_curve(self) -> None: + for physical, expected in ((8, 1.52), (12, 2.28), (16, 1.5), + (18, 1.5), (24, 1.92), (32, 2.5), (64, 2.5)): + with self.subTest(physical=physical): + self.assertEqual(self.budget._default_mps_budget_bytes(physical * GIB), int(expected * GIB)) + + def test_differing_recommended_sizes_enforce_the_same_bytes(self) -> None: + for recommended, fraction in ((20, 0.125), (25, 0.1)): + with self.subTest(recommended=recommended): + self.fresh() + def query() -> int: + self.recommended_bytes() + return recommended * GIB + self.recommended.side_effect = query + policy = self.budget.ensure_mps_memory_budget("mps") + self.assertEqual(policy.intended_bytes, int(2.5 * GIB)) + self.assertEqual(policy.effective_bytes, int(2.5 * GIB)) + self.assertEqual(policy.recommended_bytes, recommended * GIB) + self.assertEqual(policy.fraction, fraction) + self.assertEqual((policy.source, policy.enforcement), ("automatic", "enforced")) + self.assertEqual(self.events, ["recommended", ("set", fraction)]) + self.assertEqual(self.env, {LOW: "0.0"}) + self.setter.assert_called_once_with(float(fraction)) + self.assertIs(self.budget.get_mps_memory_budget(), policy) + with self.assertRaises(dataclasses.FrozenInstanceError): + policy.effective_bytes = 1 + + def test_explicit_operator_finite_and_unlimited_ratios_are_preserved(self) -> None: + for high, low in (("0.2", "0.1"), ("2", "2"), ("0", "1.4"), ("0", "0")): + with self.subTest(high=high, low=low): + self.fresh({HIGH: high, LOW: low}) + policy = self.budget.ensure_mps_memory_budget("mps") + expected = int(float(high) * 20 * GIB) if float(high) else None + self.assertEqual(policy.intended_bytes, expected) + self.assertEqual(policy.effective_bytes, expected) + self.assertEqual(policy.source, "operator") + self.assertEqual(policy.enforcement, "enforced" if expected else "disabled") + self.assertEqual(policy.requested_low_watermark_ratio, float(low)) + self.assertEqual(self.env, {HIGH: high, LOW: low}) + self.physical.assert_not_called() + self.setter.assert_called_once_with(float(high)) + + def test_invalid_operator_values_stop_before_query_or_success_state(self) -> None: + for key in (HIGH, LOW): + for invalid in ("", "nan", "NaN", "inf", "-inf", "-0.1", "2.01", "synthetic-invalid"): + with self.subTest(key=key, value=invalid): + self.fresh({key: invalid}) + with self.assertRaisesRegex(self.budget.MPSBudgetError, "finite ratio"): + self.budget.ensure_mps_memory_budget("mps") + self.recommended.assert_not_called() + self.setter.assert_not_called() + self.assertIsNone(self.budget.get_mps_memory_budget()) + + def test_low_high_conflicts_fail_before_model_allocation(self) -> None: + for env, expected_queries in (({HIGH: "0.1", LOW: "0.2"}, 0), + ({LOW: "1.8"}, 0), ({LOW: "0.2"}, 1)): + with self.subTest(env=env): + self.fresh(env) + with self.assertRaisesRegex(self.budget.MPSBudgetError, "low watermark exceeds"): + self.budget.ensure_mps_memory_budget("mps") + self.assertEqual(self.recommended.call_count, expected_queries) + self.setter.assert_not_called() + self.assertIsNone(self.budget.get_mps_memory_budget()) + + def test_missing_public_apis_or_invalid_metrics_never_publish_success(self) -> None: + for api in ("recommended_max_memory", "set_per_process_memory_fraction"): + with self.subTest(api=api): + self.fresh() + delattr(self.torch.mps, api) + with self.assertRaisesRegex(self.budget.MPSBudgetError, "public MPS") as raised: + self.budget.ensure_mps_memory_budget("mps") + self.assertIn("PyTorch>=2.5", str(raised.exception)) + self.assertIn("compatible build if available", str(raised.exception)) + self.assertIn("TRUEMEMORY_DEVICE=cpu", str(raised.exception)) + self.assertIn("restart", str(raised.exception)) + self.assertIsNone(self.budget.get_mps_memory_budget()) + for metric in ("physical", "recommended"): + for invalid in (0, -1, True, 4.5, float("nan"), None): + with self.subTest(metric=metric, value=invalid): + self.fresh() + if metric == "physical": + self.physical.return_value = types.SimpleNamespace(total=invalid) + else: + self.recommended.side_effect = None + self.recommended.return_value = invalid + with self.assertRaisesRegex(self.budget.MPSBudgetError, "positive integer byte count"): + self.budget.ensure_mps_memory_budget("mps") + self.setter.assert_not_called() + self.assertIsNone(self.budget.get_mps_memory_budget()) + + def test_failed_query_or_setter_is_terminal_without_false_success(self) -> None: + for failing in ("query", "setter"): + with self.subTest(failing=failing): + self.fresh() + target = self.recommended if failing == "query" else self.setter + target.side_effect = RuntimeError("synthetic allocator fault") + for _ in range(2): + with self.assertRaisesRegex(self.budget.MPSBudgetError, "restart this process"): + self.budget.ensure_mps_memory_budget("mps") + self.assertIsNone(self.budget.get_mps_memory_budget()) + self.assertEqual(target.call_count, 1) + self.assertIsNone(self.budget.ensure_mps_memory_budget("cpu")) + + def test_unsupported_derived_fraction_is_not_silently_clamped(self) -> None: + self.recommended.side_effect = None + self.recommended.return_value = GIB + with self.assertRaisesRegex(self.budget.MPSBudgetError, "supported ratio range"): + self.budget.ensure_mps_memory_budget("mps") + self.setter.assert_not_called() + self.assertIsNone(self.budget.get_mps_memory_budget()) + + def test_non_mps_selection_does_not_touch_allocator_or_environment(self) -> None: + for device in ("cpu", "cuda", "cuda:0", "xpu"): + with self.subTest(device=device): + self.assertIsNone(self.budget.ensure_mps_memory_budget(device)) + self.torch.cuda.is_available = lambda: True + self.assertIsNone(self.budget.ensure_mps_memory_budget(None)) + self.recommended.assert_not_called() + self.setter.assert_not_called() + self.physical.assert_not_called() + self.assertEqual(self.env, {}) + self.torch.cuda.is_available = lambda: False + self.assertIsNotNone(self.budget.ensure_mps_memory_budget(None)) + + def test_concurrent_initialization_publishes_one_complete_snapshot(self) -> None: + entered, release = threading.Event(), threading.Event() + results, errors = [], [] + def setter(ratio: float) -> None: + entered.set() + if not release.wait(2): + raise RuntimeError("synthetic setter did not receive release") + self.setter.side_effect = setter + def initialize() -> None: + try: + results.append(self.budget.ensure_mps_memory_budget("mps")) + except Exception as exc: + errors.append(exc) + threads = [threading.Thread(target=initialize) for _ in range(8)] + try: + for thread in threads: + thread.start() + self.assertTrue(entered.wait(2)) + self.assertIsNone(self.budget.get_mps_memory_budget()) + finally: + release.set() + for thread in threads: + thread.join(2) + self.assertFalse(any(thread.is_alive() for thread in threads)) + self.assertEqual(errors, []) + self.assertEqual(len(results), 8) + self.assertTrue(all(result is results[0] for result in results)) + self.recommended.assert_called_once_with() + self.setter.assert_called_once_with(0.125) + + +class TestFactoryCoverage(BudgetTestCase): + def setUp(self) -> None: + super().setUp() + self.configure_factories() + + def configure_factories(self) -> None: + self.factories: list[tuple[str, dict]] = [] + self.proxy = object() + self.use_server = Mock(return_value=False) + def transformer(name: str, **kwargs: object) -> object: + device = kwargs.get("device") + if device in (None, "mps"): + self.assertIsNotNone(self.budget.get_mps_memory_budget(), "constructor ran without budget") + self.events.append("construct") + self.factories.append((name, kwargs)) + return object() + fake_client = types.SimpleNamespace( + use_model_server=self.use_server, + get_embedding_proxy=lambda **kwargs: self.proxy, + get_reranker_proxy=lambda **kwargs: self.proxy, + ) + serving = source_module("tier_switch/serving.py") + runtime = types.SimpleNamespace(current_runtime_operation=lambda: None, + require_model_load_allowed=lambda: None, TierRuntimeError=RuntimeError) + fake_tiers = types.SimpleNamespace(resolve_custom_tier=lambda: {"embed_dim": 192}) + self.static = Mock(return_value=object()) + for name, module in { + "sentence_transformers": types.SimpleNamespace(SentenceTransformer=transformer, CrossEncoder=transformer), + "model2vec": types.SimpleNamespace(StaticModel=types.SimpleNamespace(from_pretrained=self.static)), + "truememory.model_client": fake_client, + "truememory.tier_switch.serving": serving, + "truememory.tier_switch.runtime": runtime, + "truememory.tier_config": fake_tiers, + "truememory.reranker": types.SimpleNamespace(get_current_reranker_name=lambda: "synthetic/reranker"), + }.items(): + patcher = patch.dict(sys.modules, {name: module}) + patcher.start() + self.addCleanup(patcher.stop) + + def local_embed(self, model_id: str) -> types.ModuleType: + return source_module("vector_search.py", { + "_model": None, "_frozen_embedding_target": None, "_embedding_dim": -1, "EMBEDDING_MODEL": model_id, + "_MODEL_DIMS": {"model2vec": 256, "minilm": 384, "bge-small": 384, "qwen3_256": 256}, + "_lock": threading.Lock(), "os": types.SimpleNamespace(environ=self.env), + "logger": logging.getLogger("synthetic743"), + }, ("get_model",)) + + def server(self): + tree = ast.parse((SOURCE / "model_server.py").read_text(encoding="utf-8")) + server = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == "ModelServer") + names = {"_build_embed_model", "_get_reranker", "_mark_sticky_cpu", "_flush_mps_cache", + "_check_process_memory"} + server.body = [node for node in server.body if isinstance(node, ast.FunctionDef) and node.name in names] + refusal = next(node for node in tree.body if isinstance(node, ast.ClassDef) + and node.name == "_ProcessMemoryRefused") + namespace = {"sys": sys, "os": types.SimpleNamespace(environ=self.env), + "log": logging.getLogger("synthetic743"), "gc": types.SimpleNamespace(collect=Mock())} + future = ast.ImportFrom(module="__future__", names=[ast.alias(name="annotations")], level=0) + exec(compile(ast.fix_missing_locations(ast.Module(body=[future, refusal, server], type_ignores=[])), + "model_server.py", "exec"), namespace) + instance = namespace["ModelServer"]() + instance._max_rss_bytes = 0 + instance._reranker = None + instance._reranker_name = None + instance._sticky_cpu = set() + instance._throttler = None + instance._write_status_file = Mock() + return instance + + def local_reranker(self) -> types.ModuleType: + return source_module("reranker.py", { + "_model": None, "_model_name": None, "_model_certified": False, + "contextmanager": contextmanager, "Iterator": Iterator, + "_lock": threading.Lock(), + "get_current_reranker_name": lambda: "synthetic/reranker", + "log": logging.getLogger("synthetic743"), + }, ("get_reranker", "_reranker_load_lock")) + + def test_all_local_embedding_factories_calibrate_before_constructor(self) -> None: + for model_id, dim in (("minilm", 384), ("bge-small", 384), ("qwen3_256", 256), + ("synthetic/custom", 192)): + for device in ("mps", "auto", "cpu", "cuda"): + with self.subTest(model=model_id, device=device): + self.fresh({"TRUEMEMORY_DEVICE": device, "TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD": "1"}) + self.configure_factories() + self.torch.cuda.is_available = lambda: device == "cuda" + loader = self.local_embed(model_id) + loaded = loader.get_model() + self.assertIs(loader.get_model(), loaded) + self.assertEqual(loader._embedding_dim, dim) + self.assertEqual(len(self.factories), 1) + self.assertEqual(self.factories[0][1]["device"], + {"mps": "mps", "auto": None, "cpu": "cpu", "cuda": "cuda:0"}[device]) + if device in ("mps", "auto"): + self.assertEqual(self.events, ["recommended", ("set", 0.125), "construct"]) + else: + self.assertEqual(self.events, ["construct"]) + self.assertNotIn(LOW, self.env) + + def test_server_embedding_factories_and_both_rerankers_share_one_budget(self) -> None: + self.env["TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD"] = "1" + server = self.server() + for model_id in ("qwen3_256", "synthetic/custom"): + server._build_embed_model(model_id, "mps") + server._get_reranker("synthetic/reranker") + self.local_reranker().get_reranker("synthetic/reranker", device="mps") + self.assertEqual(len(self.factories), 4) + self.assertEqual(self.events[:2], ["recommended", ("set", 0.125)]) + self.recommended.assert_called_once_with() + self.setter.assert_called_once_with(0.125) + + def test_cpu_fast_encoder_sticky_reranker_and_explicit_cpu_skip_budget(self) -> None: + server = self.server() + server._build_embed_model("qwen3_256", "cpu") + server._sticky_cpu.add("rerank") + server._get_reranker("synthetic/reranker") + self.local_reranker().get_reranker("synthetic/reranker", device="cpu") + self.assertEqual([kwargs["device"] for _, kwargs in self.factories], ["cpu"] * 3) + self.assertEqual(self.events, ["construct"] * 3) + self.assertEqual(self.env, {}) + + def test_proxy_and_static_factories_never_configure_local_mps(self) -> None: + self.use_server.return_value = True + self.assertIs(self.local_embed("qwen3_256").get_model(), self.proxy) + self.assertIs(self.local_reranker().get_reranker(), self.proxy) + self.use_server.return_value = False + for model_id in ("model2vec", "synthetic/not-opted-in"): + self.local_embed(model_id).get_model() + self.server()._build_embed_model(model_id, "mps") + self.recommended.assert_not_called() + self.setter.assert_not_called() + self.assertEqual(self.factories, []) + self.assertEqual(self.static.call_count, 4) + self.assertEqual(self.env, {}) + + def test_failed_setup_prevents_all_transformer_construction_and_cache_publish(self) -> None: + self.setter.side_effect = RuntimeError("synthetic failed setup") + embed = self.local_embed("qwen3_256") + rerank = self.local_reranker() + server = self.server() + for load in (embed.get_model, rerank.get_reranker, + lambda: server._build_embed_model("qwen3_256", "mps"), server._get_reranker): + with self.assertRaises(self.budget.MPSBudgetError): + load() + self.assertEqual(self.factories, []) + self.assertIsNone(embed._model) + self.assertIsNone(rerank._model) + self.assertIsNone(server._reranker) + self.assertIsNone(self.budget.get_mps_memory_budget()) + self.setter.assert_called_once_with(0.125) + + def test_server_sticky_flags_do_not_switch_the_whole_shared_throttler_to_cpu(self) -> None: + server = self.server() + server._throttler = types.SimpleNamespace(set_device=Mock()) + self.assertTrue(server._mark_sticky_cpu("rerank")) + server._throttler.set_device.assert_not_called() + self.assertTrue(server._mark_sticky_cpu("embed")) + self.assertFalse(server._mark_sticky_cpu("embed")) + server._throttler.set_device.assert_not_called() + + def test_pre_model_cleanup_cannot_initialize_allocator_before_low_bootstrap(self) -> None: + self.torch.mps.empty_cache = Mock() + self.torch.mps.synchronize = Mock() + self.torch.cuda.empty_cache = Mock() + self.budget.gc = types.SimpleNamespace(collect=Mock()) + idle = source_module("mcp_server.py", { + "_is_still_idle": lambda: True, "_get_rss_mb": lambda: 0, + "log": logging.getLogger("synthetic743"), "gc": types.SimpleNamespace(collect=Mock()), + }, ("_unload_models",)) + with patch.dict(sys.modules, { + "truememory.vector_search": types.SimpleNamespace(unload_model=Mock()), + "truememory.reranker": types.SimpleNamespace(unload_reranker=Mock(return_value=True)), + }): + for initialized in (False, True): + if initialized: + self.budget.ensure_mps_memory_budget("mps") + self.budget.flush_mps_cache() + self.server()._flush_mps_cache() + idle._unload_models() + self.assertEqual(self.torch.mps.empty_cache.call_count, 3 if initialized else 0) + self.assertEqual(self.torch.mps.synchronize.call_count, 2 if initialized else 0) + if not initialized: + self.assertEqual(self.env, {}) + self.recommended.assert_not_called() + self.assertEqual(self.torch.cuda.empty_cache.call_count, 2) + + +class TestThrottlerBudget(BudgetTestCase): + def setUp(self) -> None: + super().setUp() + self.sensors = source_module("tier_switch/sensors.py") + self.state = source_module("tier_switch/state_machine.py") + patcher = patch.dict(sys.modules, { + "truememory.tier_switch.sensors": self.sensors, + "truememory.tier_switch.state_machine": self.state, + }) + patcher.start() + self.addCleanup(patcher.stop) + self.module = source_module("tier_switch/throttler.py") + self.now = 0.0 + self.clock = types.SimpleNamespace(monotonic=lambda: self.now, sleep=lambda seconds: None) + for module in (self.sensors, self.state, self.module): + module.time = self.clock + self.module.read_thermal_pressure = lambda: {"status": "ok", "scheduler_limit": 100} + + def test_memory_and_growth_use_effective_bytes_not_physical_ram(self) -> None: + policy = self.budget.ensure_mps_memory_budget("mps") + throttler = self.module.DynamicThrottler("mps") + self.assertEqual(throttler.mps_cap_gb, 2.5) + for ratio, status in ((0.84, "ok"), (0.85, "warning"), (0.95, "critical")): + with self.subTest(ratio=ratio): + self.now += 20 + self.torch.mps.driver_allocated_memory.return_value = int(ratio * policy.effective_bytes) + readings = throttler._read_all_channels() + self.assertAlmostEqual(readings["mps_level"]["ratio"], ratio) + self.assertEqual(readings["mps_level"]["status"], status) + self.assertEqual(throttler.growth_tracker._cap_gb, 2.5) + self.assertAlmostEqual(readings["growth_rate"]["slope_pct"], 10) + self.assertEqual(throttler._build_metrics()["mps_budget"], { + "scope": "process_mps_allocator", + "intended_bytes": policy.intended_bytes, "recommended_bytes": 20 * GIB, + "effective_bytes": policy.effective_bytes, "source": "automatic", "enforcement": "enforced", + }) + + def test_missing_and_unlimited_budget_cannot_invent_healthy_headroom(self) -> None: + for unlimited in (False, True): + with self.subTest(unlimited=unlimited): + if unlimited: + self.env[HIGH] = "0" + self.budget.ensure_mps_memory_budget("mps") + throttler = self.module.DynamicThrottler("mps") + for _ in range(5): + self.now += 20 + readings = throttler._read_all_channels() + throttler.state_machine.safety_check(readings) + for channel in ("mps_level", "growth_rate"): + self.assertEqual(readings[channel]["status"], "unknown") + self.assertTrue(readings[channel].get("required", True)) + self.assertEqual(throttler.state_machine.good_streak, 0) + self.assertIsNone(throttler.mps_cap_gb) + self.assertIsNone(throttler.growth_tracker) + self.assertIsNone(throttler._build_metrics()["mps_budget"]["effective_bytes"]) + self.torch.mps.driver_allocated_memory.assert_not_called() + if not unlimited: + self.recommended.assert_not_called() + self.setter.assert_not_called() + + def test_budget_initialized_after_throttler_becomes_usable_on_next_sample(self) -> None: + throttler = self.module.DynamicThrottler("mps") + self.assertEqual(throttler._read_all_channels()["mps_level"]["status"], "unknown") + self.budget.ensure_mps_memory_budget("mps") + self.assertEqual(throttler._read_all_channels()["mps_level"]["status"], "ok") + self.assertEqual(throttler.mps_cap_gb, 2.5) + + def test_mixed_device_daemon_keeps_monitoring_the_surviving_mps_model(self) -> None: + policy = self.budget.ensure_mps_memory_budget("mps") + tree = ast.parse((SOURCE / "model_server.py").read_text(encoding="utf-8")) + server = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == "ModelServer") + mark = next(node for node in server.body if isinstance(node, ast.FunctionDef) and node.name == "_mark_sticky_cpu") + namespace = {"log": logging.getLogger("synthetic743")} + exec(compile(ast.fix_missing_locations(ast.Module(body=[mark], type_ignores=[])), + "model_server.py", "exec"), namespace) + for fallback in ("embed", "rerank"): + # CPU covers throttler reactivation after sticky embedding; MPS + # covers a throttler activated before either model's fallback. + for initial_device in ("cpu", "mps"): + with self.subTest(fallback=fallback, initial_device=initial_device): + throttler = self.module.DynamicThrottler(initial_device) + owner = types.SimpleNamespace( + _sticky_cpu=set(), _throttler=throttler, _write_status_file=Mock(), + _embed_state=types.SimpleNamespace(model=types.SimpleNamespace( + device="cpu" if fallback == "embed" else "mps")), + _reranker=types.SimpleNamespace(device="cpu" if fallback == "rerank" else "mps"), + ) + namespace["_mark_sticky_cpu"](owner, fallback) + self.torch.mps.driver_allocated_memory.return_value = int(0.95 * policy.effective_bytes) + readings = throttler._read_all_channels() + self.assertEqual(readings["mps_level"]["status"], "critical") + self.assertAlmostEqual(readings["mps_level"]["ratio"], 0.95) + throttler.batch_size = 8 + throttler.state_machine.safety_check(readings) + self.assertEqual(throttler.batch_size, 4) + metrics = throttler._build_metrics()["mps_budget"] + self.assertEqual(metrics["scope"], "process_mps_allocator") + self.assertEqual(metrics["effective_bytes"], policy.effective_bytes) + + def test_cpu_and_cuda_never_claim_mps_protection_or_initialize_it(self) -> None: + for device in ("cpu", "cuda:0"): + with self.subTest(device=device): + throttler = self.module.DynamicThrottler(device) + readings = throttler._read_all_channels() + for channel in ("mps_level", "growth_rate"): + self.assertEqual(readings[channel]["status"], "unsupported") + self.assertFalse(readings[channel]["required"]) + self.assertEqual(throttler._build_metrics()["mps_budget"]["enforcement"], "not_applicable") + self.assertIsNone(throttler.mps_cap_gb) + self.assertEqual(self.env, {}) + self.recommended.assert_not_called() + + def test_throttler_cleanup_skips_uninitialized_mps_and_preserves_cuda_cleanup(self) -> None: + self.torch.mps.empty_cache = Mock() + self.torch.mps.synchronize = Mock() + self.torch.cuda.empty_cache = Mock() + self.torch.cuda.is_available = lambda: True + self.module.DynamicThrottler.flush_gpu_cache() + self.torch.mps.empty_cache.assert_not_called() + self.torch.cuda.empty_cache.assert_called_once_with() + self.recommended.assert_not_called() + self.assertEqual(self.env, {}) + self.budget.ensure_mps_memory_budget("mps") + self.module.DynamicThrottler.flush_gpu_cache() + self.torch.mps.empty_cache.assert_called_once_with() + self.torch.mps.synchronize.assert_called_once_with() + + def test_new_mps_budget_invalidates_pending_cpu_only_sample(self) -> None: + throttler = self.module.DynamicThrottler("cpu") + throttler.state_machine.batch_count = 5 + throttler.state_machine.good_streak = 2 + throttler._samples.append((0, {})) + entered, release = threading.Event(), threading.Event() + errors = [] + def sensor() -> dict: + entered.set() + if not release.wait(2): + raise RuntimeError("synthetic probe did not receive release") + return {"scheduler_limit": 100, "status": "ok"} + self.module.read_thermal_pressure = sensor + def sample() -> None: + try: + throttler._sample_if_due() + except Exception as exc: + errors.append(exc) + thread = threading.Thread(target=sample) + thread.start() + try: + self.assertTrue(entered.wait(2)) + self.budget.ensure_mps_memory_budget("mps") + finally: + release.set() + thread.join(2) + self.assertFalse(thread.is_alive()) + self.assertEqual(errors, []) + self.assertEqual(throttler.state_machine.good_streak, 0) + self.assertEqual(list(throttler._samples), []) + self.assertIsNone(throttler._last_sample_time) + self.assertEqual(throttler._last_readings, {}) + self.assertEqual(throttler._build_metrics()["mps_budget"]["scope"], "process_mps_allocator") + self.assertEqual(throttler._read_all_channels()["mps_level"]["status"], "ok") + + +class TestTorchMetadata(unittest.TestCase): + def test_apple_silicon_minimum_preserves_other_platforms_and_upper_bound(self) -> None: + metadata = (SOURCE.parent / "pyproject.toml").read_text(encoding="utf-8") + match = re.search(r"(?ms)^dependencies\s*=\s*(\[.*?^\])", metadata) + self.assertIsNotNone(match) + # The string-only dependency array is also a Python literal. This + # avoids a TOML dependency for the supported Python 3.10 test runner. + requirements = [Requirement(value) for value in ast.literal_eval(match.group(1))] + torch_requirements = [requirement for requirement in requirements if requirement.name == "torch"] + for platform, machine in (("darwin", "arm64"), ("darwin", "x86_64"), + ("linux", "x86_64"), ("linux", "aarch64"), + ("win32", "AMD64"), ("win32", "ARM64")): + apple_silicon = platform == "darwin" and machine == "arm64" + environment = {"sys_platform": platform, "platform_machine": machine} + active = [requirement for requirement in torch_requirements + if requirement.marker is None or requirement.marker.evaluate(environment)] + with self.subTest(platform=platform, machine=machine): + self.assertEqual(len(active), 2 if apple_silicon else 1) + for version in ("2.0.0", "2.4.1", "2.5.0", "2.14.1", "1.13.1", "3.0.0"): + expected = version in ("2.5.0", "2.14.1") or ( + not apple_silicon and version in ("2.0.0", "2.4.1") + ) + with self.subTest(version=version): + self.assertEqual(all(version in requirement.specifier for requirement in active), expected) + + +class TestStartupEnvironment(unittest.TestCase): + def test_startup_keeps_thread_defaults_without_exporting_generated_watermarks(self) -> None: + for explicit in ({}, {HIGH: "0.2", LOW: "0.1", "OMP_NUM_THREADS": "3"}): + with self.subTest(explicit=explicit): + env = dict(explicit) + startup = source_module("model_server.py", {"os": types.SimpleNamespace(environ=env)}, + ("_set_mps_memory_cap",)) + startup._set_mps_memory_cap() + self.assertEqual({key: env[key] for key in (HIGH, LOW) if key in env}, + {key: explicit[key] for key in (HIGH, LOW) if key in explicit}) + self.assertEqual(env["OMP_NUM_THREADS"], explicit.get("OMP_NUM_THREADS", "1")) + for key in ("MKL_NUM_THREADS", "OPENBLAS_NUM_THREADS", "NUMEXPR_MAX_THREADS"): + self.assertEqual(env[key], "1") + # MCP must leave calibration to whichever process actually owns models. + tree = ast.parse((SOURCE / "mcp_server.py").read_text(encoding="utf-8")) + self.assertFalse(any(isinstance(node, ast.Constant) and node.value in (HIGH, LOW) + for node in ast.walk(tree))) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-native-hook-output-763.py b/tests/test-native-hook-output-763.py new file mode 100644 index 00000000..e810c8a1 --- /dev/null +++ b/tests/test-native-hook-output-763.py @@ -0,0 +1,498 @@ +"""Synthetic stdlib controls for native recall output, without package imports. + +The decoders model documented schemas. They do not execute native host parsers +or prove that a native model received the context. Retrieval and capture are +inert collaborators; the actual hook main/parser and serializer execute. +""" +from __future__ import annotations + +import argparse +import ast +import importlib.util +import io +import json +import os +import shlex +import subprocess +import sys +import types +import unittest +from pathlib import Path +from typing import Callable, cast +from unittest.mock import patch + +TEST_ROOT = Path(__file__).resolve().parent.parent +SOURCE_ROOT = Path(os.environ.get("TRUEMEMORY_HOOK_CONTRACT_SOURCE_ROOT", str(TEST_ROOT))) +HOOK_DIR = SOURCE_ROOT / "truememory" / "ingest" / "hooks" +HELPER_ROOT = Path(os.environ.get("TRUEMEMORY_HOOK_CONTRACT_HELPER_ROOT", str(TEST_ROOT))) +_OUTPUT_PATH = TEST_ROOT / "truememory" / "ingest" / "hooks" / "output.py" +_SPEC = importlib.util.spec_from_file_location("synthetic_hook_output_763", _OUTPUT_PATH) +assert _SPEC is not None and _SPEC.loader is not None +output = importlib.util.module_from_spec(_SPEC) +_SPEC.loader.exec_module(output) + +SYNTHETIC_CONTEXT = "synthetic blue lantern" +NATIVE_EVENTS = ( + ("cursor", "sessionStart"), + ("gemini", "SessionStart"), + ("gemini", "BeforeAgent"), + ("codex", "SessionStart"), + ("codex", "UserPromptSubmit"), +) + + +def read_source(source: Path) -> str: + commit = os.environ.get("TRUEMEMORY_HOOK_CONTRACT_SOURCE_COMMIT", "") + if not commit: + return source.read_text(encoding="utf-8") + relative = source.relative_to(SOURCE_ROOT) + return subprocess.run( + ["git", "show", f"{commit}:{relative}"], cwd=SOURCE_ROOT, + capture_output=True, text=True, check=True, + ).stdout + + +def load_shared_helpers() -> dict[str, object]: + namespace: dict[str, object] = {"Path": Path, "re": __import__("re"), "sys": sys, "shlex": shlex} + for path, functions in ( + ("truememory/hooks/adapters/base.py", {"is_owned_hook_command"}), + ("truememory/ingest/hooks/_shared.py", {"normalize_transcript_host", "hook_session_id"}), + ): + source = HELPER_ROOT / path + tree = ast.parse(source.read_text(encoding="utf-8"), filename=str(source)) + selected = [node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name in functions] + if {node.name for node in selected} != functions: + raise AssertionError("Required peer helper is absent; compose #761 before package execution") + exec(compile(ast.Module(body=selected, type_ignores=[]), str(source), "exec"), namespace) + return namespace + + +SHARED_HELPERS = load_shared_helpers() + + +def decode_fixture(host: str, event: str, response: str) -> str | None: + """Strict synthetic protocol fixture, independently spelled field paths.""" + data = json.loads(response) + if host == "cursor": + if event != "sessionStart" or set(data) != {"additional_context"}: + return None + context = data["additional_context"] + elif host in {"gemini", "codex"}: + events = {"SessionStart", "BeforeAgent"} if host == "gemini" else {"SessionStart", "UserPromptSubmit"} + if event not in events or set(data) != {"hookSpecificOutput"}: + return None + specific = data["hookSpecificOutput"] + if not isinstance(specific, dict) or set(specific) != {"hookEventName", "additionalContext"}: + return None + if specific["hookEventName"] != event: + return None + context = specific["additionalContext"] + else: + if event not in {"SessionStart", "UserPromptSubmit"} or set(data) != {"additionalContext"}: + return None + context = data["additionalContext"] + return context if isinstance(context, str) else None + + +class RecordingStream(io.StringIO): + def __init__(self, failure: str = "") -> None: + super().__init__() + self.failure = failure + self.flushed = False + + def write(self, text: str) -> int: + if self.failure == "write": + raise BrokenPipeError("synthetic write refusal") + if self.failure == "short": + return super().write(text[:len(text) // 2]) + return super().write(text) + + def flush(self) -> None: + if self.failure == "flush": + raise BrokenPipeError("synthetic flush refusal") + self.flushed = True + + +def load_functions(script: str, names: set[str]) -> dict[str, object]: + source = HOOK_DIR / script + tree = ast.parse(read_source(source), filename=str(source)) + selected = [node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name in names] + if {node.name for node in selected} != names: + raise AssertionError("Missing production function requested by fixture") + namespace: dict[str, object] = { + "argparse": argparse, "os": os, "json": json, "sys": sys, "Path": Path, + "HOOK_HOSTS": output.HOOK_HOSTS, "emit_context": output.emit_context, + "supports_context": output.supports_context, + } + exec(compile(ast.Module(body=selected, type_ignores=[]), str(source), "exec"), namespace) + return namespace + + +class HookHarness: + def __init__(self) -> None: + self.markers: set[str] = set() + self.flush_at_mark: list[bool] = [] + self.calls: list[tuple[str, object]] = [] + self.buffer_sessions: list[str] = [] + self.counter_sessions: list[str] = [] + self.store_sessions: list[str] = [] + self.first_run = False + self.recall_context = SYNTHETIC_CONTEXT + self.notice = "" + self.intensity = "standard" + self.admitted = False + self.stream = RecordingStream() + self.shared = types.ModuleType("truememory.ingest.hooks._shared") + self.shared.mark_recall_injected = self.mark # type: ignore[attr-defined] + self.shared.hook_session_id = SHARED_HELPERS["hook_session_id"] # type: ignore[attr-defined] + self.shared.consume_recall_injected = self.consume # type: ignore[attr-defined] + self.shared.is_allowed_transcript = self.admit # type: ignore[attr-defined] + self.shared.should_extract_session = lambda *_a: True # type: ignore[attr-defined] + self.shared.mark_session_extracted = lambda *_a, **_k: self.calls.append(("extracted", None)) # type: ignore[attr-defined] + self.stop = types.ModuleType("truememory.ingest.hooks.stop") + self.stop.TRACE_DIR = types.SimpleNamespace(mkdir=lambda **_k: None) # type: ignore[attr-defined] + self.stop.LOG_DIR = types.SimpleNamespace(mkdir=lambda **_k: None) # type: ignore[attr-defined] + self.stop._has_enough_messages = lambda *_a: self.calls.append(("read-transcript", _a)) or True # type: ignore[attr-defined] + self.stop._run_background_ingestion = self.background # type: ignore[attr-defined] + + def mark(self, session: str) -> None: + self.calls.append(("mark", session)) + self.flush_at_mark.append(self.stream.flushed) + self.markers.add(session) + + def consume(self, session: str) -> bool: + self.calls.append(("consume", session)) + found = session in self.markers + self.markers.discard(session) + return found + + def admit(self, transcript: str, *, host: str) -> bool: + self.calls.append(("admit", (transcript, host))) + return self.admitted + + def background(self, *args: object, transcript_host: str) -> int: + self.calls.append(("spawn", (args, transcript_host))) + return 21 + + def buffer(self, session: str, _prompt: str) -> None: + self.buffer_sessions.append(session) + self.calls.append(("buffer", None)) + + def count(self, session: str) -> int: + self.counter_sessions.append(session) + return 5 + + def store(self, _prompt: str, session: str, *_args: object) -> None: + self.store_sessions.append(session) + self.calls.append(("store", None)) + + def run( + self, script: str, host: str, event: str, *, failure: str = "", + payload: dict[str, object] | None = None, + ) -> str: + namespace = load_functions(script, {"main"}) + self.stream = RecordingStream(failure) + namespace.update({ + "_parse_args": lambda: argparse.Namespace(host=host, event=event, user="synthetic-user", db="", scan_stale=False), + "_run_maintenance_background": lambda *_a: None, + "_get_search_intensity": lambda: "standard", + "_INTENSITY_MEMORY_LIMITS": {"standard": 25}, "_BASE_MEMORY_LIMIT": 25, + "_intensity_budget": lambda *_a: 8192, "_is_first_run": lambda: self.first_run, + "_first_run_context": lambda: "synthetic onboarding", + "recall_memories": lambda *_a, **_k: self.recall_context, + "_check_for_update": lambda: self.notice, "_check_email_needed": lambda: "", + "log": types.SimpleNamespace(error=lambda *_a: self.calls.append(("hook-error", None))), + "buffer_message": self.buffer, + "_prune_old_buffers": lambda: None, "_try_capture_email": lambda *_a: None, + "_get_intensity_config": lambda: (self.intensity, "standard"), + "_increment_prompt_count": self.count, + "_try_per_exchange_store": self.store, + "_try_auto_recall": self.auto_recall, "_try_proactive_recall": self.proactive_recall, + }) + data = payload if payload is not None else {"session_id": "synthetic-session", "prompt": "Recall synthetic color"} + with patch.dict(sys.modules, {self.shared.__name__: self.shared, self.stop.__name__: self.stop}), patch.object(sys, "stdin", io.StringIO(json.dumps(data))), patch.object(sys, "stdout", self.stream), patch.dict(os.environ, {"TRUEMEMORY_EXTRACTION": ""}): + cast(Callable[[], None], namespace["main"])() + return self.stream.getvalue() + + def auto_recall(self, *_args: object, debounced: bool) -> str | None: + self.calls.append(("auto", debounced)) + return None if debounced else SYNTHETIC_CONTEXT + + def proactive_recall(self, *_args: object, debounced: bool) -> tuple[str | None, bool]: + self.calls.append(("proactive", debounced)) + return (None if debounced else SYNTHETIC_CONTEXT), True + + +class TestNativeOutput(unittest.TestCase): + def test_official_event_response_fixtures(self) -> None: + for host, event in NATIVE_EVENTS + (("claude", "SessionStart"), ("claude", "UserPromptSubmit")): + with self.subTest(host=host, event=event): + stream = RecordingStream() + self.assertTrue(output.emit_context(SYNTHETIC_CONTEXT, host=host, event=event, stream=stream)) + self.assertTrue(stream.flushed) + self.assertEqual(decode_fixture(host, event, stream.getvalue()), SYNTHETIC_CONTEXT) + self.assertEqual(stream.getvalue().count("\n"), 1) + + def test_legacy_payload_rejected_by_native_fixtures(self) -> None: + legacy = json.dumps({"additionalContext": SYNTHETIC_CONTEXT}) + for host, event in NATIVE_EVENTS: + with self.subTest(host=host, event=event): + self.assertIsNone(decode_fixture(host, event, legacy)) + + def test_wrong_events_and_hosts_emit_nothing(self) -> None: + for host, event in (("cursor", "beforeSubmitPrompt"), ("cursor", "SessionStart"), ("gemini", "UserPromptSubmit"), ("codex", "BeforeAgent"), ("codex", "Stop"), ("claude", "BeforeAgent"), ("unknown", "SessionStart"), ("codex ", "SessionStart")): + with self.subTest(host=host, event=event): + stream = RecordingStream() + self.assertFalse(output.emit_context(SYNTHETIC_CONTEXT, host=host, event=event, stream=stream)) + self.assertEqual(stream.getvalue(), "") + self.assertFalse(stream.flushed) + + def test_io_failures_do_not_report_emitted(self) -> None: + for failure in ("write", "flush", "short"): + with self.subTest(failure=failure): + self.assertFalse(output.emit_context(SYNTHETIC_CONTEXT, host="codex", event="SessionStart", stream=RecordingStream(failure))) + + def test_empty_context_emits_nothing(self) -> None: + stream = RecordingStream() + self.assertFalse(output.emit_context("", host="codex", event="SessionStart", stream=stream)) + self.assertEqual(stream.getvalue(), "") + + def test_session_main_uses_native_contract_and_marks_after_flush(self) -> None: + for host, event in (("cursor", "sessionStart"), ("gemini", "SessionStart"), ("codex", "SessionStart"), ("claude", "SessionStart")): + with self.subTest(host=host): + harness = HookHarness() + response = harness.run("session_start.py", host, event) + self.assertEqual(decode_fixture(host, event, response), SYNTHETIC_CONTEXT) + self.assertEqual(harness.markers, {"synthetic-session"}) + self.assertEqual(harness.flush_at_mark, [True]) + self.assertEqual([call for call in harness.calls if call[0] == "mark"], [("mark", "synthetic-session")]) + + def test_session_failed_emission_never_marks(self) -> None: + for failure in ("write", "flush", "short"): + with self.subTest(failure=failure): + harness = HookHarness() + harness.run("session_start.py", "codex", "SessionStart", failure=failure) + self.assertEqual(harness.markers, set()) + self.assertFalse([call for call in harness.calls if call[0] == "mark"]) + response = harness.run("user_prompt_submit.py", "codex", "UserPromptSubmit") + self.assertEqual(decode_fixture("codex", "UserPromptSubmit", response), SYNTHETIC_CONTEXT) + + def test_session_unsupported_event_never_marks(self) -> None: + harness = HookHarness() + self.assertEqual(harness.run("session_start.py", "cursor", "beforeSubmitPrompt"), "") + self.assertEqual(harness.markers, set()) + + def test_notices_and_onboarding_do_not_suppress_prompt_recall(self) -> None: + for first_run in (False, True): + with self.subTest(first_run=first_run): + harness = HookHarness() + harness.first_run = first_run + harness.recall_context = "" + harness.notice = "synthetic update" + response = harness.run("session_start.py", "gemini", "SessionStart") + context = decode_fixture("gemini", "SessionStart", response) + self.assertIsInstance(context, str) + self.assertIn("synthetic update", cast(str, context)) + self.assertEqual(harness.markers, set()) + response = harness.run("user_prompt_submit.py", "gemini", "BeforeAgent") + self.assertEqual(decode_fixture("gemini", "BeforeAgent", response), SYNTHETIC_CONTEXT) + + def test_native_prompt_main_preserves_once_only_debounce(self) -> None: + for host, event in (("gemini", "BeforeAgent"), ("codex", "UserPromptSubmit"), ("claude", "UserPromptSubmit")): + for intensity in ("standard", "max"): + with self.subTest(host=host, intensity=intensity): + harness = HookHarness() + harness.intensity = intensity + harness.run("session_start.py", host, "SessionStart") + self.assertEqual(harness.run("user_prompt_submit.py", host, event), "") + self.assertEqual(harness.markers, set()) + response = harness.run("user_prompt_submit.py", host, event) + self.assertEqual(decode_fixture(host, event, response), SYNTHETIC_CONTEXT) + self.assertEqual(len([call for call in harness.calls if call[0] == "consume"]), 2) + if intensity == "max": + self.assertFalse([call for call in harness.calls if call[0] == "auto"]) + + def test_cursor_prompt_capture_without_unsupported_recall(self) -> None: + harness = HookHarness() + harness.intensity = "max" + response = harness.run("user_prompt_submit.py", "cursor", "beforeSubmitPrompt") + self.assertEqual(response, "") + self.assertIn(("buffer", None), harness.calls) + self.assertIn(("store", None), harness.calls) + self.assertFalse([call for call in harness.calls if call[0] in {"auto", "proactive", "consume"}]) + + def test_prompt_emission_failure_does_not_escape_or_set_markers(self) -> None: + for host, event in (("gemini", "BeforeAgent"), ("codex", "UserPromptSubmit"), ("claude", "UserPromptSubmit")): + for failure in ("write", "flush", "short"): + with self.subTest(host=host, failure=failure): + harness = HookHarness() + harness.run("user_prompt_submit.py", host, event, failure=failure) + self.assertEqual(harness.markers, set()) + self.assertFalse([call for call in harness.calls if call[0] == "mark"]) + + def test_short_first_prompt_consumes_only_its_marker(self) -> None: + harness = HookHarness() + harness.run("session_start.py", "codex", "SessionStart") + data = {"session_id": "synthetic-session", "prompt": "hi"} + self.assertEqual(harness.run("user_prompt_submit.py", "codex", "UserPromptSubmit", payload=data), "") + self.assertEqual(harness.markers, set()) + self.assertEqual(len([call for call in harness.calls if call[0] == "consume"]), 1) + response = harness.run("user_prompt_submit.py", "codex", "UserPromptSubmit") + self.assertEqual(decode_fixture("codex", "UserPromptSubmit", response), SYNTHETIC_CONTEXT) + + def test_untrusted_stdin_cannot_select_host_or_event(self) -> None: + harness = HookHarness() + data = {"session_id": "synthetic-session", "host": "cursor", "hook_event_name": "beforeSubmitPrompt", "event": "BeforeAgent"} + response = harness.run("session_start.py", "codex", "SessionStart", payload=data) + self.assertEqual(decode_fixture("codex", "SessionStart", response), SYNTHETIC_CONTEXT) + + def test_cursor_native_conversations_have_distinct_capture_and_markers(self) -> None: + harness = HookHarness() + conversations = ["synthetic-conversation-a", "synthetic-conversation-b"] + for conversation in conversations: + data = {"conversation_id": conversation, "prompt": "synthetic capture prompt"} + response = harness.run("session_start.py", "cursor", "sessionStart", payload=data) + self.assertEqual(decode_fixture("cursor", "sessionStart", response), SYNTHETIC_CONTEXT) + self.assertEqual(harness.run("user_prompt_submit.py", "cursor", "beforeSubmitPrompt", payload=data), "") + self.assertEqual(harness.buffer_sessions, conversations) + self.assertEqual(harness.counter_sessions, conversations) + self.assertEqual(harness.store_sessions, conversations) + self.assertEqual(harness.markers, set(conversations)) + self.assertEqual(harness.flush_at_mark, [True, True]) + + def test_cursor_identity_precedence_and_other_hosts_session_id(self) -> None: + for host, event in (("cursor", "beforeSubmitPrompt"), ("claude", "UserPromptSubmit"), ("codex", "UserPromptSubmit"), ("gemini", "BeforeAgent")): + with self.subTest(host=host): + harness = HookHarness() + data = {"session_id": "synthetic-session", "conversation_id": "synthetic-conversation", "host": "cursor", "prompt": "synthetic capture prompt"} + harness.run("user_prompt_submit.py", host, event, payload=data) + expected = "synthetic-conversation" if host == "cursor" else "synthetic-session" + self.assertEqual(harness.buffer_sessions, [expected]) + self.assertEqual(harness.counter_sessions, [expected]) + self.assertEqual(harness.store_sessions, [expected]) + + def test_legacy_missing_session_fallback_and_no_start_marker(self) -> None: + harness = HookHarness() + data = {"prompt": "synthetic capture prompt"} + response = harness.run("session_start.py", "claude", "SessionStart", payload=data) + self.assertEqual(decode_fixture("claude", "SessionStart", response), SYNTHETIC_CONTEXT) + self.assertEqual(harness.markers, set()) + harness.run("user_prompt_submit.py", "claude", "UserPromptSubmit", payload=data) + self.assertEqual(harness.buffer_sessions, ["unknown"]) + + def test_cli_trusted_identity_defaults_and_overrides(self) -> None: + for script, default in (("session_start.py", "SessionStart"), ("user_prompt_submit.py", "UserPromptSubmit")): + parser = cast(Callable[[], argparse.Namespace], load_functions(script, {"_parse_args"})["_parse_args"]) + with patch.object(sys, "argv", [script]): + args = parser() + self.assertTrue(hasattr(args, "host")) + self.assertTrue(hasattr(args, "event")) + self.assertEqual((args.host, args.event), ("claude", default)) + with patch.object(sys, "argv", [script, "--host", "gemini", "--event", "BeforeAgent"]): + args = parser() + self.assertEqual((args.host, args.event), ("gemini", "BeforeAgent")) + with patch.object(sys, "argv", [script, "--host", "invalid"]), patch.object(sys, "stderr", io.StringIO()): + with self.assertRaises(SystemExit): + parser() + + def test_transcript_rejection_precedes_path_read(self) -> None: + harness = HookHarness() + data = {"session_id": "synthetic-session", "prompt": "Recall synthetic color", "transcript_path": "/synthetic/untrusted.jsonl"} + with patch.object(Path, "exists", side_effect=AssertionError("Unadmitted transcript stat")), patch.object(Path, "is_file", side_effect=AssertionError("Unadmitted transcript stat")): + response = harness.run("user_prompt_submit.py", "codex", "UserPromptSubmit", payload=data) + self.assertEqual(decode_fixture("codex", "UserPromptSubmit", response), SYNTHETIC_CONTEXT) + self.assertIn(("admit", ("/synthetic/untrusted.jsonl", "codex")), harness.calls) + + def test_admitted_transcript_threads_trusted_identity_to_spawn(self) -> None: + for host, event in (("cursor", "beforeSubmitPrompt"), ("gemini", "BeforeAgent"), ("codex", "UserPromptSubmit"), ("claude", "UserPromptSubmit")): + with self.subTest(host=host): + harness = HookHarness() + harness.admitted = True + data = {"session_id": "synthetic-session", "prompt": "Recall synthetic color", "transcript_path": "/synthetic/native.jsonl"} + with patch.object(Path, "is_file", return_value=True): + harness.run("user_prompt_submit.py", host, event, payload=data) + self.assertIn(("admit", ("/synthetic/native.jsonl", host)), harness.calls) + self.assertIn(("spawn", (("/synthetic/native.jsonl", "synthetic-session", "synthetic-user", ""), host)), harness.calls) + self.assertIn(("extracted", None), harness.calls) + phases = [call[0] for call in harness.calls] + self.assertLess(phases.index("admit"), phases.index("read-transcript")) + + +class VirtualPath: + files: dict[str, str] = {} + + def __init__(self, path: object) -> None: + self.path = str(path) + + @classmethod + def home(cls) -> VirtualPath: + return cls("/synthetic") + + @property + def parent(self) -> VirtualPath: + return VirtualPath(str(Path(self.path).parent)) + + def __truediv__(self, part: str) -> VirtualPath: + return VirtualPath(self.path + "/" + part) + + def __str__(self) -> str: + return self.path + + def exists(self) -> bool: + return self.path in self.files + + def mkdir(self, **_kwargs: object) -> None: + return None + + def read_text(self, **_kwargs: object) -> str: + return self.files[self.path] + + +class TestCursorInstallation(unittest.TestCase): + def test_fresh_and_upgrade_install_native_identity_preserve_other_hooks(self) -> None: + source = SOURCE_ROOT / "truememory" / "hooks" / "adapters" / "cursor.py" + tree = ast.parse(read_source(source), filename=str(source)) + owned = {"_HOOK_EVENTS", "_STALE_EVENTS", "_TRUEMEMORY_MARKER", "_CURSOR_DIR", "_MCP_CONFIG", "_HOOK_CONFIG"} + selected = [node for node in tree.body if (isinstance(node, ast.Assign) and all(isinstance(target, ast.Name) and target.id in owned for target in node.targets)) or (isinstance(node, ast.ClassDef) and node.name == "CursorAdapter")] + namespace: dict[str, object] = { + "CLIAdapter": type("SyntheticAdapter", (), {}), "Path": VirtualPath, + "json": json, "shlex": shlex, "sys": sys, "shutil": types.SimpleNamespace(), + "__file__": "/synthetic/truememory/hooks/adapters/cursor.py", + "atomic_write_text": lambda path, text: VirtualPath.files.__setitem__(str(path), text), + "is_owned_hook_command": SHARED_HELPERS["is_owned_hook_command"], + } + exec(compile(ast.Module(body=selected, type_ignores=[]), str(source), "exec"), namespace) + adapter_type = cast(type, namespace["CursorAdapter"]) + config_path = str(namespace["_HOOK_CONFIG"]) + third_party = {"command": "/synthetic/foreign-policy --target truememory", "timeout": 17, "custom": "preserve"} + for upgraded in (False, True): + with self.subTest(upgraded=upgraded): + VirtualPath.files = {} + existing = {"version": 1, "custom": "preserve", "hooks": {"sessionStart": [third_party.copy()]}} + if upgraded: + existing["hooks"]["sessionStart"].append({"command": "python /synthetic/truememory/ingest/hooks/session_start.py", "timeout": 1, "custom": "owned metadata"}) + VirtualPath.files[config_path] = json.dumps(existing) + adapter = adapter_type() + adapter.install_hooks(python_path="/synthetic/python path/python3", user_id="synthetic user", db_path="/synthetic/db path") + data = json.loads(VirtualPath.files[config_path]) + self.assertEqual(data["hooks"]["sessionStart"][0], third_party) + self.assertEqual(data["custom"], "preserve") + for event in ("sessionStart", "stop", "beforeSubmitPrompt", "preCompact"): + commands = [entry for entry in data["hooks"][event] if cast(Callable[[object], bool], SHARED_HELPERS["is_owned_hook_command"])(entry["command"])] + self.assertEqual(len(commands), 1) + parts = shlex.split(commands[0]["command"]) + self.assertIn("--host", parts) + self.assertIn("--event", parts) + self.assertEqual(parts[parts.index("--host") + 1], "cursor") + self.assertEqual(parts[parts.index("--event") + 1], event) + self.assertEqual(parts[parts.index("--user") + 1], "synthetic user") + self.assertEqual(parts[parts.index("--db") + 1], "/synthetic/db path") + if upgraded: + self.assertEqual(data["hooks"]["sessionStart"][1]["custom"], "owned metadata") + first = VirtualPath.files[config_path] + adapter.install_hooks(python_path="/synthetic/python path/python3", user_id="synthetic user", db_path="/synthetic/db path") + self.assertEqual(VirtualPath.files[config_path], first) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-openclaw-native-bridge-764.py b/tests/test-openclaw-native-bridge-764.py new file mode 100644 index 00000000..8ef6dc61 --- /dev/null +++ b/tests/test-openclaw-native-bridge-764.py @@ -0,0 +1,373 @@ +"""Actual-source native recall controls with inert collaborators and no package load. + +Python controls run under pytest or stdlib unittest. Explicit --js-controls +requires a working Bun executable and fails if it is absent; it never skips. +""" +from __future__ import annotations + +import argparse +import ast +import io +import json +import os +import re +import shutil +import subprocess +import sys +import unittest +from pathlib import Path +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] +WRAPPER = ROOT / "truememory/ingest/hooks/openclaw.py" +ADAPTER = ROOT / "truememory/hooks/adapters/openclaw.py" +TEMPLATE = ROOT / "truememory/hooks/templates/openclaw/index.js" +SESSION = "openclaw_" + "a" * 48 +RUN = "b" * 64 + + +class NativeImports(ast.NodeTransformer): + def visit_ImportFrom(self, node: ast.ImportFrom) -> ast.AST | None: + if (node.module or "").startswith("truememory"): + return None + return node + + +def actual_source(path: Path, injected: dict[str, object]) -> dict[str, object]: + tree = NativeImports().visit(ast.parse(path.read_text(encoding="utf-8"))) + ast.fix_missing_locations(tree) + namespace = {"__name__": "synthetic_openclaw764", "__file__": str(path), **injected} + exec(compile(tree, str(path), "exec"), namespace) + return namespace + + +class OpenClawNativeBridgeTests(unittest.TestCase): + def invoke(self, payload: object, result: object = "synthetic recall", *, raw: bool = False, + extracting: bool = False) -> tuple[list[tuple[object, ...]], str]: + calls: list[tuple[object, ...]] = [] + + def recall(*args: object, **kwargs: object) -> object: + calls.append((*args, kwargs)) + return result + + namespace = actual_source(WRAPPER, {"_try_auto_recall": recall}) + output = io.StringIO() + text = payload if raw else json.dumps(payload) + with patch.object(sys, "argv", ["openclaw", "--event", "before_prompt_build", + "--user", "synthetic-user", "--db", "/synthetic/db"]), \ + patch.object(sys, "stdin", io.StringIO(text)), patch.object(sys, "stdout", output), \ + patch.dict(os.environ, {"TRUEMEMORY_EXTRACTION": "1" if extracting else ""}): + namespace["main"]() + return calls, output.getvalue() + + def payload(self, prompt: object = "What do you remember about synthetic tea?") -> dict[str, object]: + return {"session_id": SESSION, "run_id": RUN, "prompt": prompt} + + def test_native_request_calls_unchanged_helper_with_installed_identity(self) -> None: + calls, output = self.invoke(self.payload()) + self.assertEqual(calls, [(self.payload()["prompt"], "synthetic-user", "/synthetic/db", + SESSION, {"debounced": False})]) + self.assertEqual(json.loads(output), {"context": "synthetic recall"}) + + def test_missing_malformed_identity_and_current_text_never_recall(self) -> None: + invalid = [None, [], {}, self.payload(None), self.payload([]), self.payload({}), + self.payload(""), self.payload("ab"), self.payload(" "), self.payload("abc\0")] + for key in ("session_id", "run_id"): + for value in (None, "", "unknown", [], "x" * 512): + invalid.append({**self.payload(), key: value}) + invalid.append({k: v for k, v in self.payload().items() if k != key}) + for payload in invalid: + with self.subTest(payload=payload): + calls, output = self.invoke(payload) + self.assertEqual(calls, []) + self.assertEqual(output, "") + + def test_native_paths_history_and_claimed_host_are_rejected(self) -> None: + for key, value in (("transcript_path", "/synthetic/forged.jsonl"), + ("messages", [{"role": "user", "content": "historical"}]), + ("host", "claude"), ("cwd", "/synthetic"), + ("user_id", "stdin-user"), ("db_path", "/stdin/db")): + calls, output = self.invoke({**self.payload(), key: value}) + self.assertEqual(calls, []) + self.assertEqual(output, "") + + def test_bounded_input_and_utf8_do_not_truncate_accepted_text(self) -> None: + for prompt in ("x" * (128 * 1024 + 1), "é" * (128 * 1024), "\ud800"): + calls, output = self.invoke(self.payload(prompt)) + self.assertEqual(calls, []) + self.assertEqual(output, "") + prompt = "synthetic\nquoted \" tea é" + calls, _ = self.invoke(self.payload(prompt)) + self.assertEqual(calls[0][0], prompt) + + def test_malformed_json_and_non_json_input_are_inert(self) -> None: + for text in ("", "{", "null", "[]", "{} trailing", "x" * (132 * 1024 + 1)): + calls, output = self.invoke(text, raw=True) + self.assertEqual(calls, []) + self.assertEqual(output, "") + + def test_no_recall_has_exact_native_empty_context(self) -> None: + calls, output = self.invoke(self.payload(), result=None) + self.assertEqual(len(calls), 1) + self.assertEqual(json.loads(output), {"context": ""}) + self.assertNotIn("additionalContext", output) + + def test_nonstring_helper_output_never_crosses_transport(self) -> None: + for result in ([], {}, 42): + calls, output = self.invoke(self.payload(), result=result) + self.assertEqual(len(calls), 1) + self.assertEqual(output, "") + + def test_extraction_recursion_is_inert(self) -> None: + calls, output = self.invoke(self.payload(), extracting=True) + self.assertEqual(calls, []) + self.assertEqual(output, "") + + def test_wrapper_has_no_capture_lifecycle_or_shared_main(self) -> None: + tree = ast.parse(WRAPPER.read_text(encoding="utf-8")) + names = {node.id for node in ast.walk(tree) if isinstance(node, ast.Name)} + self.assertTrue({"_try_capture_email", "buffer_message", "_prune_old_buffers", + "_try_per_exchange_store", "_increment_prompt_count", "emit_context", + "_run_background_ingestion"}.isdisjoint(names)) + imports = [node.module for node in ast.walk(tree) if isinstance(node, ast.ImportFrom) + and (node.module or "").startswith("truememory")] + self.assertEqual(imports, ["truememory.ingest.hooks.user_prompt_submit"]) + + def test_installer_serializes_user_db_and_interpreter_without_execution(self) -> None: + written: dict[str, str] = {} + namespace = actual_source(ADAPTER, {"CLIAdapter": object, + "atomic_write_text": lambda p, t: written.update({str(p): t})}) + namespace["_PLUGINS_DIR"] = Path("/synthetic/.openclaw/extensions") + config_path = Path("/synthetic/.openclaw/openclaw.json") + namespace["_CONFIG_PATH"] = config_path + adapter = namespace["OpenClawAdapter"]() + python = '/synthetic/quote"/python\\exe' + user, db = 'synthetic-"user', '/synthetic/db "with spaces"' + original_exists = Path.exists + with patch.object(Path, "mkdir"), patch.object(Path, "exists", lambda p: + False if p == config_path else original_exists(p)): + adapter.install_hooks(python, user, db) + first = written.copy() + adapter.install_hooks(python, user, db) + self.assertEqual(first, written) + js = written["/synthetic/.openclaw/extensions/truememory/index.js"] + installed = re.search(r"^const INSTALLED_CONFIG = (.*);$", js, re.MULTILINE) + self.assertIsNotNone(installed) + self.assertEqual(json.loads(installed.group(1)), {"pythonPath": python, "userId": user, "dbPath": db}) + self.assertEqual(set(Path(path).name for path in written), + {"index.js", "package.json", "openclaw.plugin.json", "openclaw.json"}) + self.assertFalse(adapter.has_session_start) + + def permission_install(self, seed: object, *, raw: bool = False) -> dict[str, str]: + written: dict[str, str] = {} + namespace = actual_source(ADAPTER, {"CLIAdapter": object, + "atomic_write_text": lambda p, t: written.update({str(p): t})}) + config_path = Path("/synthetic/.openclaw/openclaw.json") + namespace["_CONFIG_PATH"] = config_path + namespace["_PLUGINS_DIR"] = Path("/synthetic/.openclaw/extensions") + original_read = Path.read_text + original_exists = Path.exists + text = seed if raw else json.dumps(seed) + with patch.object(Path, "mkdir"), patch.object(Path, "exists", lambda p: + True if p == config_path else original_exists(p)), \ + patch.object(Path, "read_text", lambda p, *args, **kwargs: + text if p == config_path else original_read(p, *args, **kwargs)): + namespace["OpenClawAdapter"]().install_hooks("/synthetic/python") + return written + + def test_permission_grants_only_absent_owned_conversation_access(self) -> None: + seed = {"mcp": {"servers": {"other": {"command": "foreign"}}}, "unrelated": 42, + "plugins": {"enabled": False, "allow": ["other"], "deny": ["truememory"], + "entries": {"other": {"enabled": False, "hooks": {"allowConversationAccess": False}}, + "truememory": {"enabled": False, "config": {"foreign": "keep"}, + "hooks": {"allowPromptInjection": False}}}}} + expected = json.loads(json.dumps(seed)) + expected["plugins"]["entries"]["truememory"]["hooks"]["allowConversationAccess"] = True + written = self.permission_install(seed) + self.assertEqual(json.loads(written["/synthetic/.openclaw/openclaw.json"]), expected) + self.assertEqual(seed["plugins"]["deny"], ["truememory"]) + self.assertFalse(seed["plugins"]["entries"]["truememory"]["hooks"]["allowPromptInjection"]) + + def test_permission_preserves_explicit_denials_and_malformed_configs(self) -> None: + for value in (False, True, None, "false"): + seed = {"plugins": {"entries": {"truememory": {"hooks": {"allowConversationAccess": value}}}}} + written = self.permission_install(seed) + self.assertNotIn("/synthetic/.openclaw/openclaw.json", written) + self.assertIn("/synthetic/.openclaw/extensions/truememory/index.js", written) + for seed in ([], {"plugins": False}, {"plugins": {"entries": []}}, + {"plugins": {"entries": {"truememory": False}}}, + {"plugins": {"entries": {"truememory": {"hooks": False}}}}): + self.assertNotIn("/synthetic/.openclaw/openclaw.json", self.permission_install(seed)) + self.assertNotIn("/synthetic/.openclaw/openclaw.json", self.permission_install("{", raw=True)) + + +def js_controls(bun: str) -> None: + source = TEMPLATE.read_text(encoding="utf-8") + source = source.replace('import { spawn } from "node:child_process";', "") + source = source.replace("export default {", "const plugin = {") + harness = r''' +const checks = []; +function check(name, condition) { + if (!condition) throw new Error(name); + checks.push(name); +} +let plan = []; +let calls = []; +let children = []; +function spawn(command, args, options) { + const step = plan.shift() ?? {}; + const child = new EventEmitter(); + child.stdout = new EventEmitter(); + child.stdin = new EventEmitter(); + child.kill = (signal) => { + child.killed = signal; + if (!step.suppressClose) queueMicrotask(() => child.emit("close", null, signal)); + return true; + }; + child.stdin.end = (input) => { + calls.push({command, args, options, input: JSON.parse(input)}); + if (step.hang) return; + const deliver = () => { + if (step.error) { child.emit("error", new Error("synthetic")); return; } + for (const chunk of step.chunks ?? [JSON.stringify({context: "synthetic recall"})]) { + child.stdout.emit("data", Buffer.from(chunk)); + } + if (!step.suppressClose) child.emit("close", step.code ?? 0, step.signal ?? null); + }; + if (step.delayed) child.deliver = deliver; + else queueMicrotask(deliver); + }; + children.push(child); + return child; +} +const nativeSetTimeout = globalThis.setTimeout; +const nativeClearTimeout = globalThis.clearTimeout; +const timers = new Map(); +let timerId = 0; +globalThis.setTimeout = (fn, delay) => { + const id = ++timerId; + timers.set(id, {fn, delay}); + return id; +}; +globalThis.clearTimeout = (id) => timers.delete(id); +const handlers = new Map(); +plugin.register({pluginConfig: {pythonPath: "/synthetic/python", userId: "synthetic-user", dbPath: "/synthetic/db"}, + on: (event, fn) => handlers.set(event, fn)}); +check("single native prompt registration", handlers.size === 1 && handlers.has("before_prompt_build")); +const handler = handlers.get("before_prompt_build"); +const ctx = (session = "synthetic-session", run = "synthetic-run") => ({sessionId: session, runId: run, trigger: "user"}); +const event = {prompt: "What do you remember about synthetic tea?", messages: [{role: "user", content: "historical"}], + transcriptPath: "/synthetic/forged", user_id: "stdin-user", db_path: "/stdin/db"}; +const result = await handler(event, ctx()); +check("native prependContext", result?.prependContext === "synthetic recall"); +check("fixed native argv identity", JSON.stringify(calls[0].args) === JSON.stringify([ + "-m", "truememory.ingest.hooks.openclaw", "--event", "before_prompt_build", "--user", "synthetic-user", "--db", "/synthetic/db"])); +check("no shell or discovery worker", calls[0].command === "/synthetic/python" && calls[0].options.shell === false); +check("exact bounded native payload", Object.keys(calls[0].input).sort().join() === "prompt,run_id,session_id" && + /^openclaw_[0-9a-f]{48}$/.test(calls[0].input.session_id) && /^[0-9a-f]{64}$/.test(calls[0].input.run_id)); +const before = calls.length; +await handler({...event, currentUserMessage: ""}, ctx()); +await handler({...event, prompt: ""}, ctx()); +await handler({...event, prompt: []}, ctx()); +await handler({...event, prompt: "x".repeat(128 * 1024 + 1)}, ctx()); +for (const bad of [{trigger: "user"}, {...ctx(), sessionId: ""}, {...ctx(), runId: null}, + {...ctx(), sessionId: []}, {...ctx(), trigger: "heartbeat"}, + {...ctx(), inputProvenance: {kind: "inter_session"}}, + {...ctx(), hookInvocation: {assertActive() {throw new Error("stale");}}}]) await handler(event, bad); +check("malformed missing empty and stale never spawn", calls.length === before); +await handler(event, ctx("different-session")); +check("equal text sessions separated", calls.at(-1).input.session_id !== calls[0].input.session_id); +await handler(event, ctx("synthetic-session", "different-run")); +check("equal text runs separated", calls.at(-1).input.run_id !== calls[0].input.run_id); +const rebuildBefore = calls.length; +await handler(event, ctx()); +await handler(event, ctx()); +check("sequential rebuild re-evaluates without completed suppression", calls.length === rebuildBefore + 2); +plan.push({delayed: true}); +const concurrentBefore = calls.length; +const first = handler(event, ctx("concurrent")); +const second = handler(event, ctx("concurrent")); +check("only in-flight identical work coalesces", calls.length === concurrentBefore + 1); +children.at(-1).deliver(); +const pair = await Promise.all([first, second]); +check("both concurrent invocations return supported context", pair.every((value) => value?.prependContext === "synthetic recall")); +for (const admission of ["", " ", null, [], {}, 42, "x".repeat(513), "synthetic\0id", "synthetic\nid"]) { + const admissionBefore = calls.length; + check("explicit invalid admission ID rejected " + JSON.stringify(admission).slice(0, 80), + await handler({...event, currentUserMessageId: admission}, ctx("admission")) === undefined && + calls.length === admissionBefore); +} +plan.push({delayed: true}, {delayed: true}); +const distinctAdmissionBefore = calls.length; +const distinctFirst = handler({...event, currentUserMessageId: "synthetic-admission-1"}, ctx("admission")); +const distinctSecond = handler({...event, currentUserMessageId: "synthetic-admission-2"}, ctx("admission")); +check("distinct explicit admissions do not coalesce", calls.length === distinctAdmissionBefore + 2); +for (const child of children.slice(-2)) child.deliver(); +check("distinct explicit admissions both return context", (await Promise.all([distinctFirst, distinctSecond])) + .every((value) => value?.prependContext === "synthetic recall")); +plan.push({delayed: true}); +const sameAdmissionBefore = calls.length; +const sameFirst = handler({...event, currentUserMessageId: "synthetic-admission-3"}, ctx("admission")); +const sameSecond = handler({...event, currentUserMessageId: "synthetic-admission-3"}, ctx("admission")); +check("identical in-flight explicit admission coalesces", calls.length === sameAdmissionBefore + 1); +children.at(-1).deliver(); +check("identical explicit admission returns context to both", (await Promise.all([sameFirst, sameSecond])) + .every((value) => value?.prependContext === "synthetic recall")); +for (const step of [{code: 7}, {signal: "SIGTERM"}, {chunks: ["{"]}, {chunks: ["null"]}, + {chunks: ["{}"]}, {chunks: ['{"context":42}']}, + {chunks: ['{"context":"synthetic","unexpected":true}']}, + {chunks: [Buffer.from('{"context":"'), Buffer.from([255]), Buffer.from('"}')]}, + {chunks: ["x".repeat(64 * 1024 + 1)]}, {error: true}]) { + plan.push(step); + check("failed output rejected " + JSON.stringify(step).slice(0, 100), await handler(event, ctx("failure")) === undefined); + check("failed work remains retryable", (await handler(event, ctx("failure")))?.prependContext === "synthetic recall"); +} +plan.push({chunks: ['{"context":""}']}); +check("empty recall is not fabricated context", await handler(event, ctx("empty")) === undefined); +let active = true; +plan.push({delayed: true}); +const expired = handler(event, {...ctx("expired"), hookInvocation: {assertActive() { + if (!active) throw new Error("expired"); +}}}); +active = false; +children.at(-1).deliver(); +check("invocation lifetime checked after await", await expired === undefined); +plan.push({hang: true}); +const timeout = handler(event, ctx("timeout")); +check("worker deadline bounded to four seconds", [...timers.values()].every((timer) => timer.delay === 4000)); +for (const timer of [...timers.values()]) timer.fn(); +check("timeout returns without accepting output", await timeout === undefined); +check("timeout kills owned child", children.at(-1).killed === "SIGKILL"); +await Promise.resolve(); +check("timed out work remains retryable", (await handler(event, ctx("timeout")))?.prependContext === "synthetic recall"); +for (let i = 0; i < 4; i++) plan.push({delayed: true}); +const boundedBefore = calls.length; +const bounded = Array.from({length: 4}, (_, i) => handler(event, ctx("budget", "run" + i))); +check("fifth concurrent worker denied", await handler(event, ctx("budget", "run4")) === undefined && calls.length === boundedBefore + 4); +for (const child of children.slice(-4)) child.deliver(); +check("four admitted workers succeed", (await Promise.all(bounded)).every((value) => value?.prependContext === "synthetic recall")); +check("capacity restored after close", (await handler(event, ctx("budget", "run4")))?.prependContext === "synthetic recall"); +globalThis.setTimeout = nativeSetTimeout; +globalThis.clearTimeout = nativeClearTimeout; +process.stdout.write(JSON.stringify({checks: checks.length, passed: checks})); +''' + script = 'import { EventEmitter } from "node:events";\n' + source + harness + result = subprocess.run([bun, "--eval", script], capture_output=True, text=True, timeout=30, check=True) + report = json.loads(result.stdout) + if report["checks"] < 53: + raise AssertionError(report) + print(json.dumps(report, sort_keys=True)) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--js-controls", action="store_true") + parser.add_argument("--bun") + args, remaining = parser.parse_known_args() + if args.js_controls: + bun = args.bun or shutil.which("bun") + if not bun: + parser.error("--js-controls requires --bun PATH or Bun on PATH") + js_controls(bun) + else: + unittest.main(argv=[sys.argv[0], *remaining]) diff --git a/tests/test-preferences-status-746.py b/tests/test-preferences-status-746.py new file mode 100644 index 00000000..c69717e5 --- /dev/null +++ b/tests/test-preferences-status-746.py @@ -0,0 +1,308 @@ +"""Truthful scheduled-preference status with stdlib and in-memory SQLite only. + +Actual maintenance/serving ownership, layer transactions, status formatting and +ingest admission execute. Synthetic layer builders replace unrelated algorithms; +the personality control loads actual FTS and char-gram retrieval without models. +""" +from __future__ import annotations + +import ast +import logging +import runpy +import sqlite3 +import threading +import time +import types +import unittest +from pathlib import Path +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] +PRIMITIVES = runpy.run_path(str(Path(__file__).with_name("test-maintenance-source-revision-753.py"))) +STORAGE, MAINTENANCE = PRIMITIVES["STORAGE"], PRIMITIVES["MAINTENANCE"] +UNAVAILABLE = "UNAVAILABLE (ScheduledPreferencesUnsupported)" +LAYERS = ( + ("clusters", "cluster_messages"), ("summaries", "build_summaries"), + ("contradictions", "detect_contradictions"), ("structured_facts", "structured_facts"), + ("surprise", "build_surprise_index"), ("episodes", "detect_episodes"), + ("landmarks", "detect_landmarks"), ("dunbar", "dunbar_hierarchy"), +) + + +def source_function(path: str, name: str) -> ast.FunctionDef: + tree = ast.parse((ROOT / path).read_text(encoding="utf-8")) + return next(node for node in ast.walk(tree) if isinstance(node, ast.FunctionDef) and node.name == name) + + +class TestScheduledPreferences(unittest.TestCase): + def setUp(self) -> None: + self.conn = STORAGE.create_db(":memory:") + self.addCleanup(self.conn.close) + self.conn.execute("CREATE TABLE synthetic_outputs(layer TEXT PRIMARY KEY)") + self.conn.commit() + self.coordinator = MAINTENANCE.MaintenanceCoordinator(None) + self.builds: list[str] = [] + self.imports: list[str] = [] + + def add(self, *, commit: bool = True) -> None: + self.conn.execute( + "INSERT INTO messages(content,sender,timestamp) VALUES " + "('Synthetic coffee routine','synthetic-entity','2026-01-01')" + ) + if commit: + self.conn.commit() + + def specs(self) -> tuple: + def spec(layer: str, key: str) -> object: + def build(conn: sqlite3.Connection) -> None: + self.assertIs(conn, self.conn) + self.assertTrue(conn.in_transaction) + self.builds.append(layer) + conn.execute("INSERT OR REPLACE INTO synthetic_outputs VALUES (?)", (layer,)) + + return MAINTENANCE.LayerSpec( + layer, key, lambda: MAINTENANCE.make_layer_dependency(1, {"synthetic": layer}), + build, lambda conn: conn.execute( + "SELECT count(*) FROM synthetic_outputs WHERE layer=?", (layer,), + ).fetchone()[0], + ) + return tuple(spec(layer, key) for layer, key in LAYERS) + + def forbidden_import(self, name: str) -> object: + self.imports.append(name) + raise AssertionError("The synthetic runner must not import preference/model builders") + + def run_report(self, **kwargs: object) -> object: + with patch.object(MAINTENANCE, "engine_layer_specs", lambda *_args, **_kwargs: self.specs()), \ + patch.object(MAINTENANCE, "importlib", types.SimpleNamespace(import_module=self.forbidden_import)): + return MAINTENANCE.run_engine_maintenance( + self.conn, self.coordinator, prepare_extensions=False, **kwargs, + ) + + def result(self, outcome: str = "success", **kwargs: object) -> object: + return MAINTENANCE.LayerResult( + "summaries", "build_summaries", outcome, 1, 0.0, + kwargs.get("error_category"), kwargs.get("attempted", True), + kwargs.get("coverage", "complete"), kwargs.get("pending_caller_commit", False), + ) + + def test_forced_empty_and_populated_runs_keep_eight_layers_and_report_limit(self) -> None: + self.assertEqual(MAINTENANCE.SCHEDULED_PREFERENCES_UNAVAILABLE, UNAVAILABLE) + for populated in (False, True): + with self.subTest(populated=populated): + if populated: + self.add() + self.builds.clear() + report = self.run_report(force=True) + self.assertEqual(self.builds, [layer for layer, _ in LAYERS]) + self.assertTrue(all(result.outcome == "success" for result in report.results)) + self.assertEqual(report.preferences, UNAVAILABLE) + self.assertEqual(MAINTENANCE.maintenance_report_status(report), ("completed_with_limits", None)) + formatted = MAINTENANCE.format_maintenance_report(report) + self.assertEqual(set(formatted), {key for _, key in LAYERS} | {"extract_preferences"}) + self.assertEqual(formatted["extract_preferences"], UNAVAILABLE) + self.assertFalse(self.conn.in_transaction) + self.assertEqual(self.imports, []) + + def test_eligible_automatic_attempt_and_current_no_attempt_differ(self) -> None: + self.add() + attempted = self.run_report(threshold=1) + self.assertTrue(all(result.attempted for result in attempted.results)) + self.assertEqual(attempted.preferences, UNAVAILABLE) + self.builds.clear() + current = self.run_report(threshold=1) + self.assertEqual(self.builds, []) + self.assertTrue(all(not result.attempted for result in current.results)) + self.assertEqual(current.preferences, "SKIPPED (no maintenance attempt)") + self.assertEqual(MAINTENANCE.maintenance_report_status(current), ("success", None)) + self.assertEqual(self.imports, []) + + def test_borrowed_writes_still_rollback_and_keep_pending_precedence(self) -> None: + self.add(commit=False) + report = self.run_report(force=True, allow_caller_transaction=True) + self.assertTrue(self.conn.in_transaction) + self.assertTrue(all(result.pending_caller_commit for result in report.results)) + self.assertEqual(report.preferences, UNAVAILABLE) + self.assertEqual(MAINTENANCE.maintenance_report_status(report), ("pending", None)) + self.assertEqual(self.conn.execute("SELECT count(*) FROM synthetic_outputs").fetchone()[0], 8) + self.conn.rollback() + self.assertEqual(self.conn.execute("SELECT count(*) FROM synthetic_outputs").fetchone()[0], 0) + self.assertEqual(self.conn.execute("SELECT count(*) FROM messages").fetchone()[0], 0) + + def test_captured_cancellation_survives_cleared_event(self) -> None: + for include_style in (False, True): + with self.subTest(include_style=include_style): + cancel = threading.Event() + cancel.set() + report = self.run_report(force=True, cancel=cancel, include_style=include_style) + cancel.clear() + self.assertEqual(report.preferences, "CANCELLED") + self.assertEqual(MAINTENANCE.maintenance_report_status(report, cancel), ("cancelled", "Cancelled")) + self.assertEqual(MAINTENANCE.maintenance_report_status(report), ("cancelled", "Cancelled")) + self.assertEqual(self.builds, []) + self.assertEqual(self.imports, []) + + def test_real_layer_and_style_outcomes_outrank_known_limitation(self) -> None: + for outcome in ("failed", "unavailable", "deferred", "abandoned"): + for style in (False, True): + with self.subTest(outcome=outcome, style=style): + result = self.result(outcome, error_category="SyntheticFailure") + report = MAINTENANCE.MaintenanceReport( + () if style else (result,), UNAVAILABLE, result if style else None, + ) + self.assertEqual(MAINTENANCE.maintenance_report_status(report), (outcome, "SyntheticFailure")) + cancel = threading.Event() + cancel.set() + self.assertEqual(MAINTENANCE.maintenance_report_status(report, cancel), ("cancelled", "Cancelled")) + + def test_pending_layer_and_style_results_outrank_known_limitation(self) -> None: + for result in (self.result(pending_caller_commit=True), self.result("stale"), self.result("untrusted")): + for style in (False, True): + with self.subTest(result=result, style=style): + report = MAINTENANCE.MaintenanceReport( + () if style else (result,), UNAVAILABLE, result if style else None, + ) + self.assertEqual(MAINTENANCE.maintenance_report_status(report), ("pending", None)) + + def test_only_exact_unsupported_token_is_a_completion_limit(self) -> None: + for status in ("ERROR (SyntheticFailure)", "UNAVAILABLE (DependencyMissing)", + "UNAVAILABLE (ScheduledPreferencesUnsupportedExtra)", UNAVAILABLE + " extra", + UNAVAILABLE + " (pending caller commit)"): + with self.subTest(status=status): + report = MAINTENANCE.MaintenanceReport((self.result(),), status) + self.assertEqual(MAINTENANCE.maintenance_report_status(report), ("failed", "PreferencesUnavailable")) + report = MAINTENANCE.MaintenanceReport((self.result(),), UNAVAILABLE) + self.assertEqual(MAINTENANCE.maintenance_report_status(report), ("completed_with_limits", None)) + + def test_style_only_and_busy_contracts_remain_distinct(self) -> None: + style = self.result()._replace(layer="style_vectors", result_key="style_vectors") + with patch.object(MAINTENANCE, "run_routed_style", return_value=style) as builder: + report = self.run_report(force=True, include_layers=False, include_style=True) + self.assertIs(report.style_result, style) + builder.assert_called_once() + self.assertIs(builder.call_args.args[0], self.conn) + self.assertEqual(report.preferences, "SKIPPED (style-only)") + self.assertEqual(MAINTENANCE.maintenance_report_status(report), ("success", None)) + self.assertEqual(self.builds, []) + busy = MAINTENANCE.maintenance_busy_result() + self.assertEqual(set(busy), {key for _, key in LAYERS} | {"extract_preferences"}) + self.assertTrue(all(value == "BUSY (maintenance owner active)" for value in busy.values())) + + def test_preference_phase_uses_no_connection_or_helper(self) -> None: + function = source_function("truememory/maintenance.py", "run_engine_maintenance") + body = function.body[-1].body + start = next(index for index, node in enumerate(body) if isinstance(node, ast.Assign) + and any(isinstance(target, ast.Name) and target.id == "preferences" for target in node.targets)) + wrapper = ast.parse("def phase(conn, results, force, cancel, style_result):\n pass\n").body[0] + wrapper.body = body[start:] + forbidden: list[str] = [] + + class NoConnectionWork: + def __getattribute__(self, name: str) -> object: + forbidden.append(name) + raise AssertionError("Unsupported preference phase cannot use its connection") + + namespace = dict(MAINTENANCE.__dict__) + namespace["importlib"] = types.SimpleNamespace(import_module=self.forbidden_import) + exec(compile(ast.fix_missing_locations(ast.Module(body=[wrapper], type_ignores=[])), + "synthetic-preference-phase", "exec"), namespace) + for force, attempted in ((True, False), (False, True), (False, False)): + with self.subTest(force=force, attempted=attempted): + result = namespace["phase"](NoConnectionWork(), (self.result(attempted=attempted),), force, None, None) + self.assertEqual(result.preferences, UNAVAILABLE if force or attempted else "SKIPPED (no maintenance attempt)") + self.assertEqual(forbidden, []) + self.assertEqual(self.imports, []) + + def test_actual_ingest_reports_present_and_absent_personality_truthfully(self) -> None: + function = source_function("truememory/engine.py", "ingest") + calls: list[str] = [] + + def create(path: Path) -> sqlite3.Connection: + self.assertEqual(str(path), ":memory:") + conn = STORAGE.create_db(":memory:") + self.addCleanup(conn.close) + return conn + + def forbidden_preferences(*_args: object, **_kwargs: object) -> None: + calls.append("preferences") + raise AssertionError("Ingest must not call no-entity preference extraction") + + def load(conn: sqlite3.Connection, _path: Path) -> int: + conn.execute("INSERT INTO messages(content,sender,timestamp) VALUES ('Synthetic coffee','synthetic','2026-01-01')") + conn.commit() + return 1 + + for available in (True, False): + with self.subTest(personality_available=available): + namespace = { + "__builtins__": STORAGE.__dict__["__builtins__"], "Path": Path, "time": time, + "os": types.SimpleNamespace(environ={}), "logger": logging.getLogger("synthetic-preference-ingest"), + "create_db": create, "load_messages_from_file": load, + "build_entity_profiles": lambda _conn: calls.append("profiles"), + "build_dunbar_hierarchy": lambda _conn, **_kwargs: {}, + "extract_preferences": forbidden_preferences, + } + namespace.update({node.id: False for node in ast.walk(function) + if isinstance(node, ast.Name) and node.id.startswith("_HAS_")}) + namespace["_HAS_PERSONALITY"] = available + exec(compile(ast.fix_missing_locations(ast.Module(body=[function], type_ignores=[])), + "synthetic-actual-ingest", "exec"), namespace) + flags = {name: False for name in ("vectors", "hybrid", "temporal", "salience", "personality", + "style_vec", "consolidation", "predictive", "reranker", "hyde", "clustering")} + engine = types.SimpleNamespace(db_path=Path(":memory:"), conn=None, stats={}, + _get_maintenance_coordinator=lambda: self.coordinator, + **{"_has_" + key: value for key, value in flags.items()}) + calls.clear() + result = namespace["ingest"](engine, Path("synthetic-unused-input.json")) + expected = UNAVAILABLE if available else "SKIPPED (personality module not available)" + self.assertEqual(result["extract_preferences"], expected) + self.assertEqual(calls, ["profiles"] if available else []) + self.assertEqual(engine.conn.execute("SELECT count(*) FROM messages").fetchone()[0], 1) + self.assertTrue(engine.ready) + + def test_entity_helper_and_actual_query_time_retrieval_remain_available(self) -> None: + modules = MAINTENANCE._fixture_modules.copy() + builtins_map = STORAGE.__dict__["__builtins__"] + base_import = builtins_map["__import__"] + + def safe_import(name: str, globals: dict | None = None, locals: dict | None = None, + fromlist: tuple = (), level: int = 0) -> object: + if name.startswith("truememory.") and name.removeprefix("truememory.") in modules: + return modules[name.removeprefix("truememory.")] + return base_import(name, globals, locals, fromlist, level) + + for name in ("fts_search", "personality_style_vec", "personality"): + module = types.ModuleType("synthetic_preference_" + name) + module.__dict__["__builtins__"] = dict(builtins_map, __import__=safe_import) + path = ROOT / "truememory" / (name + ".py") + exec(compile(path.read_text(encoding="utf-8"), str(path), "exec"), module.__dict__) + modules[name] = module + api = modules["personality"] + self.conn.executemany("INSERT INTO messages(content,sender,timestamp,directive) VALUES (?,?,?,?)", [ + ("Synthetic Cedar likes coffee every morning", "Cedar", "2026-01-01", 0), + ("Synthetic Birch likes pizza for dinner", "Birch", "2026-01-02", 0), + ("Synthetic Cedar directive says sushi", "Cedar", "2026-01-03", 1), + ]) + self.conn.commit() + statements: list[str] = [] + changes = self.conn.total_changes + self.conn.set_trace_callback(statements.append) + self.assertEqual(api.extract_preferences(self.conn), {}) + self.assertEqual(statements, []) + cedar = api.extract_preferences(self.conn, "Cedar") + birch = api.extract_preferences(self.conn, "Birch") + self.assertTrue(any("coffee" in item["text"] for item in cedar["food"])) + self.assertTrue(any("pizza" in item["text"] for item in birch["food"])) + self.assertFalse(any("sushi" in item["text"] for item in cedar["food"])) + results = api.search_personality(self.conn, "What food does Cedar like?", limit=10) + self.assertTrue(any("coffee" in item["content"] for item in results)) + self.assertFalse(any("sushi" in item["content"] for item in results)) + self.assertTrue(any("MATCH" in statement.upper() for statement in statements)) + self.assertEqual(self.conn.total_changes, changes) + self.assertFalse(self.conn.in_transaction) + self.conn.set_trace_callback(None) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-prepared-target-795.py b/tests/test-prepared-target-795.py new file mode 100644 index 00000000..c21060e9 --- /dev/null +++ b/tests/test-prepared-target-795.py @@ -0,0 +1,655 @@ +"""Prepared targets through real control flow, stdlib stubs and in-memory SQL.""" +from __future__ import annotations + +import ast +from contextlib import contextmanager +from dataclasses import FrozenInstanceError +import gc +import math +import os +from pathlib import Path +import re +import runpy +import sqlite3 +import sys +import threading +import time +import types +import unittest +from unittest.mock import Mock, patch +import weakref + + +ROOT = Path(__file__).resolve().parents[1] +SOURCE_TEXTS: dict[str, str] = {} +BASE = runpy.run_path(str(Path(__file__).with_name("test-tier-target-model-795.py"))) + + +def load(name: str, *, definitions: bool = False, namespace: dict | None = None) -> types.ModuleType: + path = ROOT / "truememory" / f"{name}.py" + tree = ast.parse(SOURCE_TEXTS.get(name, path.read_text())) + if definitions: + tree.body = [ast.ImportFrom(module="__future__", names=[ast.alias(name="annotations")], level=0), + *[node for node in tree.body if isinstance(node, (ast.FunctionDef, ast.ClassDef))]] + tree = ast.fix_missing_locations(tree) + module = types.ModuleType(f"truememory.{name}") + if namespace: + module.__dict__.update(namespace) + sys.modules[module.__name__] = module + exec(compile(tree, str(path), "exec"), module.__dict__) + return module + + +class Array(BASE["Array"]): + def __getitem__(self, key: slice) -> Array: + return Array(self.rows[key], (len(self.rows[key]), self.shape[1])) + + +class Numpy(BASE["Numpy"]): + @staticmethod + def empty(shape: tuple[int, int], **_kwargs: object) -> Array: + return Array([[] for _ in range(shape[0])], shape) + + +class VecConnection: + """Actual SQLite transactions, with only native vec0 DDL replaced by a table.""" + + def __init__(self) -> None: + self.db = sqlite3.connect(":memory:") + self.schemas: dict[str, str] = {} + self.fail_table: str | None = None + self.extensions: list[bool] = [] + + @property + def in_transaction(self) -> bool: + return self.db.in_transaction + + def enable_load_extension(self, enabled: bool) -> None: + self.extensions.append(enabled) + + def execute(self, sql: str, parameters: tuple = ()): + match = re.fullmatch(r'CREATE VIRTUAL TABLE main."(\w+)" USING (vec0\(.+\))', sql) + if match: + name = match[1] + if name == self.fail_table: + raise sqlite3.OperationalError("synthetic second-table denial") + result = self.db.execute(f'CREATE TABLE main."{name}"(embedding BLOB)') + self.schemas[name] = f'CREATE VIRTUAL TABLE "{name}" USING {match[2]}' + return result + result = self.db.execute(sql, parameters) + if sql == "SELECT type, sql FROM main.sqlite_master WHERE name = ?": + row = result.fetchone() + if row and parameters[0] in self.schemas: + row = (row[0], self.schemas[parameters[0]]) + return types.SimpleNamespace(fetchone=lambda: row) + return result + + def commit(self) -> None: + self.db.commit() + + def rollback(self) -> None: + self.db.rollback() + + def close(self) -> None: + self.db.close() + + +class TestPreparedTargets(unittest.TestCase): + def setUp(self) -> None: + self.fixture = BASE["TestTierTargetIdentity"]() + self.fixture.setUp() + self.addCleanup(self.fixture.doCleanups) + self.target_module = load("embedding_target") + self.config = load("tier_config") + environment = patch.dict("os.environ", { + "TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD": "1", + "TRUEMEMORY_CUSTOM_EMBED_MODEL": "synthetic/encoder", + "TRUEMEMORY_CUSTOM_EMBED_DIM": "384", + }) + environment.start() + self.addCleanup(environment.stop) + self.Target = self.target_module.EmbeddingTarget + self.target = self.Target.capture("base") + self.calls: list[tuple[str, dict]] = [] + self.encoded: list[tuple[str, list[str], dict]] = [] + self.width: int | None = None + self.build_hook = lambda: None + self.encode_hook = lambda: None + self.refs: list[weakref.ReferenceType] = [] + owner = self + + class Model: + def __init__(self, identity: str, **kwargs: object): + owner.refs.append(weakref.ref(self)) + owner.build_hook() + owner.calls.append((identity, kwargs)) + self.identity = identity + self.dimension = kwargs.get("truncate_dim", 256) + self.device = kwargs.get("device", "cpu") + self.oom_once = False + + def encode(self, texts: list[str], **kwargs: object) -> Array: + owner.encode_hook() + owner.encoded.append((self.identity, list(texts), kwargs)) + if self.oom_once: + self.oom_once = False + raise RuntimeError("MPS backend out of memory") + width = self.dimension if owner.width is None else owner.width + return Array([[float(index)] * width for index in range(len(texts))], (len(texts), width)) + + def to(self, device: str) -> None: + self.device = device + + self.Model = Model + self.mps = load("mps_utils") + self.mps.ensure_mps_memory_budget = Mock() + self.mps.resolve_device = lambda _device: "cpu" + self.mps.flush_mps_cache = Mock() + modules = patch.dict(sys.modules, { + "model2vec": types.SimpleNamespace(StaticModel=types.SimpleNamespace(from_pretrained=Model)), + "sentence_transformers": types.SimpleNamespace(SentenceTransformer=Model), + "sqlite_vec": types.SimpleNamespace(load=Mock()), + }) + modules.start() + self.addCleanup(modules.stop) + self.ms = self.fixture.ms + self.ms.np = Numpy() + if "model_server" in SOURCE_TEXTS: + tree = ast.parse(SOURCE_TEXTS["model_server"]) + tree.body = [node for node in tree.body if isinstance(node, (ast.FunctionDef, ast.ClassDef))] + with patch.dict(sys.modules, {self.ms.__name__: self.ms}): + exec(compile(tree, "synthetic-server-override", "exec"), self.ms.__dict__) + self.server = self.ms.ModelServer() + self.addCleanup(self.server._workers.shutdown, wait=True) + self.server._write_status_file = lambda: None + self.server._SUSTAINED_THRESHOLD = 1000 + self.client = load("model_client", definitions=True, namespace={"np": Numpy()}) + self.client._request_with_autostart = lambda request, timeout=None: self.server.handle_request(request) + self.client.use_model_server = lambda: False + self.vector = load("vector_search", definitions=True, namespace={ + "np": Numpy(), "threading": threading, "contextmanager": contextmanager, + "database_operation": lambda function: function, + "time": time, "math": math, "re": re, "sqlite3": sqlite3, + "_lock": threading.Lock(), "_prepared_target_slot": threading.Lock(), + "_model": None, "EMBEDDING_MODEL": "model2vec", "_embedding_dim": 256, + "_model_generation": 71, "_VEC_DISTANCE_METRIC": "cosine", + }) + self.vector._TIER_ALIASES = {"base": "qwen3_256", "pro": "qwen3_256", "edge": "model2vec"} + self.assertTrue(hasattr(self.vector, "prepare_embedding_target"), "Prepared lease API missing") + self.assertTrue(hasattr(self.client, "PreparedEmbeddingProxy"), "Certified client protocol missing") + self.assertTrue(hasattr(self.server, "_get_prepared_embed_model"), "Certified server ownership missing") + + def request(self, *, target=None, prepare: bool = True, texts=None, **fields: object) -> dict: + target = self.target if target is None else target + request = {"op": "prepare_embed_target_v1" if prepare else "embed_target_v1", + "target": target.to_wire(), "batch_size": 8, **fields} + if not prepare: + request["texts"] = ["synthetic"] if texts is None else texts + return self.server.handle_request(request) + + def globals(self) -> tuple: + return (self.vector.EMBEDDING_MODEL, self.vector._embedding_dim, + self.vector._model_generation, self.vector._model) + + def test_descriptor_is_frozen_strict_and_aliases_share_effective_identity(self) -> None: + self.assertEqual(self.target.identity, self.Target.capture("pro").identity) + self.assertEqual(self.target.tables, ("vec_messages_basepro", "vec_messages_sep_basepro")) + self.assertEqual(self.Target.from_wire(self.target.to_wire()), self.target) + with self.assertRaises(FrozenInstanceError): + self.target.dimension = 4 + for mutation in ({"dimension": True}, {"version": True}, {"tier_group": "x; DROP TABLE x"}, + {"model_id": "synthetic/other"}, {"extra": 1}): + with self.subTest(mutation=mutation), self.assertRaises(ValueError): + self.Target.from_wire({**self.target.to_wire(), **mutation}) + + def test_server_certifies_probe_and_every_batch_without_global_mutation(self) -> None: + before = self.globals() + response = self.request() + self.assertTrue(response["ok"]) + self.assertEqual(response["target"], self.target.to_wire()) + self.assertEqual(self.encoded[0][1], [self.target_module.TARGET_PROBE_TEXT]) + result = self.request(prepare=False, texts=["synthetic"] * 19) + self.assertEqual(result["vectors"].shape, (19, 256)) + self.assertEqual([len(entry[1]) for entry in self.encoded], [1, 8, 8, 3]) + self.assertEqual(len(self.calls), 1) + self.assertEqual(self.globals(), before) + self.assertFalse(self.server._inference_lock.locked()) + + def test_builtin_alias_reuses_resident_model_without_second_constructor(self) -> None: + self.request() + resident = self.server._embed_state.model + self.request(target=self.Target.capture("pro")) + self.assertIs(self.server._embed_state.model, resident) + self.assertEqual(len(self.calls), 1) + + def test_custom_legacy_fallback_is_replaced_and_frozen_args_are_used(self) -> None: + target = self.Target.capture("custom") + stale = self.Model("wrong-fallback") + self.server._embed_state = self.ms._EmbedState(stale, "custom", target.model_id) + stale_ref = weakref.ref(stale) + stale = None + self.build_hook = lambda: self.assertIsNone(stale_ref()) + response = self.request(target=target) + self.assertTrue(response["ok"]) + identity, kwargs = self.calls[-1] + self.assertEqual(identity, "synthetic/encoder") + self.assertEqual(kwargs["truncate_dim"], 384) + self.assertIs(kwargs["trust_remote_code"], False) + + def test_wrong_actual_width_cannot_certify_or_return_vectors(self) -> None: + target = self.Target.capture("custom") + self.width = 128 + for preparing in (True, False): + with self.subTest(preparing=preparing), self.assertRaises(self.target_module.EmbeddingTargetError): + self.request(target=target, prepare=preparing) + self.assertFalse(self.server._inference_lock.locked()) + self.assertFalse(self.server._lock.locked()) + + def test_custom_config_mismatch_and_revoked_permission_refuse_even_empty_input(self) -> None: + target = self.Target.capture("custom") + for setting in ({"TRUEMEMORY_CUSTOM_EMBED_DIM": "512"}, {"TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD": "0"}): + with self.subTest(setting=setting), patch.dict("os.environ", setting), self.assertRaises(ValueError): + self.request(target=target, prepare=False, texts=[]) + self.assertEqual(self.calls, []) + + def test_constructor_direct_call_rechecks_permission_and_deadline(self) -> None: + target = self.Target.capture("custom") + with patch.dict("os.environ", {"TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD": "0"}), self.assertRaises(ValueError): + self.target_module.build_target_model(target, "cpu") + with self.assertRaises(TimeoutError): + self.target_module.build_target_model(target, "cpu", check=Mock(side_effect=TimeoutError("expired"))) + self.assertEqual(self.calls, []) + + def test_empty_encode_still_certifies_and_preserves_requested_width(self) -> None: + proxy = self.client.PreparedEmbeddingProxy(self.target) + result = proxy.encode([]) + self.assertEqual(result.shape, (0, 256)) + self.assertEqual(len(self.encoded), 1) + self.width = 2 + with self.assertRaises(self.target_module.EmbeddingTargetError): + proxy.encode([]) + + def test_old_daemon_missing_or_mismatched_receipt_never_falls_back(self) -> None: + proxy = self.client.PreparedEmbeddingProxy(self.target) + for response in ({"ok": False, "error": "Unknown op: prepare_embed_target_v1"}, + {"ok": True}, {"ok": True, "target": self.Target.capture("edge").to_wire()}): + with self.subTest(response=response): + request = Mock(return_value=response) + self.client._request_with_autostart = request + with self.assertRaises(self.client.ProtocolMismatchError): + proxy.prepare() + self.assertEqual(request.call_count, 1) + self.assertEqual(request.call_args.args[0]["op"], "prepare_embed_target_v1") + + def test_expired_or_rss_refused_preparation_starts_no_constructor(self) -> None: + expired = self.request(deadline=time.time() - 1) + self.assertFalse(expired["ok"]) + self.server._max_rss_bytes = 1 + self.ms.psutil = types.SimpleNamespace(Process=lambda: types.SimpleNamespace(memory_info=lambda: types.SimpleNamespace(rss=1))) + refused = self.request() + self.assertEqual(refused["error_code"], "server_busy") + self.assertEqual(self.calls, []) + self.assertFalse(self.server._inference_lock.locked()) + + def test_expiry_during_rss_sensor_prevents_strict_constructor(self) -> None: + clock = [1000.0] + self.ms.time = types.SimpleNamespace(time=lambda: clock[0], monotonic=lambda: clock[0]) + self.server._max_rss_bytes = 2 + + def sample(): + clock[0] = 1002.0 + return types.SimpleNamespace(rss=1) + + self.ms.psutil = types.SimpleNamespace(Process=lambda: types.SimpleNamespace(memory_info=sample)) + result = self.request(deadline=1001.0) + self.assertFalse(result["ok"]) + self.assertIn("deadline", result["error"]) + self.assertEqual(self.calls, []) + self.assertFalse(self.server._lock.locked()) + self.assertFalse(self.server._inference_lock.locked()) + + def test_prepared_server_recovery_uses_sticky_cpu_and_one_resident_model(self) -> None: + self.request() + resident = self.server._embed_state.model + resident.device = "mps" + resident.oom_once = True + result = self.request(prepare=False) + self.assertTrue(result["ok"]) + self.assertIs(self.server._embed_state.model, resident) + self.assertEqual(resident.device, "cpu") + self.assertIn("embed", self.server._sticky_cpu) + self.assertEqual(len(self.calls), 1) + + def test_local_lease_is_exclusive_and_never_changes_active_globals(self) -> None: + before = self.globals() + self.build_hook = lambda: self.assertFalse(self.vector._lock.locked()) + self.encode_hook = lambda: self.assertFalse(self.vector._lock.locked()) + with self.vector.prepare_embedding_target(self.target) as lease: + self.assertEqual(lease.target, self.target) + self.assertEqual(lease.encode(["synthetic"]).shape, (1, 256)) + with self.assertRaises(RuntimeError): + with self.vector.prepare_embedding_target(self.target): + self.fail("Second lease admitted") + lease.close() + with self.assertRaises(RuntimeError): + lease.encode(["synthetic"]) + self.assertEqual(self.globals(), before) + self.assertFalse(self.vector._prepared_target_slot.locked()) + gc.collect() + self.assertTrue(all(ref() is None for ref in self.refs)) + + def test_shared_context_lease_uses_certified_protocol_without_local_slot(self) -> None: + self.client.use_model_server = lambda: True + before = self.globals() + with self.vector.prepare_embedding_target(self.target) as lease: + self.assertFalse(self.vector._prepared_target_slot.locked()) + self.assertEqual(lease.encode(["synthetic"] * 3, batch_size=2).shape, (3, 256)) + self.assertEqual([len(entry[1]) for entry in self.encoded], [1, 2, 1]) + self.assertIsNone(lease._encoder) + self.assertIsNotNone(self.server._embed_state) + self.assertEqual(self.globals(), before) + + def test_matching_local_active_builtin_is_borrowed_without_constructor(self) -> None: + active = self.Model("Qwen/Qwen3-Embedding-0.6B", truncate_dim=256) + self.vector._model = active + self.vector.EMBEDDING_MODEL = "qwen3_256" + before = self.globals() + with self.vector.prepare_embedding_target(self.target) as lease: + lease.encode(["synthetic"]) + self.assertEqual(len(self.calls), 1) + self.assertEqual(self.globals(), before) + + def test_local_constructor_and_probe_baseexception_release_slot(self) -> None: + for stage in ("construction", "probe"): + with self.subTest(stage=stage): + error = KeyboardInterrupt("synthetic cancellation") + failing = Mock(side_effect=error) + self.build_hook = failing if stage == "construction" else lambda: None + self.encode_hook = failing if stage == "probe" else lambda: None + with self.assertRaises(KeyboardInterrupt) as caught: + with self.vector.prepare_embedding_target(self.target): + self.fail("Cancelled preparation returned a lease") + self.assertIs(caught.exception, error) + self.assertFalse(self.vector._prepared_target_slot.locked()) + self.assertFalse(self.vector._lock.locked()) + error.__traceback__ = None + failing.side_effect = None + self.build_hook = self.encode_hook = lambda: None + gc.collect() + self.assertTrue(all(ref() is None for ref in self.refs)) + + def test_active_state_wait_uses_preparation_deadline(self) -> None: + lock = Mock() + lock.acquire.return_value = False + self.vector._lock = lock + with self.assertRaises(TimeoutError): + with self.vector.prepare_embedding_target(self.target, timeout=0.25): + self.fail("Busy active state admitted") + self.assertGreater(lock.acquire.call_args.kwargs["timeout"], 0) + self.assertLessEqual(lock.acquire.call_args.kwargs["timeout"], 0.25) + self.assertEqual(self.calls, []) + self.assertFalse(self.vector._prepared_target_slot.locked()) + + def test_local_native_owner_and_registry_waits_respect_same_deadline(self) -> None: + with self.vector.prepare_embedding_target(self.target) as lease: + model = lease._encoder + encoded = len(self.encoded) + with self.mps._own_model(model): + with self.assertRaises(TimeoutError): + lease.encode(["synthetic"], timeout=0.01) + self.assertEqual(self.mps._model_owners[id(model)].users, 1) + self.assertEqual(self.mps._model_owners, {}) + with self.mps._device_lock: + with self.assertRaises(TimeoutError): + lease.encode(["synthetic"], timeout=0.01) + self.assertEqual(self.mps._model_owners, {}) + self.assertEqual(len(self.encoded), encoded) + self.assertFalse(lease._encode_lock.locked()) + self.assertEqual(lease.encode(["synthetic"]).shape, (1, 256)) + + def test_legacy_model_owner_preserves_context_manager_lock_contract(self) -> None: + entered: list[str] = [] + + class ContextOnlyLock: + def __enter__(self) -> None: + entered.append("enter") + + def __exit__(self, *_exception: object) -> None: + entered.append("exit") + + model = object() + factory = self.mps._ModelOwnership + with patch.object(self.mps, "_ModelOwnership", lambda: factory(lock=ContextOnlyLock())): + with self.mps._own_model(model): + self.assertEqual(entered, ["enter"]) + self.assertEqual(self.mps._model_owners[id(model)].users, 1) + self.assertEqual(entered, ["enter", "exit"]) + self.assertEqual(self.mps._model_owners, {}) + + def test_permission_revoked_during_admission_or_budget_setup_starts_no_constructor(self) -> None: + target = self.Target.capture("custom") + def revoke(limit: int, _deadline: object) -> tuple[int, None]: + os.environ["TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD"] = "0" + return limit, None + + self.server._request_batch_limit = revoke + with self.assertRaises(ValueError): + self.request(target=target) + os.environ["TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD"] = "1" + self.mps.ensure_mps_memory_budget.side_effect = lambda _device: os.environ.update( + TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD="0", + ) + with self.assertRaises(ValueError): + self.target_module.build_target_model(target, "mps") + self.assertEqual(self.calls, []) + + def test_late_slice_width_mismatch_returns_no_partial_output(self) -> None: + def vary_width() -> None: + if self.encoded: + self.width = 128 + + self.encode_hook = vary_width + with self.assertRaises(self.target_module.EmbeddingTargetError): + self.request(prepare=False, texts=["synthetic"] * 9) + self.assertEqual([len(entry[1]) for entry in self.encoded], [8, 1]) + self.assertFalse(self.server._inference_lock.locked()) + + def test_preparation_deadline_consumed_by_constructor_prevents_probe(self) -> None: + clock = [100.0] + self.vector.time = types.SimpleNamespace(monotonic=lambda: clock[0]) + self.build_hook = lambda: clock.__setitem__(0, 102.0) + with self.assertRaises(TimeoutError): + with self.vector.prepare_embedding_target(self.target, timeout=1.0): + self.fail("Expired construction started a preparation probe") + self.assertEqual(self.encoded, []) + self.assertFalse(self.vector._prepared_target_slot.locked()) + + def test_close_marks_closed_before_wait_and_holds_slot_until_encode_drains(self) -> None: + entered, finish, closed = threading.Event(), threading.Event(), threading.Event() + errors: list[BaseException] = [] + with self.vector.prepare_embedding_target(self.target) as lease: + def encode_hook() -> None: + entered.set() + if not finish.wait(3): + raise AssertionError("Synthetic encode barrier timed out") + + self.encode_hook = encode_hook + + def encode() -> None: + try: + lease.encode(["synthetic"]) + except BaseException as error: + errors.append(error) + + thread = threading.Thread(target=encode) + thread.start() + self.assertTrue(entered.wait(3)) + original_close = lease._finish_close + lease._finish_close = lambda: (original_close(), closed.set()) + closer = threading.Thread(target=lease.close) + closer.start() + deadline = time.monotonic() + 3 + while not lease._closed and time.monotonic() < deadline: + closed.wait(0.001) + try: + self.assertTrue(lease._closed) + self.assertFalse(closed.is_set()) + with self.assertRaises(RuntimeError): + lease.encode(["queued"]) + with self.assertRaises(RuntimeError): + with self.vector.prepare_embedding_target(self.target): + self.fail("Closing target released residency early") + finally: + finish.set() + thread.join(3) + closer.join(3) + self.assertFalse(thread.is_alive()) + self.assertFalse(closer.is_alive()) + self.assertEqual(errors, []) + self.assertTrue(closed.is_set()) + self.assertIsNone(lease._encoder) + self.assertFalse(self.vector._prepared_target_slot.locked()) + + def test_interrupted_close_can_be_retried_and_does_not_reopen_lease(self) -> None: + with self.vector.prepare_embedding_target(self.target) as lease: + actual = lease._encode_lock + failing = Mock() + failing.__enter__ = Mock(side_effect=KeyboardInterrupt("synthetic close cancellation")) + failing.__exit__ = Mock(return_value=False) + failing.acquire.return_value = False + lease._encode_lock = failing + with self.assertRaises(KeyboardInterrupt): + lease.close() + self.assertTrue(self.vector._prepared_target_slot.locked()) + with self.assertRaises(RuntimeError): + lease.encode(["synthetic"]) + lease._encode_lock = actual + lease.close() + self.assertFalse(self.vector._prepared_target_slot.locked()) + self.assertIsNone(lease._encoder) + + def test_named_pair_is_idempotent_without_active_metadata_or_registry(self) -> None: + conn = VecConnection() + self.addCleanup(conn.close) + before = self.globals() + self.assertEqual(self.vector.init_prepared_target_tables(conn, self.target), self.target.tables) + self.vector.init_prepared_target_tables(conn, self.target) + names = {row[0] for row in conn.db.execute("SELECT name FROM sqlite_master")} + self.assertEqual(names, set(self.target.tables)) + self.assertFalse(conn.in_transaction) + self.assertEqual(self.globals(), before) + self.assertEqual(conn.extensions, [True, False, True, False]) + + def test_target_ddl_preserves_borrowed_transaction_and_rolls_back_failed_pair(self) -> None: + for borrowed in (False, True): + with self.subTest(borrowed=borrowed): + conn = VecConnection() + self.addCleanup(conn.close) + conn.db.execute("CREATE TABLE unrelated(value TEXT)") + if borrowed: + conn.db.execute("INSERT INTO unrelated VALUES('synthetic')") + conn.fail_table = self.target.tables[1] + with self.assertRaises(sqlite3.OperationalError): + self.vector.init_prepared_target_tables(conn, self.target) + self.assertEqual(conn.in_transaction, borrowed) + self.assertEqual(conn.db.execute("SELECT count(*) FROM unrelated").fetchone()[0], int(borrowed)) + self.assertIsNone(conn.db.execute("SELECT 1 FROM sqlite_master WHERE name=?", (self.target.tables[0],)).fetchone()) + conn.fail_table = None + self.vector.init_prepared_target_tables(conn, self.target) + self.assertEqual(conn.in_transaction, borrowed) + if borrowed: + conn.rollback() + self.assertIsNone(conn.db.execute("SELECT 1 FROM sqlite_master WHERE name=?", (self.target.tables[0],)).fetchone()) + + def test_target_schema_rejects_wrong_width_metric_ordinary_table_and_temp_shadow(self) -> None: + for kind in ("width", "metric", "ordinary", "temp", "temp_upper", "temp_mixed"): + with self.subTest(kind=kind): + conn = VecConnection() + self.addCleanup(conn.close) + name = self.target.tables[0] + if kind == "ordinary": + conn.db.execute(f"CREATE TABLE {name}(embedding BLOB)") + else: + self.vector.init_prepared_target_tables(conn, self.target) + if kind == "width": + conn.schemas[name] = conn.schemas[name].replace("float[256]", "float[384]") + elif kind == "metric": + conn.schemas[name] = conn.schemas[name].replace("cosine", "l2") + else: + shadow = name.upper() if kind == "temp_upper" else name + if kind == "temp_mixed": + shadow = self.target.tables[1].title() + conn.db.execute(f"CREATE TEMP TABLE {shadow}(embedding BLOB)") + with self.assertRaises(ValueError): + self.vector.init_prepared_target_tables(conn, self.target) + self.assertFalse(conn.in_transaction) + + +@unittest.skipUnless(os.environ.get("TRUEMEMORY_PREPARED_NATIVE_SQLITE_TEST") == "1", + "Actual sqlite-vec is an explicit isolated native gate") +class TestNativePreparedTables(unittest.TestCase): + def test_real_vec0_pair_owned_and_borrowed_transactions(self) -> None: + # Run only in the isolated native test container, never in local checks. + import sqlite_vec + + package = types.ModuleType("truememory") + package.__path__ = [] + with patch.dict(sys.modules, {"truememory": package, "sqlite_vec": sqlite_vec}): + target_module = load("embedding_target") + load("tier_config") + vector = load("vector_search", definitions=True, namespace={ + "np": Numpy(), "threading": threading, "contextmanager": contextmanager, + "database_operation": lambda function: function, + "time": time, "math": math, "re": re, "sqlite3": sqlite3, + "_VEC_DISTANCE_METRIC": "cosine", + }) + target = target_module.EmbeddingTarget.capture("base") + for borrowed in (False, True): + with self.subTest(borrowed=borrowed): + conn = sqlite3.connect(":memory:") + try: + conn.execute("CREATE TABLE unrelated(value TEXT)") + if borrowed: + conn.execute("INSERT INTO unrelated VALUES('synthetic')") + self.assertEqual(vector.init_prepared_target_tables(conn, target), target.tables) + self.assertEqual(vector.init_prepared_target_tables(conn, target), target.tables) + self.assertEqual(conn.in_transaction, borrowed) + for name in target.tables: + self.assertEqual(conn.execute(f'SELECT count(*) FROM main."{name}"').fetchone()[0], 0) + if borrowed: + conn.rollback() + self.assertIsNone(conn.execute("SELECT 1 FROM main.sqlite_master WHERE name=?", + (target.tables[0],)).fetchone()) + else: + conn.execute(f"CREATE TEMP TABLE {target.tables[0]}(value TEXT)") + with self.assertRaises(ValueError): + vector.init_prepared_target_tables(conn, target) + finally: + conn.close() + for mismatched in target.tables: + with self.subTest(mismatched=mismatched): + conn = sqlite3.connect(":memory:") + try: + conn.enable_load_extension(True) + sqlite_vec.load(conn) + conn.enable_load_extension(False) + conn.execute( + f'CREATE VIRTUAL TABLE main."{mismatched}" ' + 'USING vec0(embedding float[384] distance_metric=cosine)' + ) + conn.commit() + with self.assertRaises(ValueError): + vector.init_prepared_target_tables(conn, target) + self.assertFalse(conn.in_transaction) + for name in target.tables: + row = conn.execute("SELECT sql FROM main.sqlite_master WHERE name=?", (name,)).fetchone() + if name == mismatched: + self.assertIn("float[384]", row[0]) + else: + self.assertIsNone(row) + finally: + conn.close() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-profile-freshness-748.py b/tests/test-profile-freshness-748.py new file mode 100644 index 00000000..a899dde8 --- /dev/null +++ b/tests/test-profile-freshness-748.py @@ -0,0 +1,179 @@ +"""Actual profile token and incremental deferral controls; synthetic in-memory SQLite.""" +from __future__ import annotations + +import hashlib +import json +import runpy +import sqlite3 +import unittest +from pathlib import Path + +LOAD = runpy.run_path(str(Path(__file__).with_name("test-source-invalidation-748.py"))) +P, _STYLE = LOAD["_load_modules"]() +STORAGE = runpy.run_path(str(Path(__file__).with_name("test-l0-style-accumulator-747.py")))["STORAGE"] + + +class ProfileFreshnessTests(unittest.TestCase): + def db(self, *, certified: bool = True) -> sqlite3.Connection: + conn = sqlite3.connect(":memory:", cached_statements=0); self.addCleanup(conn.close) + conn.executescript(STORAGE._SCHEMA_SQL); STORAGE._initialize_maintenance_tracking(conn) + conn.execute("INSERT INTO messages(content,sender,recipient,timestamp) VALUES('synthetic code original','Synthetic','Other','2020-01-01')") + conn.commit() + if certified: + P.build_entity_profiles(conn) + return conn + + def rows(self, conn: sqlite3.Connection) -> list[tuple]: + return conn.execute("SELECT * FROM entity_profiles ORDER BY entity").fetchall() + + def append(self, conn: sqlite3.Connection, *, content: str = "synthetic code append", sender: str = "Synthetic", recipient: str = "Other", directive: int = 0) -> None: + conn.execute("INSERT INTO messages(content,sender,recipient,timestamp,directive) VALUES(?,?,?,'2020-01-02',?)", (content,sender,recipient,directive)); conn.commit() + + def test_source_proof_matches_actual_storage_contract(self) -> None: + self.assertEqual(P._PROFILE_SOURCE_TRIGGER_SHA256, + {name: hashlib.sha256(" ".join(sql.split()).encode()).hexdigest() + for name, sql in STORAGE._maintenance_trigger_definitions().items()}) + + def test_bulk_certifies_exact_current_generation(self) -> None: + conn = self.db(); profile = P.get_entity_profile(conn, "SYNTHETIC") + self.assertEqual(profile["message_count"], 1) + source, previous = P._profile_read_state(conn) + self.assertEqual(source, previous) + self.assertFalse(conn.in_transaction) + + def test_every_correction_hides_retained_rows_and_defers_increment(self) -> None: + for column, value in {"content":"synthetic changed","sender":"Changed","recipient":"Changed", + "timestamp":"2021-01-01","directive":1,"metadata":'{"synthetic":true}'}.items(): + with self.subTest(column=column): + conn = self.db(); before = self.rows(conn) + conn.execute(f"UPDATE messages SET {column}=?", (value,)); conn.commit() + self.assertIsNone(P.get_entity_profile(conn, "Synthetic")) + with self.assertRaises(P.ProfileRebuildRequired): + P.update_entity_profile_incremental(conn, "Synthetic", "synthetic partial", "Other") + self.assertEqual(self.rows(conn), before) + self.assertFalse(conn.in_transaction) + + def test_exact_ordinary_append_extends_certificate_without_formula_change(self) -> None: + conn = self.db(); self.append(conn) + self.assertEqual(P.get_entity_profile(conn, "Synthetic")["message_count"], 1) + P.update_entity_profile_incremental(conn, "SYNTHETIC", "synthetic code append", "OTHER") + self.assertEqual(P.get_entity_profile(conn, "Synthetic")["message_count"], 2) + source, previous = P._profile_read_state(conn); self.assertEqual(source, previous) + + def test_multiple_appends_defer_without_partial_generation(self) -> None: + conn = self.db(); before = self.rows(conn); self.append(conn); self.append(conn) + with self.assertRaises(P.ProfileRebuildRequired): + P.update_entity_profile_incremental(conn, "Synthetic", "synthetic code append", "Other") + self.assertEqual(self.rows(conn), before) + + def test_mismatched_content_sender_recipient_and_directive_refuse(self) -> None: + for kwargs in ({"content":"different"},{"sender":"Different"},{"recipient":"Different"},{"directive":1}): + with self.subTest(kwargs=kwargs): + conn = self.db(); before = self.rows(conn); self.append(conn, **kwargs) + with self.assertRaises(P.ProfileRebuildRequired): + P.update_entity_profile_incremental(conn, "Synthetic", "synthetic code append", "Other") + self.assertEqual(self.rows(conn), before) + + def test_source_free_direct_call_cannot_extend_certified_generation(self) -> None: + conn = self.db(); before = self.rows(conn) + with self.assertRaises(P.ProfileRebuildRequired): + P.update_entity_profile_incremental(conn, "Synthetic", "synthetic fabricated", "Other") + self.assertEqual(self.rows(conn), before) + + def test_direct_output_mutation_and_missing_token_preserve_enrollment(self) -> None: + for sql in ("UPDATE entity_profiles SET message_count=999", "DELETE FROM metadata WHERE key='entity_profile_source_v1'", + "UPDATE metadata SET value='{}' WHERE key='entity_profile_source_v1'"): + with self.subTest(sql=sql): + conn = self.db(); conn.execute(sql); conn.commit(); before = self.rows(conn) + self.assertIsNone(P.get_entity_profile(conn, "Synthetic")) + with self.assertRaises(P.ProfileRebuildRequired): + P.update_entity_profile_incremental(conn, "Synthetic", "synthetic append", "Other") + self.assertEqual(self.rows(conn), before) + + def test_epoch_reset_and_tracking_loss_do_not_revive_old_rows(self) -> None: + for sql in ("UPDATE maintenance_source_state SET epoch='synthetic reset'", "DROP TABLE maintenance_source_state"): + with self.subTest(sql=sql): + conn = self.db(); conn.execute(sql); conn.commit(); before = self.rows(conn) + self.assertIsNone(P.get_entity_profile(conn, "Synthetic")) + self.assertEqual(self.rows(conn), before) + + def test_borrowed_corrective_repair_and_rollback_restore_output_token_source(self) -> None: + conn = self.db(); before = self.rows(conn); token = conn.execute("SELECT value FROM metadata WHERE key=?", (P._PROFILE_TOKEN_KEY,)).fetchone() + conn.execute("UPDATE messages SET content='synthetic corrected code'") + self.assertIsNone(P.get_entity_profile(conn, "Synthetic")); self.assertTrue(conn.in_transaction) + P.build_entity_profiles(conn) + self.assertIsNotNone(P.get_entity_profile(conn, "Synthetic")); self.assertTrue(conn.in_transaction) + conn.rollback(); self.assertEqual(self.rows(conn), before) + self.assertEqual(conn.execute("SELECT value FROM metadata WHERE key=?", (P._PROFILE_TOKEN_KEY,)).fetchone(), token) + + def test_borrowed_append_rollback_restores_certificate_and_rows(self) -> None: + conn = self.db(); before = self.rows(conn) + conn.execute("INSERT INTO messages(content,sender,recipient) VALUES('synthetic append','Synthetic','Other')") + P.update_entity_profile_incremental(conn, "Synthetic", "synthetic append", "Other") + self.assertEqual(P.get_entity_profile(conn, "Synthetic")["message_count"], 2); self.assertTrue(conn.in_transaction) + conn.rollback(); self.assertEqual(self.rows(conn), before) + source, previous = P._profile_read_state(conn); self.assertEqual(source, previous) + + def test_known_nonappend_history_without_certificate_is_unavailable(self) -> None: + conn = self.db(certified=False) + conn.execute("INSERT INTO entity_profiles(entity,message_count) VALUES('synthetic',99)") + conn.execute("UPDATE messages SET content='synthetic correction'"); conn.commit() + self.assertIsNone(P.get_entity_profile(conn, "Synthetic")) + with self.assertRaises(P.ProfileRebuildRequired): + P.update_entity_profile_incremental(conn, "Synthetic", "synthetic partial") + P.build_entity_profiles(conn); self.assertEqual(P.get_entity_profile(conn, "Synthetic")["message_count"], 1) + + def test_legacy_bulk_and_source_free_helpers_remain_compatible(self) -> None: + conn = LOAD["_db"](":memory:"); self.addCleanup(conn.close); conn.commit() + P.update_entity_profile_incremental(conn, "Synthetic", "synthetic first", "Other") + P.update_entity_profile_incremental(conn, "SYNTHETIC", "synthetic second", "OTHER") + self.assertEqual(P.get_entity_profile(conn, "Synthetic")["message_count"], 2) + self.assertEqual(P.build_entity_profiles(conn), {}) + self.assertIsNone(P.get_entity_profile(conn, "Synthetic")) + canonical = self.db(certified=False) + P.update_entity_profile_incremental(canonical, "Direct", "synthetic source-free") + self.assertEqual(P.get_entity_profile(canonical, "Direct")["message_count"], 1) + + def test_read_is_bounded_and_never_reads_messages(self) -> None: + conn = self.db(); expected = P.get_entity_profile(conn, "Synthetic") + def authorize(action: int, table: str | None, *_args: object) -> int: + if action == sqlite3.SQLITE_READ and table == 'messages':return sqlite3.SQLITE_DENY + if action in (sqlite3.SQLITE_INSERT,sqlite3.SQLITE_UPDATE,sqlite3.SQLITE_DELETE):return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + conn.set_authorizer(authorize) + try:self.assertEqual(P.get_entity_profile(conn, "Synthetic"), expected) + finally:conn.set_authorizer(None) + self.assertFalse(conn.in_transaction) + + def test_missing_tracking_or_output_trigger_is_unavailable(self) -> None: + for name in (*P._PROFILE_OUTPUT_TRIGGERS, *P._PROFILE_SOURCE_TRIGGER_SHA256): + with self.subTest(name=name): + conn = self.db(); conn.execute(f"DROP TRIGGER {name}"); conn.commit() + self.assertIsNone(P.get_entity_profile(conn, "Synthetic")) + self.assertTrue(self.rows(conn)) + + def test_duplicate_keys_in_source_token_cannot_certify_rows(self) -> None: + conn = self.db(); before = self.rows(conn) + value = conn.execute("SELECT value FROM metadata WHERE key=?", (P._PROFILE_TOKEN_KEY,)).fetchone()[0] + duplicate = '{"schema_version":9,' + value[1:] + conn.execute("UPDATE metadata SET value=? WHERE key=?", (duplicate, P._PROFILE_TOKEN_KEY)); conn.commit() + self.assertIsNone(P.get_entity_profile(conn, "Synthetic")) + with self.assertRaises(P.ProfileRebuildRequired): + P.update_entity_profile_incremental(conn, "Synthetic", "synthetic append", "Other") + self.assertEqual(self.rows(conn), before) + + def test_invalid_utf8_source_epoch_or_token_is_controlled_unavailable(self) -> None: + for sql in ("UPDATE maintenance_source_state SET epoch=CAST(X'eda080' AS TEXT)", + "UPDATE metadata SET value=CAST(X'eda080' AS TEXT) WHERE key='entity_profile_source_v1'"): + with self.subTest(sql=sql): + conn = self.db(); before = self.rows(conn) + conn.text_factory = lambda value: value.decode('utf-8', 'surrogatepass') + conn.execute(sql); conn.commit() + self.assertIsNone(P.get_entity_profile(conn, "Synthetic")) + with self.assertRaises(P.ProfileRebuildRequired): + P.update_entity_profile_incremental(conn, "Synthetic", "synthetic append", "Other") + self.assertEqual(self.rows(conn), before) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-rebuild-foreground-publication-756.py b/tests/test-rebuild-foreground-publication-756.py new file mode 100644 index 00000000..18f1a924 --- /dev/null +++ b/tests/test-rebuild-foreground-publication-756.py @@ -0,0 +1,376 @@ +"""Actual foreground methods and rebuild publication on synthetic databases.""" + +import ast +import logging +import runpy +import sqlite3 +import threading +import types +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +BASE = types.SimpleNamespace(**runpy.run_path(str(ROOT / "tests/test-rebuild-source-streaming-756.py"))) + + +class TestForegroundPublication(unittest.TestCase): + setUp = BASE.TestStreamingBuilders.setUp + seed = BASE.TestStreamingBuilders.seed + ids = BASE.TestStreamingBuilders.ids + manifest = BASE.TestStreamingBuilders.manifest + writer = BASE.TestStreamingBuilders.writer + + def engine(self, conn=None): + tree = ast.parse((ROOT / "truememory/engine.py").read_text(encoding="utf-8")) + original = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == "TrueMemoryEngine") + methods = [node for node in original.body if isinstance(node, ast.FunctionDef) and node.name in ("add", "update")] + cls = ast.ClassDef(name="SyntheticEngine", bases=[], keywords=[], body=methods, decorator_list=[]) + namespace = dict(engine_operation=lambda function: function, __builtins__=self.vector.__dict__["__builtins__"], + logger=logging.getLogger(__name__), MAX_CONTENT_LENGTH=10000, + insert_message=self.modules["storage"].insert_message, + update_message=self.modules["storage"].update_message) + validators = [node for node in tree.body if isinstance(node, ast.FunctionDef) + and node.name == "_validate_add_content"] + exec(compile(ast.fix_missing_locations(ast.Module(body=[*validators, cls], type_ignores=[])), "actual-engine-boundaries", "exec"), namespace) + engine = namespace["SyntheticEngine"]() + engine.conn = conn or self.conn + engine._write_lock = threading.Lock() + engine._ensure_connection = lambda: None + engine._maybe_auto_consolidate = lambda: None + engine._has_vectors = True + engine._has_personality = engine._has_style_vec = False + engine.get = lambda mid: self.modules["storage"].get_message(engine.conn, mid) + return engine + + def test_actual_add_above_active_range_survives_builder_completion(self): + self.seed(range(1, 5)) + other = self.writer() + engine = self.engine(other) + inserted = [] + def encode(texts): + if not inserted: + inserted.append(True) + item = engine.add("synthetic-late-append", sender="sender", recipient="recipient", timestamp="2026-01-02") + self.assertEqual(item["id"], 5) + inserted.append(item["id"]) + return [[1., 0.] for _ in texts] + self.on_encode = encode + self.assertEqual(self.vector.build_vectors(self.conn, txn_batch=1), 4) + self.assertEqual(self.ids(), [1, 2, 3, 4, 5]) + self.assertEqual((self.manifest().cursor, self.manifest().consumed, self.manifest().source.high_id), (4, 4, 4)) + self.assertEqual(self.ids("vec_messages_sep"), [5]) + + def test_actual_update_invalidates_build_but_keeps_corrected_vector(self): + self.seed(range(1, 5)) + engine = self.engine(self.writer()) + updated = [] + def encode(texts): + if not updated: + updated.append(True) + self.assertEqual(engine.update(1, "synthetic-correction")["content"], "synthetic-correction") + return [[1., 0.] for _ in texts] + self.on_encode = encode + with self.assertRaises(self.source.RebuildSourceChanged): + self.vector.build_vectors(self.conn, txn_batch=1) + self.assertEqual(self.ids(), [1]) + self.assertEqual(self.manifest().consumed, 0) + self.assertTrue(self.vector._build_in_progress(self.conn, "vec_messages")) + + def test_late_model_change_rolls_back_add_source_and_does_not_write_vectors(self): + engine = self.engine() + def encode(texts): + if len(self.calls) == 2: + self.vector._model_generation += 1 + return [[1., 0.] for _ in texts] + self.on_encode = encode + with self.assertRaisesRegex(self.vector.VectorPublicationChanged, "model changed"): + engine.add("synthetic-stale-model") + self.assertEqual(self.conn.execute("SELECT COUNT(*) FROM messages").fetchone()[0], 0) + self.assertEqual(self.ids(), []) + self.assertFalse(self.conn.in_transaction) + + def test_late_model_change_update_reports_already_committed_source(self): + self.seed((1,)) + engine = self.engine() + def encode(texts): + if len(self.calls) == 2: + self.vector._model_generation += 1 + return [[1., 0.] for _ in texts] + self.on_encode = encode + with self.assertRaisesRegex(self.vector.VectorPublicationChanged, "Source update committed"): + engine.update(1, "synthetic-committed-correction") + self.assertEqual(self.conn.execute("SELECT content FROM messages WHERE id=1").fetchone()[0], "synthetic-committed-correction") + self.assertEqual(self.ids(), []) + + def test_completed_foreign_model_manifest_rejects_old_process_write(self): + self.seed((1,)) + self.vector.build_vectors(self.conn) + manifest = self.manifest() + from dataclasses import replace + self.source.save_manifest(self.conn, self.source.manifest_key(("vec_messages",)), replace(manifest, model="different-synthetic-model")) + self.conn.commit() + with self.assertRaisesRegex(self.vector.VectorPublicationChanged, "different embedding model"): + self.engine().add("synthetic-old-process") + self.assertEqual(self.conn.execute("SELECT COUNT(*) FROM messages").fetchone()[0], 1) + self.assertEqual(self.ids(), [1]) + + def test_unattested_marker_rejects_add_without_silent_success(self): + self.vector._mark_build_in_progress(self.conn, "vec_messages") + self.conn.commit() + with self.assertRaisesRegex(self.vector.VectorPublicationChanged, "without matching source"): + self.engine().add("synthetic-unattested") + self.assertEqual(self.conn.execute("SELECT COUNT(*) FROM messages").fetchone()[0], 0) + + def test_guard_releases_model_lock_when_sql_publication_raises(self): + self.seed((1,)) + identity = (0, "synthetic-model", 2) + with self.assertRaisesRegex(RuntimeError, "synthetic write failure"): + with self.vector._foreground_vector_publication(self.conn, identity, 1): + raise RuntimeError("synthetic write failure") + self.assertTrue(self.vector._lock.acquire(blocking=False)) + self.vector._lock.release() + + def test_embed_single_finishes_both_encodes_before_owned_database_write(self): + self.seed((1,)) + def encode(texts): + self.assertFalse(self.conn.in_transaction) + self.assertFalse(self.vector._lock.locked()) + return [[1., 0.] for _ in texts] + self.on_encode = encode + self.vector.embed_single(self.conn, 1, "synthetic-1") + self.assertEqual(self.ids(), [1]) + self.assertEqual(self.ids("vec_messages_sep"), [1]) + self.assertFalse(self.conn.in_transaction) + + def test_embed_single_stale_model_is_rejected_before_either_insert(self): + self.seed((1,)) + def encode(texts): + if len(self.calls) == 2: + self.vector._model_generation += 1 + return [[1., 0.] for _ in texts] + self.on_encode = encode + with self.assertRaises(self.vector.VectorPublicationChanged): + self.vector.embed_single(self.conn, 1, "synthetic-1") + self.assertEqual(self.ids(), []) + self.assertEqual(self.ids("vec_messages_sep"), []) + + def _attempt_foreign_manifest_at_vector_insert(self, operation): + self.seed((1,)) + other = self.writer() + other.execute("PRAGMA busy_timeout = 0") + manifest = self.source.RebuildManifest.new( + "different-synthetic-model", 2, ("vec_messages",), total=0, + ) + attempts = [] + ownership = [] + real = self.conn + case = self + + class Probe: + def __getattr__(self, name): + return getattr(real, name) + + def execute(self, sql, parameters=()): + if not attempts and sql.startswith(("INSERT INTO vec_messages(", "DELETE FROM vec_messages WHERE")): + ownership.append((real.in_transaction, case.vector._lock.locked())) + try: + case.source.save_manifest(other, case.source.manifest_key(("vec_messages",)), manifest) + other.commit() + attempts.append("committed") + except sqlite3.OperationalError as error: + case.assertIn("locked", str(error).lower()) + attempts.append("blocked") + finally: + other.rollback() + return real.execute(sql, parameters) + + probe = Probe() + if operation == "embed": + self.vector.embed_single(probe, 1, "synthetic-1") + expected = [1] + elif operation == "update": + self.engine(probe).update(1, "synthetic-correction") + expected = [1] + else: + self.engine(probe).add("synthetic-append") + expected = [2] + self.assertEqual(attempts, ["blocked"]) + self.assertEqual(ownership, [(True, True)]) + self.assertIsNone(self.manifest()) + self.assertEqual(self.ids(), expected) + self.assertEqual(self.ids("vec_messages_sep"), expected) + self.assertEqual(self.conn.execute("SELECT value FROM metadata WHERE key='embed_model'").fetchone()[0], + "synthetic-model") + self.assertFalse(self.conn.in_transaction) + + def test_embed_single_excludes_foreign_manifest_between_validation_and_insert(self): + self._attempt_foreign_manifest_at_vector_insert("embed") + + def test_update_excludes_foreign_manifest_after_source_helper_commits(self): + self._attempt_foreign_manifest_at_vector_insert("update") + + def test_add_excludes_foreign_manifest_through_vector_publication(self): + self._attempt_foreign_manifest_at_vector_insert("add") + + def test_rejected_add_preserves_unrelated_caller_write_and_transaction(self): + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('synthetic-caller','pending')") + def encode(texts): + if len(self.calls) == 2: + self.vector._model_generation += 1 + return [[1., 0.] for _ in texts] + self.on_encode = encode + with self.assertRaises(self.vector.VectorPublicationChanged): + self.engine().add("synthetic-rejected") + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.conn.execute("SELECT value FROM metadata WHERE key='synthetic-caller'").fetchone()[0], "pending") + self.assertEqual(self.conn.execute("SELECT COUNT(*) FROM messages").fetchone()[0], 0) + self.conn.commit() + self.assertEqual(self.conn.execute("SELECT value FROM metadata WHERE key='synthetic-caller'").fetchone()[0], "pending") + self.assertEqual(self.ids(), []) + + def test_foreign_target_rejection_rolls_back_only_new_add_after_source_insert(self): + manifest = self.source.RebuildManifest.new("different-synthetic-model", 2, ("vec_messages",)) + self.source.save_manifest(self.conn, self.source.manifest_key(("vec_messages",)), manifest) + self.conn.commit() + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('synthetic-caller','pending')") + with self.assertRaisesRegex(self.vector.VectorPublicationChanged, "different embedding model"): + self.engine().add("synthetic-rejected") + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.conn.execute("SELECT COUNT(*) FROM messages").fetchone()[0], 0) + self.conn.commit() + self.assertEqual(self.conn.execute("SELECT value FROM metadata WHERE key='synthetic-caller'").fetchone()[0], "pending") + self.assertEqual(self.ids(), []) + + def test_successful_add_remains_inside_caller_transaction(self): + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('synthetic-caller','pending')") + item = self.engine().add("synthetic-pending-add") + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.ids(), [item["id"]]) + self.conn.rollback() + self.assertEqual(self.ids(), []) + self.assertEqual(self.ids("vec_messages_sep"), []) + self.assertEqual(self.conn.execute("SELECT COUNT(*) FROM messages").fetchone()[0], 0) + + def test_embed_single_keeps_caller_work_pending(self): + self.seed((1,)) + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('synthetic-caller','pending')") + self.vector.embed_single(self.conn, 1, "synthetic-1") + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.ids(), [1]) + self.conn.rollback() + self.assertEqual(self.ids(), []) + self.assertEqual(self.ids("vec_messages_sep"), []) + + def _assert_foreground_writer_and_terminal_fence(self, operation, caller_owned): + self.seed((1,)) + if caller_owned: + self.conn.execute("BEGIN") + real = self.conn + case = self + boundaries = [] + + class Probe: + def __getattr__(self, name): + return getattr(real, name) + + def execute(self, sql, parameters=()): + if sql == "BEGIN IMMEDIATE" or sql == "UPDATE messages SET id = id WHERE 0": + case.assertTrue(case.vector._lock.locked()) + boundaries.append("writer") + if sql == "RELEASE SAVEPOINT rebuild_source": + case.assertTrue(case.vector._lock.locked()) + boundaries.append("release") + return real.execute(sql, parameters) + + def commit(self): + case.assertTrue(case.vector._lock.locked()) + boundaries.append("commit") + real.commit() + + if operation == "embed": + self.vector.embed_single(Probe(), 1, "synthetic-1") + else: + self.engine(Probe()).add("synthetic-append") + self.assertEqual(boundaries, ["writer", "release" if caller_owned else "commit"]) + self.assertEqual(real.in_transaction, caller_owned) + self.assertFalse(self.vector._lock.locked()) + + def test_embed_owned_commit_stays_inside_model_fence(self): + self._assert_foreground_writer_and_terminal_fence("embed", False) + + def test_embed_caller_fence_precedes_writer_upgrade_and_contains_release(self): + self._assert_foreground_writer_and_terminal_fence("embed", True) + + def test_add_owned_commit_stays_inside_model_fence(self): + self._assert_foreground_writer_and_terminal_fence("add", False) + + def test_add_caller_fence_precedes_writer_upgrade_and_contains_release(self): + self._assert_foreground_writer_and_terminal_fence("add", True) + + def test_denied_add_savepoint_release_rolls_back_only_add(self): + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('synthetic-caller','pending')") + denied = [] + def authorize(action, first, second, _database, _source): + if action == sqlite3.SQLITE_SAVEPOINT and first == "RELEASE" and second == "rebuild_source" and not denied: + self.assertTrue(self.vector._lock.locked()) + denied.append(True) + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + self.conn.set_authorizer(authorize) + try: + with self.assertRaises(sqlite3.DatabaseError): + self.engine().add("synthetic-denied-release") + finally: + self.conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.assertEqual(denied, [True]) + self.assertTrue(self.conn.in_transaction) + self.assertFalse(self.vector._lock.locked()) + self.conn.commit() + self.assertEqual(self.conn.execute("SELECT value FROM metadata WHERE key='synthetic-caller'").fetchone()[0], "pending") + self.assertEqual(self.conn.execute("SELECT COUNT(*) FROM messages").fetchone()[0], 0) + self.assertEqual(self.ids(), []) + self.assertEqual(self.ids("vec_messages_sep"), []) + + def test_cancelled_add_rolls_back_only_add_and_releases_model_fence(self): + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('synthetic-caller','pending')") + real = self.conn + class Probe: + def __getattr__(self, name): + return getattr(real, name) + + def execute(self, sql, parameters=()): + if sql.startswith("INSERT INTO vec_messages_sep("): + raise KeyboardInterrupt("synthetic cancellation") + return real.execute(sql, parameters) + with self.assertRaises(KeyboardInterrupt): + self.engine(Probe()).add("synthetic-cancelled") + self.assertTrue(self.conn.in_transaction) + self.assertFalse(self.vector._lock.locked()) + self.conn.commit() + self.assertEqual(self.conn.execute("SELECT value FROM metadata WHERE key='synthetic-caller'").fetchone()[0], "pending") + self.assertEqual(self.conn.execute("SELECT COUNT(*) FROM messages").fetchone()[0], 0) + self.assertEqual(self.ids(), []) + self.assertEqual(self.ids("vec_messages_sep"), []) + + def test_failed_add_owned_commit_does_not_publish_on_later_commit(self): + real = self.conn + case = self + class Probe: + def __getattr__(self, name): + return getattr(real, name) + + def commit(self): + case.assertTrue(case.vector._lock.locked()) + raise sqlite3.OperationalError("synthetic commit failure") + with self.assertRaisesRegex(sqlite3.OperationalError, "synthetic commit failure"): + self.engine(Probe()).add("synthetic-uncommitted") + self.assertFalse(self.conn.in_transaction) + self.assertFalse(self.vector._lock.locked()) + self.conn.commit() + self.assertEqual(self.conn.execute("SELECT COUNT(*) FROM messages").fetchone()[0], 0) + self.assertEqual(self.ids(), []) + self.assertEqual(self.ids("vec_messages_sep"), []) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-rebuild-source-streaming-756.py b/tests/test-rebuild-source-streaming-756.py new file mode 100644 index 00000000..49143845 --- /dev/null +++ b/tests/test-rebuild-source-streaming-756.py @@ -0,0 +1,1210 @@ +"""Synthetic SQLite and actual rebuild control flow; no model imports.""" + +import ast +import builtins +from collections.abc import Iterator +import datetime +import logging +import math +import os +import sqlite3 +import struct +import sys +import tempfile +import threading +import types +import unittest +from contextlib import closing, contextmanager, nullcontext +from dataclasses import replace +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] + + +def load_modules(): + serving_source = ast.parse((ROOT / "truememory/tier_switch/serving.py").read_text(encoding="utf-8")) + serving_names = {"ServingLeaseError", "ServingLeaseTimeout", "ServingLeaseCancelled"} + serving_namespace = {} + exec(compile(ast.Module(body=[node for node in serving_source.body + if isinstance(node, ast.ClassDef) and node.name in serving_names], + type_ignores=[]), "actual-serving-errors", "exec"), serving_namespace) + runtime_source = ast.parse((ROOT / "truememory/tier_switch/runtime.py").read_text(encoding="utf-8")) + runtime_names = {"TierRuntimeError", "raise_if_serving_rejection", "_read_selection", "_read_policy", + "require_legacy_vector_mutation", "legacy_vector_mutation", + "open_serving_connection", "_open_serving_connection_at_path", "_modules", "_legacy_key"} + runtime_namespace = {"__builtins__": dict(vars(builtins)), "sqlite3": sqlite3, + "contextmanager": contextmanager, "serving": types.SimpleNamespace( + **{name: serving_namespace[name] for name in serving_names})} + runtime_nodes = [ast.ImportFrom(module="__future__", names=[ast.alias(name="annotations")], level=0)] + runtime_nodes.extend(node for node in runtime_source.body + if isinstance(node, (ast.ClassDef, ast.FunctionDef)) and node.name in runtime_names) + exec(compile(ast.fix_missing_locations(ast.Module(body=runtime_nodes, type_ignores=[])), + "actual-runtime-guards", "exec"), runtime_namespace) + + def legacy_operation(conn: sqlite3.Connection) -> types.SimpleNamespace: + vector = modules["vector_search"] + return types.SimpleNamespace(selection=None, policy=None, + tables=(vector._active_vec_table(conn), vector._active_sep_table(conn)), + key=runtime_namespace["_legacy_key"]()) + + modules = {"tier_switch.runtime": types.SimpleNamespace( + **{name: runtime_namespace[name] for name in runtime_names}, + serving_operation=lambda conn, **kwargs: nullcontext(legacy_operation(conn)), + current_operation=legacy_operation), + "reranker": types.SimpleNamespace(get_current_reranker_name=lambda: "synthetic/reranker")} + + def safe_import(name, globals=None, locals=None, fromlist=(), level=0): + if name == "torch": + raise ImportError("Native models are excluded from synthetic tests") + if name == "psutil": + return types.SimpleNamespace() + if name == "truememory": + return types.SimpleNamespace(vector_search=modules["vector_search"], reranker=modules["reranker"]) + if name.startswith("truememory."): + short = name.removeprefix("truememory.") + if short in modules: + return modules[short] + raise AssertionError("Unexpected application import: " + name) + return builtins.__import__(name, globals, locals, fromlist, level) + + runtime_namespace["__builtins__"]["__import__"] = safe_import + for name in ("storage", "_platform", "maintenance", "rebuild_source", "tier_switch.writer", + "embedding_target", "tier_config", "tier_switch.cache", "tier_switch.job", + "tier_switch.source", "tier_switch.activation"): + module = types.ModuleType("synthetic_stream_" + name.replace(".", "_")) + sys.modules[module.__name__] = module + module.__dict__["__builtins__"] = dict(vars(builtins), __import__=safe_import) + path = ROOT / "truememory" / (name.replace(".", "/") + ".py") + exec(compile(path.read_text(encoding="utf-8"), str(path), "exec"), module.__dict__) + modules[name] = module + activation = modules["tier_switch.activation"] + runtime_namespace.update(_guard_storage=activation._guard_storage, _state=activation._state, + read_activation_state=activation.read_activation_state) + vector = types.ModuleType("synthetic_stream_vector") + vector.__dict__.update( + __builtins__=dict(vars(builtins), __import__=safe_import, Path=Path), + database_operation=lambda function: function, + contextmanager=contextmanager, closing=closing, Iterator=Iterator, replace=replace, datetime=datetime, + WriterCapture=modules["tier_switch.writer"].WriterCapture, + sqlite3=sqlite3, logger=logging.getLogger(__name__), _lock=threading.Lock(), + _model_generation=0, EMBEDDING_MODEL="synthetic-model", _embedding_dim=2, + resolve_tier=lambda: "edge", + _BUILD_VECTORS_TXN_BATCH=100, _BUILD_STATE_KEY_PREFIX="vec_build_state:", + _get_batch_size=lambda: 2, _flush_mps_cache=lambda: None, + _active_vec_table=lambda conn: "vec_messages", _active_sep_table=lambda conn: "vec_messages_sep", + serialize_f32=lambda vector: struct.pack("2f", *vector), + _normalize_for_cosine=lambda vector: vector if all(math.isfinite(v) for v in vector) and any(vector) else None, + ) + selected = {"_capture_rebuild_model", "_rebuild_model_fence", "_build_streamed_vectors", + "build_vectors", "build_separation_vectors", "_build_sep_text", "_ensure_metadata_table", + "_build_state_key", "_mark_build_in_progress", "_clear_build_in_progress", + "_build_in_progress", "_write_embedder_metadata_no_commit", "_write_embedder_metadata", + "_write_foreground_embedder_metadata_no_commit", + "_foreground_vector_publication", "_foreground_model_fence", "_validate_foreground_vector_target", + "VectorPublicationChanged", "embed_single"} + tree = ast.parse((ROOT / "truememory/vector_search.py").read_text(encoding="utf-8")) + body = [node for node in tree.body if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) and node.name in selected] + exec(compile(ast.Module(body=body, type_ignores=[]), "actual-vector-builders", "exec"), vector.__dict__) + modules["vector_search"] = vector + runtime_namespace["sys"] = types.SimpleNamespace( + modules={"truememory." + name: module for name, module in modules.items()}) + return modules + + +class TestStreamingBuilders(unittest.TestCase): + def setUp(self): + self.modules = load_modules() + self.source = self.modules["rebuild_source"] + self.vector = self.modules["vector_search"] + self.temp = tempfile.TemporaryDirectory(prefix="synthetic-streaming-") + self.addCleanup(self.temp.cleanup) + self.path = Path(self.temp.name) / "synthetic.sqlite" + self.conn = self.modules["storage"].create_db(self.path) + self.addCleanup(self.conn.close) + self.conn.executescript("CREATE TABLE vec_messages(embedding BLOB); CREATE TABLE vec_messages_sep(embedding BLOB);") + self.model = object() + self.vector._model = self.model + self.vector.get_model = lambda: self.model + self.calls = [] + self.on_encode = None + + def encode(model, texts, **kwargs): + self.assertIs(model, self.model) + self.calls.append(list(texts)) + if self.on_encode is not None: + return self.on_encode(texts) + return [[1., 0.] for _ in texts] + self.vector._encode_with_mps_fallback = encode + + def seed(self, ids=range(1, 10)): + for mid in ids: + self.conn.execute("INSERT INTO messages(id,content,sender,recipient,timestamp) VALUES (?,?,?,?,?)", + (mid, f"synthetic-{mid}", "sender", "recipient", "2026-01-02")) + self.conn.commit() + + def ids(self, table="vec_messages"): + return [row[0] for row in self.conn.execute(f"SELECT rowid FROM {table} ORDER BY rowid")] + + def manifest(self, table="vec_messages"): + return self.source.load_manifest(self.conn, self.source.manifest_key((table,))) + + def writer(self): + conn = self.modules["storage"].create_db(self.path) + self.addCleanup(conn.close) + return conn + + def test_exact_ids_texts_and_separation_with_negative_zero_and_holes(self): + self.seed((-10, 0, 2, 8, 11)) + self.assertEqual(self.vector.build_vectors(self.conn, txn_batch=2), 5) + self.assertEqual(self.ids(), [-10, 0, 2, 8, 11]) + self.assertEqual(sum(self.calls, []), [f"synthetic-{mid}" for mid in (-10, 0, 2, 8, 11)]) + self.calls.clear() + self.assertEqual(self.vector.build_separation_vectors(self.conn, txn_batch=2), 5) + self.assertEqual(sum(self.calls, []), [f"sender to recipient on 2026-01-02: synthetic-{mid}" for mid in (-10, 0, 2, 8, 11)]) + + def test_pages_release_read_and_writer_transactions_before_each_encode(self): + self.seed(range(1, 22)) + writer = self.writer() + def encode(texts): + self.assertFalse(self.conn.in_transaction) + writer.execute("INSERT OR REPLACE INTO metadata(key,value) VALUES ('synthetic-writer','ok')") + writer.commit() + self.assertLessEqual(len(texts), 2) + return [[1., 0.] for _ in texts] + self.on_encode = encode + self.vector.build_vectors(self.conn, txn_batch=3) + self.assertEqual(len(self.ids()), 21) + self.assertFalse(self.conn.in_transaction) + + def test_resume_uses_consumed_input_when_last_vector_skipped(self): + self.seed(range(1, 7)) + def encode(texts): + if len(self.calls) == 2: + raise RuntimeError("synthetic interruption") + return [[1., 0.], [0., 0.]] + self.on_encode = encode + with self.assertRaisesRegex(RuntimeError, "synthetic interruption"): + self.vector.build_vectors(self.conn, txn_batch=1) + self.assertEqual(self.ids(), [1]) + self.assertEqual((self.manifest().cursor, self.manifest().consumed), (2, 2)) + self.on_encode = None + self.calls.clear() + self.assertEqual(self.vector.build_vectors(self.conn, txn_batch=1), 4) + self.assertEqual(sum(self.calls, []), [f"synthetic-{mid}" for mid in range(3, 7)]) + self.assertEqual(self.ids(), [1, 3, 4, 5, 6]) + + def test_all_invalid_outputs_finish_with_nonzero_consumed_cursor(self): + self.seed() + self.on_encode = lambda texts: [[0., 0.] for _ in texts] + self.assertEqual(self.vector.build_vectors(self.conn, txn_batch=2), 0) + manifest = self.manifest() + self.assertEqual((manifest.consumed, manifest.cursor, manifest.outputs, manifest.complete), (9, 9, 0, True)) + self.assertFalse(self.vector._build_in_progress(self.conn, "vec_messages")) + + def test_explicit_list_keeps_order_and_text_without_database_rows(self): + messages = [{"id": 9, "content": "synthetic-nine"}, {"id": -1, "content": "synthetic-minus"}, + {"id": 3, "content": "synthetic-three"}] + self.assertEqual(self.vector.build_vectors(self.conn, messages, txn_batch=1), 3) + self.assertEqual(sum(self.calls, []), [message["content"] for message in messages]) + self.assertIsNone(self.manifest().source) + + def test_caller_transaction_is_preserved_on_success(self): + self.seed() + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('caller','pending')") + reader = self.writer() + self.vector.build_vectors(self.conn, txn_batch=1) + self.assertTrue(self.conn.in_transaction) + self.assertEqual(reader.execute("SELECT COUNT(*) FROM vec_messages").fetchone()[0], 0) + self.conn.rollback() + self.assertEqual(self.ids(), []) + self.assertIsNone(self.manifest()) + + def test_correction_during_compute_rejects_publication_and_fresh_run_restarts(self): + self.seed() + writer = self.writer() + def encode(texts): + writer.execute("UPDATE messages SET content='synthetic-changed' WHERE id=1") + writer.commit() + return [[1., 0.] for _ in texts] + self.on_encode = encode + with self.assertRaises(self.source.RebuildSourceChanged): + self.vector.build_vectors(self.conn, txn_batch=1) + self.assertEqual(self.ids(), []) + self.assertTrue(self.vector._build_in_progress(self.conn, "vec_messages")) + self.on_encode = None + self.vector.build_vectors(self.conn, txn_batch=1) + self.assertEqual(len(self.ids()), 9) + + def test_model_change_during_compute_rejects_rows_and_metadata(self): + self.seed() + def encode(texts): + self.vector._model_generation += 1 + return [[1., 0.] for _ in texts] + self.on_encode = encode + with self.assertRaisesRegex(RuntimeError, "model changed"): + self.vector.build_vectors(self.conn, txn_batch=1) + self.assertEqual(self.ids(), []) + self.assertIsNone(self.conn.execute("SELECT value FROM metadata WHERE key='embed_model'").fetchone()) + + def test_empty_database_finishes_without_loading_a_native_model(self): + def no_load(): + raise AssertionError("Empty source must not load a model") + self.vector.get_model = no_load + self.assertEqual(self.vector.build_vectors(self.conn), 0) + self.assertTrue(self.manifest().complete) + self.assertEqual(self.calls, []) + + def test_incomplete_model_output_is_rejected_before_writes(self): + self.seed() + self.on_encode = lambda texts: [] + with self.assertRaisesRegex(ValueError, "count"): + self.vector.build_vectors(self.conn, txn_batch=1) + self.assertEqual(self.ids(), []) + self.assertEqual(self.manifest().consumed, 0) + + def test_epoch_change_invalidates_saved_resume(self): + self.seed() + def encode(texts): + if len(self.calls) > 1: + raise RuntimeError("synthetic interruption") + return [[1., 0.] for _ in texts] + self.on_encode = encode + with self.assertRaises(RuntimeError): + self.vector.build_vectors(self.conn, txn_batch=1) + old = self.manifest() + self.conn.execute("UPDATE maintenance_source_state SET epoch='synthetic-new-epoch'") + self.conn.commit() + self.on_encode = None + self.calls.clear() + self.vector.build_vectors(self.conn, txn_batch=1) + self.assertNotEqual(self.manifest().generation, old.generation) + self.assertEqual(sum(self.calls, []), [f"synthetic-{mid}" for mid in range(1, 10)]) + + def test_source_mutations_after_fetch_are_classified_without_partial_publication(self): + cases = ( + ("hole", "INSERT INTO messages(id,content) VALUES (3,'synthetic-hole')", False), + ("delete", "DELETE FROM messages WHERE id=2", False), + ("rowid", "UPDATE messages SET rowid=3 WHERE id=2", False), + ("sender", "UPDATE messages SET sender='synthetic-change' WHERE id=2", False), + ("timestamp", "UPDATE messages SET timestamp='2025-01-01' WHERE id=2", False), + ("derived", "UPDATE messages SET episode_id=13 WHERE id=2", True), + ("append", "INSERT INTO messages(id,content) VALUES (100,'synthetic-tail')", True), + ) + for name, sql, accepted in cases: + with self.subTest(name=name): + # Each mutation gets an independent generation and database. + case = TestStreamingBuilders() + case.setUp() + try: + case.seed((2, 4, 6)) + writer = case.writer() + changed = [] + def encode(texts): + if not changed: + changed.append(True) + writer.execute(sql) + writer.commit() + return [[1., 0.] for _ in texts] + case.on_encode = encode + if accepted: + case.vector.build_vectors(case.conn, txn_batch=1) + self.assertEqual(case.ids(), [2, 4, 6]) + self.assertEqual(case.manifest().consumed, 3) + else: + with self.assertRaises(case.source.RebuildSourceChanged): + case.vector.build_vectors(case.conn, txn_batch=1) + self.assertEqual(case.ids(), []) + finally: + case.doCleanups() + + def test_rolled_back_source_correction_does_not_invalidate(self): + self.seed() + writer = self.writer() + def encode(texts): + writer.execute("UPDATE messages SET content='synthetic-rolled-back' WHERE id=1") + writer.rollback() + return [[1., 0.] for _ in texts] + self.on_encode = encode + self.assertEqual(self.vector.build_vectors(self.conn, txn_batch=1), 9) + + def test_short_pages_do_not_pin_a_wal_reader_during_native_work(self): + self.seed() + writer = self.writer() + checks = [] + def encode(texts): + self.assertFalse(self.conn.in_transaction) + writer.execute("INSERT OR REPLACE INTO metadata(key,value) VALUES ('checkpoint-probe','synthetic')") + writer.commit() + busy, log_frames, checkpointed = writer.execute("PRAGMA wal_checkpoint(PASSIVE)").fetchone() + checks.append((busy, log_frames, checkpointed)) + self.assertEqual(busy, 0) + self.assertEqual(log_frames, checkpointed) + return [[1., 0.] for _ in texts] + self.on_encode = encode + self.vector.build_vectors(self.conn, txn_batch=2) + self.assertTrue(checks) + + def test_group_staging_and_commit_cadence_stay_bounded(self): + self.seed(range(1, 24)) + real = self.conn + sizes = [] + class Observed: + commits = 0 + def __getattr__(self, name): + return getattr(real, name) + def execute(self, sql, parameters=()): + normalized = " ".join(sql.lower().split()) + if normalized.startswith("select id, content"): + self_check.assertIn(" limit ?", normalized) + self_check.assertLessEqual(parameters[-1], 2) + return real.execute(sql, parameters) + def executemany(self, sql, rows): + sizes.append((len(rows), sum(len(row[1]) for row in rows))) + return real.executemany(sql, rows) + def commit(self): + self.commits += 1 + real.commit() + self_check = self + observed = Observed() + self.vector.build_vectors(observed, txn_batch=3) + self.assertEqual([item[0] for item in sizes], [6, 6, 6, 5]) + self.assertLessEqual(max(item[1] for item in sizes), 3 * 2 * 2 * 4) + self.assertEqual(observed.commits, 1 + 4 + 1) + + def test_cancellation_before_group_commit_has_no_partial_group(self): + self.seed() + def encode(texts): + if len(self.calls) == 2: + raise KeyboardInterrupt("synthetic cancellation") + return [[1., 0.] for _ in texts] + self.on_encode = encode + with self.assertRaises(KeyboardInterrupt): + self.vector.build_vectors(self.conn, txn_batch=3) + self.assertEqual(self.ids(), []) + self.assertEqual(self.manifest().consumed, 0) + self.assertFalse(self.conn.in_transaction) + + def test_failed_commit_cannot_leave_vectors_or_cursor_for_later_commit(self): + self.seed() + real = self.conn + class DeniedCommit: + commits = 0 + def __getattr__(self, name): + return getattr(real, name) + def commit(self): + self.commits += 1 + if self.commits == 2: + raise sqlite3.OperationalError("synthetic commit failure") + real.commit() + with self.assertRaisesRegex(sqlite3.OperationalError, "synthetic commit failure"): + self.vector.build_vectors(DeniedCommit(), txn_batch=1) + real.commit() + self.assertEqual(self.ids(), []) + self.assertEqual(self.manifest().consumed, 0) + + def test_savepoint_release_denial_rolls_back_group_but_keeps_caller_work(self): + self.seed() + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('caller','pending')") + denied = [] + def authorize(action, first, second, *_args): + if action == sqlite3.SQLITE_SAVEPOINT and first == "RELEASE" and second == "rebuild_source" and self.calls and not denied: + denied.append(True) + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + def encode(texts): + # Reset the statement cache after initial savepoints were prepared. + self.conn.set_authorizer(authorize) + return [[1., 0.] for _ in texts] + self.on_encode = encode + try: + with self.assertRaises(sqlite3.DatabaseError): + self.vector.build_vectors(self.conn, txn_batch=1) + finally: + self.conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.ids(), []) + self.assertEqual(self.manifest().consumed, 0) + self.assertEqual(self.conn.execute("SELECT value FROM metadata WHERE key='caller'").fetchone()[0], "pending") + self.conn.commit() + self.assertEqual(self.ids(), []) + + def test_legacy_marker_with_high_output_id_is_not_a_resume_certificate(self): + self.seed((1, 2, 3)) + self.conn.execute("INSERT INTO vec_messages(rowid,embedding) VALUES (999,?)", (struct.pack("2f", 1., 0.),)) + self.vector._mark_build_in_progress(self.conn, "vec_messages") + self.conn.commit() + self.assertEqual(self.vector.build_vectors(self.conn, txn_batch=1), 3) + self.assertEqual(self.ids(), [1, 2, 3]) + + def test_explicit_input_resume_tracks_position_not_sorted_id(self): + messages = [{"id": mid, "content": f"synthetic-{mid}"} for mid in (9, -4, 8, 2)] + def encode(texts): + if len(self.calls) == 2: + raise RuntimeError("synthetic interruption") + return [[1., 0.] for _ in texts] + self.on_encode = encode + with self.assertRaises(RuntimeError): + self.vector.build_vectors(self.conn, messages, txn_batch=1) + self.on_encode = None + self.calls.clear() + self.assertEqual(self.vector.build_vectors(self.conn, messages, txn_batch=1), 2) + self.assertEqual(sum(self.calls, []), ["synthetic-8", "synthetic-2"]) + + def test_competing_owner_is_rejected_before_loading_or_clearing(self): + self.seed() + ready = threading.Event() + release = threading.Event() + errors = [] + def own(): + try: + with self.modules["maintenance"].maintenance_owner(self.path): + ready.set() + if not release.wait(3): + raise RuntimeError("synthetic owner timeout") + except BaseException as exc: + errors.append(exc) + owner = threading.Thread(target=own) + owner.start() + try: + self.assertTrue(ready.wait(3)) + with self.assertRaises(self.modules["maintenance"].MaintenanceBusyError): + self.vector.build_vectors(self.conn) + self.assertEqual(self.calls, []) + self.assertIsNone(self.manifest()) + finally: + release.set() + owner.join(3) + self.assertFalse(owner.is_alive()) + self.assertEqual(errors, []) + + def test_unmigrated_legacy_connection_keeps_bridge_after_full_storage_migration(self): + legacy_path = Path(self.temp.name) / "synthetic-legacy.sqlite" + legacy = sqlite3.connect(legacy_path) + self.addCleanup(legacy.close) + legacy.executescript("CREATE TABLE messages(id INTEGER PRIMARY KEY,content TEXT,sender TEXT,recipient TEXT,timestamp TEXT);" + "CREATE TABLE vec_messages(embedding BLOB);" + "INSERT INTO messages VALUES(1,'synthetic-legacy','sender','recipient','2026-01-01');") + legacy.execute("INSERT INTO vec_messages(rowid,embedding) VALUES (1,?)", (struct.pack("2f", 1., 0.),)) + legacy.commit() + original_columns = legacy.execute("PRAGMA table_info(messages)").fetchall() + self.assertEqual(self.vector.build_vectors(legacy, txn_batch=1), 1) + self.assertEqual(legacy.execute("SELECT rowid FROM vec_messages").fetchall(), [(1,)]) + self.assertFalse(legacy.in_transaction) + self.assertEqual(legacy.execute("PRAGMA table_info(messages)").fetchall(), original_columns) + self.assertIsNone(legacy.execute("SELECT name FROM sqlite_master WHERE name='maintenance_source_state'").fetchone()) + before = self.source.capture_source(legacy) + self.assertEqual(before.tracker, "rebuild-bridge-v1") + migrated = self.modules["storage"].create_db(legacy_path) + self.addCleanup(migrated.close) + with self.assertRaises(self.source.RebuildSourceChanged): + before.check(migrated) + self.assertEqual(self.vector.build_vectors(migrated, txn_batch=1), 1) + self.assertEqual(self.source.capture_source(migrated).tracker, "rebuild-bridge-v1") + self.assertEqual(len(self.source._installed_triggers(migrated, self.source._BRIDGE_TRIGGERS)), 3) + self.modules["maintenance"].read_source_revision(migrated) + + def test_schema_replacement_during_compute_rejects_target_identity(self): + self.seed() + writer = self.writer() + changed = [] + def encode(texts): + if not changed: + changed.append(True) + writer.execute("CREATE TABLE synthetic_new_schema(value TEXT)") + writer.commit() + return [[1., 0.] for _ in texts] + self.on_encode = encode + with self.assertRaisesRegex(self.source.RebuildSourceChanged, "schema changed"): + self.vector.build_vectors(self.conn, txn_batch=1) + self.assertEqual(self.ids(), []) + self.assertEqual(self.manifest().consumed, 0) + + def test_model_fence_covers_initial_group_final_and_empty_commits(self): + self.seed((1, 2, 3)) + real = self.conn + fixture = self + class Observed: + commits = 0 + def __getattr__(self, name): + return getattr(real, name) + def commit(self): + acquired = fixture.vector._lock.acquire(blocking=False) + if acquired: + fixture.vector._lock.release() + fixture.assertFalse(acquired, "Model identity was released before publication commit") + self.commits += 1 + real.commit() + observed = Observed() + self.vector.build_vectors(observed, txn_batch=1) + self.assertEqual(observed.commits, 4) + real.execute("DELETE FROM messages") + real.commit() + self.vector.build_vectors(observed, txn_batch=1) + self.assertEqual(observed.commits, 5) + + def test_competing_model_switch_waits_until_actual_final_commit(self): + self.seed((1, 2)) + underlying = threading.Lock() + attempted = threading.Event() + switched = threading.Event() + threads = [] + fixture = self + class ObservedLock: + def acquire(self, *args, **kwargs): + if threading.current_thread().name == "synthetic-model-switch": + attempted.set() + return underlying.acquire(*args, **kwargs) + def release(self): + underlying.release() + def __enter__(self): + self.acquire() + return self + def __exit__(self, *_args): + self.release() + self.vector._lock = ObservedLock() + real = self.conn + def switch(): + with fixture.vector._lock: + fixture.vector._model_generation += 1 + fixture.vector.EMBEDDING_MODEL = "synthetic-next-model" + switched.set() + class CommitBoundary: + def __getattr__(self, name): + return getattr(real, name) + def commit(self): + manifest = fixture.manifest() + if manifest is not None and manifest.complete: + thread = threading.Thread(target=switch, name="synthetic-model-switch") + threads.append(thread) + thread.start() + fixture.assertTrue(attempted.wait(3)) + fixture.assertFalse(switched.is_set(), "Model switched before final commit") + real.commit() + try: + self.vector.build_vectors(CommitBoundary(), txn_batch=1) + finally: + for thread in threads: + thread.join(3) + self.assertEqual(len(threads), 1) + self.assertFalse(threads[0].is_alive()) + self.assertTrue(switched.is_set()) + self.assertTrue(self.manifest().complete) + self.assertEqual(self.manifest().model, "synthetic-model") + self.assertEqual(self.conn.execute("SELECT value FROM metadata WHERE key='embed_model'").fetchone()[0], "synthetic-model") + + def test_caller_writer_acquisition_precedes_model_fence_and_release_is_fenced(self): + self.seed((1, 2)) + real = self.conn + real.execute("INSERT INTO metadata(key,value) VALUES ('caller','pending')") + fixture = self + releases = [] + class Observed: + def __init__(self): + self.savepoints = [] + def __getattr__(self, name): + return getattr(real, name) + def execute(self, sql, parameters=()): + if sql == "SAVEPOINT rebuild_source": + self.savepoints.append(False) + if sql == "UPDATE messages SET id = id WHERE 0": + acquired = fixture.vector._lock.acquire(blocking=False) + fixture.assertTrue(acquired, "Writer acquisition must precede model fence") + fixture.vector._lock.release() + self.savepoints[-1] = True + if sql == "RELEASE SAVEPOINT rebuild_source": + publication = self.savepoints.pop() + if publication: + acquired = fixture.vector._lock.acquire(blocking=False) + if acquired: + fixture.vector._lock.release() + fixture.assertFalse(acquired, "Publication RELEASE must retain the model fence") + releases.append(True) + return real.execute(sql, parameters) + self.vector.build_vectors(Observed(), txn_batch=1) + self.assertGreaterEqual(len(releases), 3) + self.assertTrue(real.in_transaction) + real.rollback() + self.assertEqual(self.ids(), []) + + +@unittest.skipUnless(os.environ.get("TRUEMEMORY_TEST_NATIVE_VEC") == "1", + "Actual sqlite-vec verification is an explicit isolated-host gate") +class TestStreamingSQLiteVec(TestStreamingBuilders): + def setUp(self): + super().setUp() + import sqlite_vec + self.sqlite_vec = sqlite_vec + self.conn.enable_load_extension(True) + sqlite_vec.load(self.conn) + self.conn.enable_load_extension(False) + for table in ("vec_messages", "vec_messages_sep"): + self.conn.execute(f"DROP TABLE {table}") + self.conn.execute(f"CREATE VIRTUAL TABLE {table} USING vec0(embedding float[2] distance_metric=cosine)") + self.conn.commit() + + def writer(self): + conn = super().writer() + conn.enable_load_extension(True) + self.sqlite_vec.load(conn) + conn.enable_load_extension(False) + return conn + + +class TestLegacyRebuildBridge(unittest.TestCase): + setUp = TestStreamingBuilders.setUp + + def legacy(self, *, minimal=False, unique_content=False, ids=(-3, 0, 2, 5), cached_statements=0): + path = Path(self.temp.name) / f"synthetic-legacy-{len(getattr(self, 'legacy_paths', []))}.sqlite" + self.legacy_paths = [*getattr(self, "legacy_paths", []), path] + conn = sqlite3.connect(path, cached_statements=cached_statements) + self.addCleanup(conn.close) + fields = "id INTEGER PRIMARY KEY,content TEXT" + if unique_content: + fields += " UNIQUE" + if not minimal: + fields += ",sender TEXT,recipient TEXT,timestamp TEXT" + conn.execute(f"CREATE TABLE messages({fields})") + conn.executescript("CREATE TABLE vec_messages(embedding BLOB); CREATE TABLE vec_messages_sep(embedding BLOB);") + for mid in ids: + if minimal: + conn.execute("INSERT INTO messages VALUES(?,?)", (mid, f"synthetic-{mid}")) + else: + conn.execute("INSERT INTO messages VALUES(?,?,?,?,?)", (mid, f"synthetic-{mid}", "sender", "recipient", "2026-01-02")) + conn.commit() + return conn, path + + def reopen(self, path): + conn = sqlite3.connect(path) + self.addCleanup(conn.close) + return conn + + def test_original_minimal_separation_shape_reaches_encoding_without_full_migration(self): + conn, _ = self.legacy(ids=range(1, 6)) + columns = conn.execute("PRAGMA table_info(messages)").fetchall() + self.assertEqual(self.vector.build_separation_vectors(conn, txn_batch=1), 5) + self.assertEqual(sum(self.calls, []), [f"sender to recipient on 2026-01-02: synthetic-{mid}" for mid in range(1, 6)]) + self.assertEqual(conn.execute("PRAGMA table_info(messages)").fetchall(), columns) + tables = {row[0] for row in conn.execute("SELECT name FROM sqlite_master WHERE type='table'")} + self.assertEqual(tables, {"messages", "vec_messages", "vec_messages_sep", "metadata", self.source._BRIDGE_TABLE}) + self.assertEqual(self.source.capture_source(conn).tracker, "rebuild-bridge-v1") + + def test_id_content_only_completion_preserves_negative_zero_and_hole_ids(self): + conn, _ = self.legacy(minimal=True) + self.assertEqual(self.vector.build_vectors(conn, txn_batch=1), 4) + self.assertEqual(conn.execute("SELECT rowid FROM vec_messages ORDER BY rowid").fetchall(), [(-3,), (0,), (2,), (5,)]) + first = self.source.capture_source(conn) + self.source.ensure_rebuild_tracking(conn) + self.assertEqual(first, self.source.capture_source(conn)) + self.assertEqual(len(conn.execute("PRAGMA table_info(messages)").fetchall()), 2) + + def test_bridge_install_and_successful_build_preserve_caller_transaction(self): + conn, _ = self.legacy() + conn.execute("CREATE TABLE synthetic_caller(value TEXT)") + conn.execute("INSERT INTO synthetic_caller VALUES('pending')") + self.assertEqual(self.vector.build_vectors(conn, txn_batch=1), 4) + self.assertTrue(conn.in_transaction) + self.assertEqual(conn.execute("SELECT value FROM synthetic_caller").fetchone(), ("pending",)) + conn.rollback() + self.assertEqual(conn.execute("SELECT COUNT(*) FROM vec_messages").fetchone()[0], 0) + self.assertIsNone(conn.execute("SELECT 1 FROM sqlite_master WHERE name=?", (self.source._BRIDGE_TABLE,)).fetchone()) + self.assertEqual(conn.execute("SELECT COUNT(*) FROM synthetic_caller").fetchone()[0], 0) + + def test_bridge_resume_across_connection_uses_consumed_cursor(self): + conn, path = self.legacy() + def encode(texts): + if len(self.calls) == 2: + raise RuntimeError("synthetic interruption") + return [[1., 0.] for _ in texts] + self.on_encode = encode + with self.assertRaisesRegex(RuntimeError, "synthetic interruption"): + self.vector.build_vectors(conn, txn_batch=1) + key = self.source.manifest_key(("vec_messages",)) + before = self.source.load_manifest(conn, key) + self.assertEqual((before.cursor, before.consumed), (0, 2)) + self.on_encode = None + self.calls.clear() + reopened = self.reopen(path) + self.assertEqual(self.vector.build_vectors(reopened, txn_batch=1), 2) + after = self.source.load_manifest(reopened, key) + self.assertEqual(before.generation, after.generation) + self.assertEqual(sum(self.calls, []), ["synthetic-2", "synthetic-5"]) + + def test_bridge_rejects_concurrent_corrections_deletes_low_inserts_and_replace(self): + mutations = ("UPDATE messages SET content='synthetic-corrected' WHERE id=2", + "UPDATE messages SET sender='synthetic-new-sender' WHERE id=2", + "UPDATE messages SET rowid=3 WHERE id=2", "DELETE FROM messages WHERE id=2", + "INSERT INTO messages(id,content) VALUES(1,'synthetic-hole')", + "INSERT OR REPLACE INTO messages(id,content) VALUES(2,'synthetic-replacement')") + for statement in mutations: + with self.subTest(statement=statement): + conn, path = self.legacy() + other = self.reopen(path) + other.execute("PRAGMA recursive_triggers=OFF") + changed = [] + def encode(texts): + self.assertFalse(conn.in_transaction) + if not changed: + changed.append(True) + other.execute(statement) + other.commit() + return [[1., 0.] for _ in texts] + self.on_encode = encode + with self.assertRaises(self.source.RebuildSourceChanged): + self.vector.build_vectors(conn, txn_batch=1) + self.assertEqual(conn.execute("SELECT COUNT(*) FROM vec_messages").fetchone()[0], 0) + + def test_bridge_allows_high_id_append_outside_captured_range(self): + conn, path = self.legacy() + other = self.reopen(path) + changed = [] + def encode(texts): + if not changed: + changed.append(True) + other.execute("INSERT INTO messages(id,content) VALUES(9,'synthetic-tail')") + other.commit() + return [[1., 0.] for _ in texts] + self.on_encode = encode + self.assertEqual(self.vector.build_vectors(conn, txn_batch=1), 4) + self.assertEqual(conn.execute("SELECT rowid FROM vec_messages ORDER BY rowid").fetchall(), [(-3,), (0,), (2,), (5,)]) + manifest = self.source.load_manifest(conn, self.source.manifest_key(("vec_messages",))) + self.assertEqual((manifest.source.high_id, manifest.consumed), (5, 4)) + manifest.source.check(conn) + + def test_bridge_noop_and_derived_only_writes_do_not_dirty_source(self): + conn, _ = self.legacy() + conn.execute("ALTER TABLE messages ADD COLUMN episode_id INTEGER") + self.source.ensure_rebuild_tracking(conn) + before = self.source.capture_source(conn) + conn.execute("UPDATE messages SET content=content,episode_id=99") + conn.commit() + self.assertEqual(before.revision, self.source.capture_source(conn).revision) + + def test_missing_replaced_trigger_new_column_or_missing_state_rotates_epoch(self): + for fault in ("missing_trigger", "replaced_trigger", "new_column", "missing_state"): + with self.subTest(fault=fault): + conn, _ = self.legacy() + self.source.ensure_rebuild_tracking(conn) + before = self.source.capture_source(conn) + trigger = self.source._BRIDGE_TRIGGERS[1] + if fault in ("missing_trigger", "replaced_trigger"): + conn.execute(f"DROP TRIGGER {trigger}") + if fault == "replaced_trigger": + conn.execute(f"CREATE TRIGGER {trigger} AFTER UPDATE ON messages BEGIN SELECT 1; END") + conn.execute("UPDATE messages SET content='synthetic-untracked' WHERE id=2") + elif fault == "new_column": + conn.execute("ALTER TABLE messages ADD COLUMN metadata TEXT") + else: + conn.execute(f"DELETE FROM {self.source._BRIDGE_TABLE}") + conn.commit() + with self.assertRaises(self.source.RebuildSourceChanged): + before.check(conn) + self.source.ensure_rebuild_tracking(conn) + after = self.source.capture_source(conn) + self.assertNotEqual(before.revision.epoch, after.revision.epoch) + with self.assertRaises(self.source.RebuildSourceChanged): + before.check(conn) + + def test_rolled_back_bridge_source_change_does_not_invalidate(self): + conn, _ = self.legacy() + self.source.ensure_rebuild_tracking(conn) + before = self.source.capture_source(conn) + conn.execute("UPDATE messages SET content='synthetic-rolled-back' WHERE id=2") + conn.rollback() + self.assertEqual(before, self.source.capture_source(conn)) + + def test_denied_setup_or_release_preserves_target_and_caller_data_before_model(self): + # Python 3.10 keeps at least five cached statements even when given zero. + for fault, cached_statements in ((fault, size) for fault in ("ddl", "release") for size in (0, 5, 100)): + with self.subTest(fault=fault, cached_statements=cached_statements): + conn, _ = self.legacy(cached_statements=cached_statements) + conn.execute("INSERT INTO vec_messages(rowid,embedding) VALUES (99,?)", (struct.pack("2f", 1., 0.),)) + conn.execute("CREATE TABLE synthetic_caller(value TEXT)") + conn.commit() + conn.execute("INSERT INTO synthetic_caller VALUES('pending')") + denied = [] + def authorize(action, first, second, _database, _source): + is_target = ((fault == "ddl" and action == sqlite3.SQLITE_CREATE_TRIGGER) + or (fault == "release" and action == sqlite3.SQLITE_SAVEPOINT and first == "RELEASE" + and second == "rebuild_source")) + if is_target and not denied: + denied.append(True) + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + bridge_table = self.source._BRIDGE_TABLE + armed = [] + + class Probe: + def __getattr__(self, name: str) -> object: + return getattr(conn, name) + + def execute(self, sql: str, *args: object) -> sqlite3.Cursor: + if fault == "release" and sql == "RELEASE SAVEPOINT rebuild_source" and not armed: + installed = conn.execute( + "SELECT 1 FROM sqlite_master WHERE name=?", (bridge_table,), + ).fetchone() is not None + if installed: + # Authorizers run at prepare time. Re-arm outside + # the callback so cached RELEASE is reauthorized. + conn.set_authorizer(authorize) + armed.append(True) + return conn.execute(sql, *args) + + if fault == "ddl": + conn.set_authorizer(authorize) + before_calls = len(self.calls) + try: + with self.assertRaises(sqlite3.DatabaseError): + self.vector.build_vectors(Probe(), txn_batch=1) + finally: + conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.assertEqual(denied, [True]) + self.assertEqual(armed, [True] if fault == "release" else []) + self.assertTrue(conn.in_transaction) + self.assertEqual(len(self.calls), before_calls) + conn.commit() + self.assertEqual(conn.execute("SELECT value FROM synthetic_caller").fetchone(), ("pending",)) + self.assertEqual(conn.execute("SELECT rowid FROM vec_messages").fetchall(), [(99,)]) + self.assertIsNone(conn.execute("SELECT 1 FROM sqlite_master WHERE name=?", (self.source._BRIDGE_TABLE,)).fetchone()) + + def test_bridge_commit_failure_rolls_back_install_before_model_or_clear(self): + conn, _ = self.legacy() + conn.execute("INSERT INTO vec_messages(rowid,embedding) VALUES (99,?)", (struct.pack("2f", 1., 0.),)) + conn.commit() + class Probe: + def __getattr__(self, name): + return getattr(conn, name) + def commit(self): + raise sqlite3.OperationalError("synthetic setup commit failure") + with self.assertRaisesRegex(sqlite3.OperationalError, "synthetic setup commit failure"): + self.vector.build_vectors(Probe(), txn_batch=1) + self.assertEqual(self.calls, []) + conn.commit() + self.assertEqual(conn.execute("SELECT rowid FROM vec_messages").fetchall(), [(99,)]) + self.assertIsNone(conn.execute("SELECT 1 FROM sqlite_master WHERE name=?", (self.source._BRIDGE_TABLE,)).fetchone()) + + def test_readonly_legacy_install_fails_before_model_or_clear(self): + conn, path = self.legacy() + readonly = sqlite3.connect(f"file:{path}?mode=ro", uri=True) + self.addCleanup(readonly.close) + with self.assertRaises(sqlite3.OperationalError): + self.vector.build_vectors(readonly, txn_batch=1) + self.assertEqual(self.calls, []) + self.assertFalse(readonly.in_transaction) + self.assertEqual(conn.execute("SELECT COUNT(*) FROM messages").fetchone()[0], 4) + + def test_unsupported_source_schema_fails_before_model(self): + conn, _ = self.legacy() + conn.execute("ALTER TABLE messages RENAME TO synthetic_old_messages") + conn.execute("CREATE TABLE messages(id TEXT PRIMARY KEY,content TEXT)") + with self.assertRaisesRegex(self.modules["maintenance"].MaintenanceUnavailableError, "INTEGER PRIMARY KEY"): + self.vector.build_vectors(conn, txn_batch=1) + self.assertEqual(self.calls, []) + + def test_empty_legacy_generation_never_loads_a_model(self): + conn, _ = self.legacy(ids=()) + def no_model(): + self.fail("Empty legacy build loaded a model") + self.vector.get_model = no_model + self.assertEqual(self.vector.build_vectors(conn, txn_batch=1), 0) + manifest = self.source.load_manifest(conn, self.source.manifest_key(("vec_messages",))) + self.assertTrue(manifest.complete) + self.assertEqual((manifest.source.tracker, manifest.total), ("rebuild-bridge-v1", 0)) + + def test_bridge_schema_change_during_encode_rejects_publication_and_restarts(self): + for fault in ("drop_trigger", "add_source_column"): + with self.subTest(fault=fault): + conn, path = self.legacy() + other = self.reopen(path) + changed = [] + def encode(texts): + if not changed: + changed.append(True) + if fault == "drop_trigger": + other.execute(f"DROP TRIGGER {self.source._BRIDGE_TRIGGERS[1]}") + other.execute("UPDATE messages SET content='synthetic-untracked-correction' WHERE id=2") + else: + other.execute("ALTER TABLE messages ADD COLUMN metadata TEXT") + other.commit() + return [[1., 0.] for _ in texts] + self.on_encode = encode + with self.assertRaises(self.source.RebuildSourceChanged): + self.vector.build_vectors(conn, txn_batch=1) + key = self.source.manifest_key(("vec_messages",)) + before = self.source.load_manifest(conn, key) + self.assertEqual(conn.execute("SELECT COUNT(*) FROM vec_messages").fetchone()[0], 0) + self.on_encode = None + self.calls.clear() + self.assertEqual(self.vector.build_vectors(conn, txn_batch=1), 4) + after = self.source.load_manifest(conn, key) + self.assertNotEqual(before.source.revision.epoch, after.source.revision.epoch) + self.assertNotEqual(before.generation, after.generation) + self.assertEqual(len(sum(self.calls, [])), 4) + + def test_canonical_handoff_does_not_resume_an_incomplete_bridge_prefix(self): + conn = self.conn + conn.executemany("INSERT INTO messages(id,content) VALUES (?,?)", + [(mid, f"synthetic-{mid}") for mid in (-3, 0, 2, 5)]) + conn.execute("UPDATE maintenance_source_state SET tracking_ready=0") + conn.commit() + def encode(texts): + if len(self.calls) == 2: + raise RuntimeError("synthetic interruption") + return [[1., 0.] for _ in texts] + self.on_encode = encode + with self.assertRaisesRegex(RuntimeError, "synthetic interruption"): + self.vector.build_vectors(conn, txn_batch=1) + key = self.source.manifest_key(("vec_messages",)) + before = self.source.load_manifest(conn, key) + self.modules["storage"]._initialize_maintenance_tracking(conn) + self.on_encode = None + self.calls.clear() + self.assertEqual(self.vector.build_vectors(conn, txn_batch=1), 4) + after = self.source.load_manifest(conn, key) + self.assertNotEqual(before.generation, after.generation) + self.assertEqual((before.source.tracker, after.source.tracker), ("rebuild-bridge-v1", "canonical-v1")) + self.assertEqual(sum(self.calls, []), ["synthetic--3", "synthetic-0", "synthetic-2", "synthetic-5"]) + self.assertEqual(self.source._installed_triggers(conn, self.source._BRIDGE_TRIGGERS), {}) + self.assertEqual(conn.execute(f"SELECT tracking_ready FROM {self.source._BRIDGE_TABLE}").fetchone()[0], 0) + + def test_actual_deprecated_open_rebuilds_existing_legacy_connection(self): + import warnings + from unittest.mock import patch + conn, path = self.legacy() + tree = ast.parse((ROOT / "truememory/engine.py").read_text(encoding="utf-8")) + original = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == "TrueMemoryEngine") + method = next(node for node in original.body if isinstance(node, ast.FunctionDef) and node.name == "open") + schedule = next(node for node in original.body + if isinstance(node, ast.FunctionDef) and node.name == "_maybe_auto_consolidate") + cls = ast.ClassDef(name="LegacyEngine", bases=[], keywords=[], body=[method, schedule], decorator_list=[]) + self.vector._check_embedder_compatibility = lambda _conn: None + self.vector.vectors_are_built = lambda _conn, _table: False + namespace = dict(__builtins__=self.vector.__dict__["__builtins__"], + warnings=warnings, sqlite3=sqlite3, DEFAULT_BUSY_TIMEOUT_MS=1000, + logger=logging.getLogger(__name__), _HAS_VECTOR=True, _HAS_STYLE_VEC=False, + _HAS_HYBRID=False, _HAS_CLUSTERING=False, resolve_tier=lambda: "base", + _check_rebuild_allowed=lambda _conn: None, init_vec_table=lambda _conn: None, + build_vectors=self.vector.build_vectors, + TrueMemoryMigrationError=type("SyntheticMigrationError", (RuntimeError,), {})) + exec(compile(ast.fix_missing_locations(ast.Module(body=[cls], type_ignores=[])), "actual-legacy-open", "exec"), namespace) + engine = namespace["LegacyEngine"]() + engine.db_path, engine.stats = path, {} + engine._write_lock = threading.Lock() + engine._has_consolidation = engine._has_style_vec = False + engine._purge_legacy_entity_profile_summaries = lambda: None + fake_vec = types.ModuleType("sqlite_vec") + fake_vec.load = lambda _conn: None + with patch.dict(sys.modules, {"sqlite_vec": fake_vec}), warnings.catch_warnings(record=True): + self.assertIs(engine.open(), engine) + self.addCleanup(engine.conn.close) + self.assertTrue(engine._has_vectors) + self.assertTrue(engine.ready) + self.assertEqual(engine.stats["message_count"], 4) + self.assertEqual(conn.execute("SELECT COUNT(*) FROM vec_messages").fetchone()[0], 4) + self.assertEqual(self.source.capture_source(engine.conn).tracker, "rebuild-bridge-v1") + self.assertIsNone(conn.execute("SELECT 1 FROM sqlite_master WHERE name='maintenance_source_state'").fetchone()) + + def test_custom_unique_replacements_invalidate_conservative_bridge_even_with_canonical_ready(self): + for statement in ( + "INSERT OR REPLACE INTO messages(id,content,synthetic_unique) VALUES(9,'synthetic-high-replacement','slot-2')", + "UPDATE OR REPLACE messages SET synthetic_unique='slot-2' WHERE id=1", + ): + with self.subTest(statement=statement): + path = Path(self.temp.name) / ("synthetic-canonical-insert.sqlite" if statement.startswith("INSERT") + else "synthetic-canonical-update.sqlite") + conn = self.modules["storage"].create_db(path) + self.addCleanup(conn.close) + conn.execute("ALTER TABLE messages ADD COLUMN synthetic_unique TEXT") + conn.execute("CREATE UNIQUE INDEX synthetic_unique_key ON messages(synthetic_unique)") + conn.executemany("INSERT INTO messages(id,content,synthetic_unique) VALUES(?,?,?)", + [(1, "synthetic-first", "slot-1"), (2, "synthetic-second", "slot-2")]) + conn.commit() + other = self.reopen(path) + other.execute("PRAGMA recursive_triggers=OFF") + canonical_before = self.modules["maintenance"].read_source_revision(conn) + changed = [] + def encode(texts): + if not changed: + changed.append(True) + other.execute(statement) + other.commit() + return [[1., 0.] for _ in texts] + self.on_encode = encode + conn.executescript("CREATE TABLE vec_messages(embedding BLOB); CREATE TABLE vec_messages_sep(embedding BLOB);") + with self.assertRaises(self.source.RebuildSourceChanged): + self.vector.build_vectors(conn, txn_batch=1) + # Canonical tracking also fences these UNIQUE writes; the + # rebuild retains its independently scoped bridge certificate. + canonical_after = self.modules["maintenance"].read_source_revision(conn) + self.assertFalse(canonical_after.is_append_only_since(canonical_before)) + manifest = self.source.load_manifest(conn, self.source.manifest_key(("vec_messages",))) + self.assertEqual(manifest.source.tracker, "rebuild-bridge-conservative-v1") + self.assertEqual(conn.execute("SELECT COUNT(*) FROM vec_messages").fetchone()[0], 0) + + def test_expression_partial_unique_index_is_conservative_and_index_changes_rotate_epoch(self): + conn, _ = self.legacy() + conn.execute("ALTER TABLE messages ADD COLUMN synthetic_unique TEXT") + self.source.ensure_rebuild_tracking(conn) + normal = self.source.capture_source(conn) + conn.execute("CREATE UNIQUE INDEX synthetic_unique_expression ON messages(lower(synthetic_unique)) WHERE synthetic_unique IS NOT NULL") + conn.commit() + with self.assertRaises(self.source.RebuildSourceChanged): + normal.check(conn) + self.source.ensure_rebuild_tracking(conn) + conservative = self.source.capture_source(conn) + self.assertEqual(conservative.tracker, "rebuild-bridge-conservative-v1") + self.assertNotEqual(normal.revision.epoch, conservative.revision.epoch) + conn.execute("INSERT INTO messages(id,content) VALUES(99,'synthetic-high-append')") + conn.commit() + with self.assertRaises(self.source.RebuildSourceChanged): + conservative.check(conn) + + def test_canonical_handoff_retires_only_bridge_owned_triggers(self): + conn = self.conn + conn.execute("UPDATE maintenance_source_state SET tracking_ready=0") + conn.commit() + self.source.ensure_rebuild_tracking(conn) + conn.execute("CREATE TRIGGER synthetic_unrelated AFTER UPDATE ON messages BEGIN SELECT 1; END") + conn.commit() + self.modules["storage"]._initialize_maintenance_tracking(conn) + self.source.ensure_rebuild_tracking(conn) + self.assertEqual(self.source.capture_source(conn).tracker, "canonical-v1") + self.assertEqual(self.source._installed_triggers(conn, self.source._BRIDGE_TRIGGERS), {}) + self.assertIsNotNone(conn.execute("SELECT 1 FROM sqlite_master WHERE name='synthetic_unrelated'").fetchone()) + + def test_unowned_reserved_trigger_is_not_replaced_or_silently_adopted(self): + conn, _ = self.legacy() + trigger = self.source._BRIDGE_TRIGGERS[0] + conn.execute(f"CREATE TRIGGER {trigger} AFTER INSERT ON messages BEGIN SELECT 1; END") + original = conn.execute("SELECT sql FROM sqlite_master WHERE name=?", (trigger,)).fetchone()[0] + with self.assertRaisesRegex(self.modules["maintenance"].MaintenanceUnavailableError, "already in use"): + self.vector.build_vectors(conn) + self.assertEqual(self.calls, []) + self.assertEqual(conn.execute("SELECT sql FROM sqlite_master WHERE name=?", (trigger,)).fetchone()[0], original) + + def test_schema_attestation_accepts_caller_row_factory(self): + conn, _ = self.legacy() + conn.row_factory = sqlite3.Row + self.assertEqual(self.vector.build_vectors(conn, txn_batch=1), 4) + self.assertEqual(self.source.capture_source(conn).tracker, "rebuild-bridge-v1") + + def test_unique_column_constraint_detects_hidden_replacement_deletion(self): + conn, _ = self.legacy(unique_content=True) + conn.execute("PRAGMA recursive_triggers=OFF") + self.source.ensure_rebuild_tracking(conn) + before = self.source.capture_source(conn) + self.assertEqual(before.tracker, "rebuild-bridge-conservative-v1") + conn.execute("INSERT OR REPLACE INTO messages(id,content) VALUES(99,'synthetic-2')") + conn.commit() + self.assertIsNone(conn.execute("SELECT 1 FROM messages WHERE id=2").fetchone()) + self.assertEqual(conn.execute("SELECT COUNT(*) FROM messages").fetchone()[0], 4) + with self.assertRaises(self.source.RebuildSourceChanged): + before.check(conn) + + def test_indexed_integer_primary_key_replacement_on_rowid_invalidates_prefix(self): + conn = sqlite3.connect(":memory:") + self.addCleanup(conn.close) + conn.execute("CREATE TABLE messages(id INTEGER PRIMARY KEY DESC, content TEXT)") + conn.execute("INSERT INTO messages(rowid,id,content) VALUES(1,1,'synthetic-first')") + conn.commit() + conn.execute("PRAGMA recursive_triggers=OFF") + self.source.ensure_rebuild_tracking(conn) + before = self.source.capture_source(conn) + self.assertEqual(before.tracker, "rebuild-bridge-conservative-v1") + conn.execute("INSERT OR REPLACE INTO messages(rowid,id,content) VALUES(1,9,'synthetic-replacement')") + conn.commit() + self.assertEqual(conn.execute("SELECT id FROM messages").fetchall(), [(9,)]) + with self.assertRaises(self.source.RebuildSourceChanged): + before.check(conn) + + def test_removing_custom_unique_index_enables_safe_canonical_handoff(self): + conn = self.conn + conn.execute("CREATE UNIQUE INDEX synthetic_content_key ON messages(content)") + self.source.ensure_rebuild_tracking(conn) + before = self.source.capture_source(conn) + self.assertEqual(before.tracker, "rebuild-bridge-conservative-v1") + conn.execute("DROP INDEX synthetic_content_key") + conn.commit() + with self.assertRaises(self.source.RebuildSourceChanged): + before.check(conn) + self.source.ensure_rebuild_tracking(conn) + self.assertEqual(self.source.capture_source(conn).tracker, "canonical-v1") + self.assertEqual(self.source._installed_triggers(conn, self.source._BRIDGE_TRIGGERS), {}) + + def test_collation_equivalent_changes_reject_actual_builder_publication(self): + for canonical_ready in (False, True): + for collation, changed_text in (("NOCASE", "synthetic"), ("RTRIM", "Synthetic ")): + with self.subTest(canonical_ready=canonical_ready, collation=collation): + path = Path(self.temp.name) / f"synthetic-collation-{canonical_ready}-{collation}.sqlite" + conn = sqlite3.connect(path) + self.addCleanup(conn.close) + if canonical_ready: + schema = self.modules["storage"]._SCHEMA_SQL.replace( + "content TEXT NOT NULL,", f"content TEXT COLLATE {collation} NOT NULL,", 1, + ) + conn.executescript(schema) + self.modules["storage"]._initialize_maintenance_tracking(conn) + else: + conn.execute(f"CREATE TABLE messages(id INTEGER PRIMARY KEY, content TEXT COLLATE {collation})") + conn.executescript("CREATE TABLE vec_messages(embedding BLOB); CREATE TABLE vec_messages_sep(embedding BLOB);") + conn.execute("INSERT INTO messages(id,content) VALUES(1,'Synthetic')") + conn.commit() + other = self.reopen(path) + if canonical_ready: + canonical_before = self.modules["maintenance"].read_source_revision(conn) + def encode(texts): + self.assertEqual(texts, ["Synthetic"]) + self.assertFalse(conn.in_transaction) + other.execute("UPDATE messages SET content=? WHERE id=1", (changed_text,)) + other.commit() + return [[1., 0.]] + self.on_encode = encode + with self.assertRaises(self.source.RebuildSourceChanged): + self.vector.build_vectors(conn, txn_batch=1) + if canonical_ready: + # Explicit BINARY comparison also invalidates the + # canonical source counters for this collated edit. + canonical_after = self.modules["maintenance"].read_source_revision(conn) + self.assertGreater(canonical_after.revision, canonical_before.revision) + self.assertGreater(canonical_after.correction_count, canonical_before.correction_count) + self.assertFalse(canonical_after.is_append_only_since(canonical_before)) + self.assertEqual(conn.execute("SELECT COUNT(*) FROM vec_messages").fetchone()[0], 0) + manifest = self.source.load_manifest(conn, self.source.manifest_key(("vec_messages",))) + self.assertEqual(manifest.source.tracker, "rebuild-bridge-v1") + self.assertEqual((manifest.complete, manifest.consumed), (False, 0)) + self.on_encode = None + self.calls.clear() + self.assertEqual(self.vector.build_vectors(conn, txn_batch=1), 1) + self.assertEqual(self.calls, [[changed_text]]) + + def test_bridge_compares_storage_type_when_blob_cast_bytes_match(self): + conn = sqlite3.connect(":memory:") + self.addCleanup(conn.close) + conn.execute("CREATE TABLE messages(id INTEGER PRIMARY KEY, content)") + conn.execute("INSERT INTO messages VALUES(1,?)", ("synthetic",)) + conn.commit() + self.source.ensure_rebuild_tracking(conn) + before = self.source.capture_source(conn) + conn.execute("UPDATE messages SET content=? WHERE id=1", (b"synthetic",)) + conn.commit() + self.assertEqual(conn.execute("SELECT CAST(content AS BLOB) FROM messages").fetchone()[0], b"synthetic") + with self.assertRaises(self.source.RebuildSourceChanged): + before.check(conn) + + def test_standard_messages_definition_reuses_canonical_without_bridge(self): + self.source.ensure_rebuild_tracking(self.conn) + self.assertEqual(self.source.capture_source(self.conn).tracker, "canonical-v1") + self.assertIsNone(self.conn.execute("SELECT 1 FROM sqlite_master WHERE name=?", (self.source._BRIDGE_TABLE,)).fetchone()) + + def test_compatible_custom_definition_keeps_bridge_despite_ready_canonical(self): + conn = sqlite3.connect(":memory:") + self.addCleanup(conn.close) + schema = self.modules["storage"]._SCHEMA_SQL.replace("sender TEXT DEFAULT '',", "sender TEXT DEFAULT '' COLLATE BINARY,", 1) + conn.executescript(schema) + self.modules["storage"]._initialize_maintenance_tracking(conn) + self.source.ensure_rebuild_tracking(conn) + self.assertEqual(self.source.capture_source(conn).tracker, "rebuild-bridge-v1") + self.modules["maintenance"].read_source_revision(conn) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-rebuild-value-exactness-756.py b/tests/test-rebuild-value-exactness-756.py new file mode 100644 index 00000000..dd4ee806 --- /dev/null +++ b/tests/test-rebuild-value-exactness-756.py @@ -0,0 +1,348 @@ +"""Source-value fences using real SQLite and model-free rebuild control flow.""" + +import math +from pathlib import Path +import runpy +import sqlite3 +import struct +import unittest +import uuid + + +_LOAD_MODULES = runpy.run_path( + str(Path(__file__).with_name("test-rebuild-source-streaming-756.py")), +)["load_modules"] + + +class TestRebuildValueExactness(unittest.TestCase): + def setUp(self) -> None: + modules = _LOAD_MODULES() + self.source = modules["rebuild_source"] + self.storage = modules["storage"] + self.vector = modules["vector_search"] + self.model = object() + self.vector.get_model = lambda: self.model + self.vector._get_batch_size = lambda: 1 + self.calls = [] + self.vector._encode_with_mps_fallback = self.encode + + def encode(self, model: object, texts: list, **kwargs: object) -> list[list[float]]: + self.assertIs(model, self.model) + self.calls.append(list(texts)) + return [[1.0, 0.0] for _ in texts] + + def database(self, fields: str = "content", *, shared: bool = False) -> tuple[sqlite3.Connection, sqlite3.Connection | None]: + uri = "file:synthetic-value-fence-" + uuid.uuid4().hex + "?mode=memory&cache=shared" + conn = sqlite3.connect(uri if shared else ":memory:", uri=shared, cached_statements=0) + self.addCleanup(conn.close) + conn.execute(f"CREATE TABLE messages(id INTEGER PRIMARY KEY, {fields})") + conn.execute("CREATE TABLE vec_messages(embedding BLOB)") + conn.execute("CREATE TABLE vec_messages_sep(embedding BLOB)") + other = sqlite3.connect(uri, uri=True, cached_statements=0) if shared else None + if other is not None: + self.addCleanup(other.close) + return conn, other + + def seed(self, conn: sqlite3.Connection, value: object) -> None: + conn.execute("INSERT INTO messages(id,content) VALUES(1,?)", (value,)) + conn.commit() + self.source.ensure_rebuild_tracking(conn) + + def manifest(self, conn: sqlite3.Connection, table: str = "vec_messages_sep") -> object: + return self.source.load_manifest(conn, self.source.manifest_key((table,))) + + def install_previous_update_trigger(self, conn: sqlite3.Connection) -> None: + covered, _, conservative = self.source._source_schema(conn) + self.assertFalse(conservative) + changed = " OR ".join( + f"typeof(old.{name}) IS NOT typeof(new.{name}) " + f"OR CAST(old.{name} AS BLOB) IS NOT CAST(new.{name} AS BLOB)" + for name in covered + ) + conn.execute("DROP TRIGGER truememory_rebuild_messages_v1_au") + conn.execute(f"""CREATE TRIGGER truememory_rebuild_messages_v1_au + AFTER UPDATE ON messages WHEN {changed} BEGIN + UPDATE truememory_rebuild_source_v1 SET nonappend_revision = revision + 1, + max_seen_message_id = MAX(max_seen_message_id, new.id), + revision = revision + 1, correction_count = correction_count + 1 + WHERE singleton = 1 AND tracking_ready = 1; + END""") + conn.commit() + + def test_storage_value_comparison_matrix(self) -> None: + values = [None, 0, 1, -1, 9007199254740992, 9007199254740993, + 0.0, -0.0, 1.0, 1.0000000000000002, 1.0000000000000004, + 5e-324, float("inf"), float("-inf"), "0", "synthetic", "Synthetic", + "Synthetic ", "a\x00b", "a\x00c", b"synthetic", b"a\x00b", b"a\x00c"] + conn, _ = self.database() + self.source.ensure_rebuild_tracking(conn) + for before_value in values: + for after_value in values: + with self.subTest(before=repr(before_value), after=repr(after_value)): + conn.execute("DELETE FROM messages") + conn.execute("INSERT INTO messages VALUES(1,?)", (before_value,)) + conn.commit() + before = self.source.capture_source(conn) + stored_before = conn.execute("SELECT content FROM messages").fetchone()[0] + conn.execute("UPDATE messages SET content=?", (after_value,)) + conn.commit() + stored_after = conn.execute("SELECT content FROM messages").fetchone()[0] + changed = repr(stored_before) != repr(stored_after) + zero = isinstance(stored_before, float) and stored_before == 0 + if changed or zero: + with self.assertRaises(self.source.RebuildSourceChanged): + before.check(conn) + else: + before.check(conn) + self.assertEqual(before, self.source.capture_source(conn)) + + def test_text_affinity_noop_and_collation_controls(self) -> None: + cases = [("TEXT", 1.0000000000000002, 1.0000000000000004, False), + ("REAL", 1.25, 1.25, False), + ("TEXT COLLATE NOCASE", "Synthetic", "synthetic", True), + ("TEXT COLLATE RTRIM", "Synthetic", "Synthetic ", True)] + for affinity, old, new, expected_change in cases: + with self.subTest(affinity=affinity): + conn, _ = self.database("content " + affinity) + self.seed(conn, old) + before = self.source.capture_source(conn) + conn.execute("UPDATE messages SET content=?", (new,)) + conn.commit() + if expected_change: + with self.assertRaises(self.source.RebuildSourceChanged): + before.check(conn) + else: + before.check(conn) + self.assertEqual(before, self.source.capture_source(conn)) + + def test_real_sender_precision_rejects_actual_separation_publication(self) -> None: + for affinity in ("REAL", ""): + with self.subTest(sender_affinity=affinity): + conn, other = self.database( + f"content TEXT, sender {affinity}, recipient TEXT, timestamp TEXT", shared=True, + ) + conn.execute("INSERT INTO messages VALUES(1,?,?,?,?)", + ("synthetic content", 1.0000000000000002, "recipient", "2026-01-02")) + conn.commit() + encoded = [] + + def encode(model: object, texts: list, **kwargs: object) -> list[list[float]]: + encoded.extend(texts) + self.assertTrue(all(isinstance(text, str) for text in texts)) + self.assertFalse(conn.in_transaction) + other.execute("UPDATE messages SET sender=?", (1.0000000000000004,)) + other.commit() + return [[1.0, 0.0]] + + self.vector._encode_with_mps_fallback = encode + with self.assertRaises(self.source.RebuildSourceChanged): + self.vector.build_separation_vectors(conn, txn_batch=1) + row = conn.execute("SELECT sender,recipient,timestamp,content FROM messages").fetchone() + self.assertNotEqual(encoded[0], self.vector._build_sep_text(*row)) + self.assertEqual(conn.execute("SELECT COUNT(*) FROM vec_messages_sep").fetchone()[0], 0) + self.assertEqual((self.manifest(conn).complete, self.manifest(conn).consumed), (False, 0)) + + def test_signed_zero_directions_reject_actual_separation_publication(self) -> None: + for field in ("content", "sender"): + for old, new in ((0.0, -0.0), (-0.0, 0.0)): + with self.subTest(field=field, old=repr(old)): + fields = "content, sender TEXT" if field == "content" else "content TEXT, sender" + conn, other = self.database(fields + ", recipient TEXT, timestamp TEXT", shared=True) + values = {"content": "synthetic content", "sender": "sender"} + values[field] = old + conn.execute("INSERT INTO messages VALUES(1,?,?,?,?)", + (values["content"], values["sender"], "recipient", "2026-01-02")) + conn.commit() + encoded = [] + + def encode(model: object, texts: list, **kwargs: object) -> list[list[float]]: + encoded.extend(texts) + self.assertTrue(all(isinstance(text, str) for text in texts)) + other.execute(f"UPDATE messages SET {field}=?", (new,)) + other.commit() + return [[1.0, 0.0]] + + self.vector._encode_with_mps_fallback = encode + with self.assertRaises(self.source.RebuildSourceChanged): + self.vector.build_separation_vectors(conn, txn_batch=1) + stored = conn.execute(f"SELECT {field} FROM messages").fetchone()[0] + self.assertEqual(math.copysign(1, stored), math.copysign(1, new)) + row = conn.execute("SELECT sender,recipient,timestamp,content FROM messages").fetchone() + if field == "content": + self.assertNotEqual(encoded[0], self.vector._build_sep_text(*row)) + else: + # Metadata zero becomes '?'; the source fence is more + # conservative than this particular text transform. + self.assertEqual(encoded[0], self.vector._build_sep_text(*row)) + self.assertFalse(self.manifest(conn).complete) + self.assertEqual(conn.execute("SELECT COUNT(*) FROM vec_messages_sep").fetchone()[0], 0) + + def test_zero_source_write_invalidates_but_derived_only_write_does_not(self) -> None: + conn, _ = self.database("content, episode_id INTEGER") + self.seed(conn, -0.0) + before = self.source.capture_source(conn) + conn.execute("UPDATE messages SET episode_id=7") + conn.commit() + before.check(conn) + self.assertEqual(before, self.source.capture_source(conn)) + conn.execute("UPDATE messages SET content=content") + conn.commit() + with self.assertRaises(self.source.RebuildSourceChanged): + before.check(conn) + + def test_unshadowed_rowid_aliases_still_invalidate_source(self) -> None: + for name in ("rowid", "ROWID", "_rowid_", "OID"): + with self.subTest(alias=name): + conn, _ = self.database() + self.seed(conn, "synthetic") + before = self.source.capture_source(conn) + conn.execute(f"UPDATE messages SET {name}=2") + conn.commit() + self.assertEqual(conn.execute("SELECT id FROM messages").fetchone()[0], 2) + with self.assertRaises(self.source.RebuildSourceChanged): + before.check(conn) + + def test_shadowed_rowid_field_is_derived_and_other_alias_still_tracks_id(self) -> None: + conn, _ = self.database("content, ROWID TEXT") + self.seed(conn, 0.0) + before = self.source.capture_source(conn) + conn.execute("UPDATE messages SET rowid='derived'") + conn.commit() + before.check(conn) + conn.execute("UPDATE messages SET _rowid_=2") + conn.commit() + with self.assertRaises(self.source.RebuildSourceChanged): + before.check(conn) + + def test_generated_source_tracks_uncovered_dependency_updates(self) -> None: + for kind in ("VIRTUAL", "STORED"): + with self.subTest(kind=kind): + conn, _ = self.database(f"raw_value, content GENERATED ALWAYS AS (raw_value) {kind}") + conn.execute("INSERT INTO messages(id,raw_value) VALUES(1,?)", (1.0000000000000002,)) + conn.commit() + self.source.ensure_rebuild_tracking(conn) + before = self.source.capture_source(conn) + conn.execute("UPDATE messages SET raw_value=?", (1.0000000000000004,)) + conn.commit() + with self.assertRaises(self.source.RebuildSourceChanged): + before.check(conn) + + def test_unique_bridge_keeps_all_update_invalidation(self) -> None: + conn, _ = self.database("content, derived_key TEXT UNIQUE") + self.seed(conn, "synthetic") + before = self.source.capture_source(conn) + self.assertEqual(before.tracker, "rebuild-bridge-conservative-v1") + conn.execute("UPDATE messages SET derived_key='derived'") + conn.commit() + with self.assertRaises(self.source.RebuildSourceChanged): + before.check(conn) + + def test_precision_change_rollback_restores_token_and_preserves_caller_work(self) -> None: + conn, _ = self.database() + self.seed(conn, 1.0000000000000002) + conn.execute("CREATE TABLE synthetic_caller(value TEXT)") + conn.execute("INSERT INTO synthetic_caller VALUES('pending')") + before = self.source.capture_source(conn) + with self.assertRaisesRegex(RuntimeError, "synthetic rollback"): + with self.source.rebuild_transaction(conn, write=True): + conn.execute("UPDATE messages SET content=?", (1.0000000000000004,)) + with self.assertRaises(self.source.RebuildSourceChanged): + before.check(conn) + raise RuntimeError("synthetic rollback") + self.assertTrue(conn.in_transaction) + self.assertEqual(conn.execute("SELECT value FROM synthetic_caller").fetchone()[0], "pending") + self.assertEqual(conn.execute("SELECT content FROM messages").fetchone()[0], 1.0000000000000002) + before.check(conn) + self.assertEqual(before, self.source.capture_source(conn)) + + def test_failed_second_publication_keeps_committed_prefix_and_cursor(self) -> None: + conn, other = self.database("content TEXT, sender REAL, recipient TEXT, timestamp TEXT", shared=True) + conn.executemany("INSERT INTO messages VALUES(?,?,?,?,?)", [ + (1, "synthetic first", 1.25, "recipient", "2026-01-02"), + (2, "synthetic second", 1.0000000000000002, "recipient", "2026-01-02"), + ]) + conn.commit() + + def encode(model: object, texts: list, **kwargs: object) -> list[list[float]]: + result = self.encode(model, texts, **kwargs) + if len(self.calls) == 2: + other.execute("UPDATE messages SET sender=? WHERE id=2", (1.0000000000000004,)) + other.commit() + return result + + self.vector._encode_with_mps_fallback = encode + with self.assertRaises(self.source.RebuildSourceChanged): + self.vector.build_separation_vectors(conn, txn_batch=1) + conn.commit() + self.assertEqual(conn.execute("SELECT rowid,embedding FROM vec_messages_sep").fetchall(), + [(1, struct.pack("2f", 1.0, 0.0))]) + manifest = self.manifest(conn) + self.assertEqual((manifest.complete, manifest.cursor, manifest.consumed), (False, 1, 1)) + + def test_previous_trigger_repair_changes_epoch_and_invalidates_manifest(self) -> None: + conn, _ = self.database("content TEXT") + self.seed(conn, "synthetic") + self.vector.build_vectors(conn, txn_batch=1) + before = self.source.capture_source(conn) + manifest = self.manifest(conn, "vec_messages") + self.install_previous_update_trigger(conn) + with self.assertRaises(self.source.RebuildSourceChanged): + before.check(conn) + self.source.ensure_rebuild_tracking(conn) + after = self.source.capture_source(conn) + self.assertNotEqual(before.revision.epoch, after.revision.epoch) + with self.assertRaises(self.source.RebuildSourceChanged): + manifest.check(conn, self.source.manifest_key(("vec_messages",))) + self.calls.clear() + self.assertEqual(self.vector.build_vectors(conn, txn_batch=1), 1) + self.assertEqual(self.calls, [["synthetic"]]) + self.assertEqual(self.manifest(conn, "vec_messages").source.revision.epoch, after.revision.epoch) + + def test_denied_trigger_repair_preserves_target_and_caller_transaction(self) -> None: + for caller_owned in (False, True): + with self.subTest(caller_owned=caller_owned): + conn, _ = self.database("content TEXT") + self.seed(conn, "synthetic") + self.install_previous_update_trigger(conn) + conn.execute("INSERT INTO vec_messages(rowid,embedding) VALUES(99,?)", (b"synthetic-old",)) + conn.execute("CREATE TABLE synthetic_caller(value TEXT)") + conn.commit() + before_row = self.source._bridge_row(conn) + before_triggers = self.source._installed_triggers(conn, self.source._BRIDGE_TRIGGERS) + if caller_owned: + conn.execute("INSERT INTO synthetic_caller VALUES('pending')") + denied = [] + + def authorize(action: int, first: str, second: str, *_args: object) -> int: + if action == sqlite3.SQLITE_DROP_TRIGGER and first in self.source._BRIDGE_TRIGGERS: + denied.append(first) + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + + conn.set_authorizer(authorize) + self.calls.clear() + try: + with self.assertRaises(sqlite3.DatabaseError): + self.vector.build_vectors(conn, txn_batch=1) + finally: + conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.assertTrue(denied) + self.assertEqual(self.calls, []) + self.assertEqual(conn.in_transaction, caller_owned) + self.assertEqual(self.source._bridge_row(conn), before_row) + self.assertEqual(self.source._installed_triggers(conn, self.source._BRIDGE_TRIGGERS), before_triggers) + self.assertEqual(conn.execute("SELECT rowid,embedding FROM vec_messages").fetchall(), [(99, b"synthetic-old")]) + self.assertEqual(conn.execute("SELECT COUNT(*) FROM synthetic_caller").fetchone()[0], int(caller_owned)) + + def test_trusted_default_schema_stays_canonical(self) -> None: + conn = sqlite3.connect(":memory:") + self.addCleanup(conn.close) + conn.executescript(self.storage._SCHEMA_SQL) + self.storage._initialize_maintenance_tracking(conn) + self.source.ensure_rebuild_tracking(conn) + self.assertEqual(self.source.capture_source(conn).tracker, "canonical-v1") + self.assertIsNone(conn.execute("SELECT 1 FROM sqlite_master WHERE name=?", (self.source._BRIDGE_TABLE,)).fetchone()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-source-fts-exactness-753.py b/tests/test-source-fts-exactness-753.py new file mode 100644 index 00000000..860108c8 --- /dev/null +++ b/tests/test-source-fts-exactness-753.py @@ -0,0 +1,504 @@ +"""Actual storage SQL on synthetic SQLite only; no application/native imports.""" + +import ast +import builtins +import math +import sqlite3 +import sys +import types +import unittest +from pathlib import Path +from typing import NamedTuple +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] + + +def allow_sqlite_operation(*_args: object) -> int: + # Disabling authorizers with None requires Python 3.11 or newer. + return sqlite3.SQLITE_OK + + +def load_primitives() -> tuple[types.ModuleType, types.ModuleType]: + def safe_import(name: str, globals: dict | None = None, locals: dict | None = None, + fromlist: tuple[str, ...] = (), level: int = 0) -> object: + if name.split(".", 1)[0] not in sys.stdlib_module_names: + raise AssertionError("Non-stdlib import forbidden: " + name) + return builtins.__import__(name, globals, locals, fromlist, level) + + storage = types.ModuleType("synthetic_source_fts_storage") + storage.__dict__["__builtins__"] = dict(vars(builtins), __import__=safe_import) + path = ROOT / "truememory/storage.py" + exec(compile(ast.parse(path.read_text(encoding="utf-8")), str(path), "exec"), storage.__dict__) + maintenance = types.ModuleType("synthetic_source_fts_maintenance") + maintenance.__dict__.update(sqlite3=sqlite3, NamedTuple=NamedTuple) + names = {"MaintenanceUnavailableError", "SourceRevision", "read_source_revision"} + path = ROOT / "truememory/maintenance.py" + body = [node for node in ast.parse(path.read_text(encoding="utf-8")).body + if isinstance(node, (ast.FunctionDef, ast.ClassDef)) and node.name in names] + exec(compile(ast.Module(body=body, type_ignores=[]), str(path), "exec"), maintenance.__dict__) + return storage, maintenance + + +STORAGE, MAINTENANCE = load_primitives() +FIELDS = { + "id": "INTEGER PRIMARY KEY AUTOINCREMENT", + "content": "TEXT NOT NULL", + "sender": "TEXT DEFAULT ''", + "recipient": "TEXT DEFAULT ''", + "timestamp": "TEXT DEFAULT ''", + "category": "TEXT DEFAULT ''", + "modality": "TEXT DEFAULT ''", + "episode_id": "INTEGER", + "emotional_valence": "REAL DEFAULT 0.0", + "embedding_separation": "BLOB", + "directive": "INTEGER DEFAULT 0", + "metadata": "TEXT DEFAULT '{}'", +} + + +class SourceFTSFixture(unittest.TestCase): + def database(self, replacements: dict[str, str] | None = None, *, extra: str = "", + suffix: str = "", migrate: bool = True) -> sqlite3.Connection: + conn = sqlite3.connect(":memory:") + self.addCleanup(conn.close) + if replacements is not None or extra or suffix: + fields = dict(FIELDS) + fields.update(replacements or {}) + columns = ", ".join(name + " " + declaration for name, declaration in fields.items()) + conn.execute("CREATE TABLE messages (" + columns + (", " + extra if extra else "") + ")" + suffix) + conn.executescript(STORAGE._SCHEMA_SQL) + if migrate: + STORAGE._migrate_messages_fts_trigger(conn) + STORAGE._initialize_maintenance_tracking(conn) + return conn + + def token(self, conn: sqlite3.Connection) -> MAINTENANCE.SourceRevision: + return MAINTENANCE.read_source_revision(conn) + + def insert(self, conn: sqlite3.Connection, mid: int = 1, content: str = "synthetic original") -> None: + conn.execute("INSERT INTO messages(id,content) VALUES (?,?)", (mid, content)) + + def fts_rows(self, conn: sqlite3.Connection) -> list[tuple]: + return conn.execute( + "SELECT rowid,content,sender,recipient,category,modality FROM messages_fts ORDER BY rowid" + ).fetchall() + + def assert_fts_matches(self, conn: sqlite3.Connection) -> None: + self.assertEqual(self.fts_rows(conn), conn.execute( + "SELECT id,content,sender,recipient,category,modality FROM messages ORDER BY id" + ).fetchall()) + + +class TestSourceComparisons(SourceFTSFixture): + def test_declared_collations_do_not_hide_source_or_fts_edits(self) -> None: + for collation, before, after in (("NOCASE", "mixed", "MIXED"), ("RTRIM", "mixed", "mixed ")): + for field in STORAGE._MAINTENANCE_SOURCE_FIELDS[1:]: + with self.subTest(collation=collation, field=field): + conn = self.database({field: "TEXT COLLATE " + collation}) + self.insert(conn) + conn.execute(f"UPDATE messages SET {field}=?", (before,)) + old = self.token(conn) + conn.execute(f"UPDATE messages SET {field}=?", (after,)) + new = self.token(conn) + self.assertEqual(new.revision, old.revision + 1) + self.assertFalse(new.is_append_only_since(old)) + self.assert_fts_matches(conn) + + def test_adjacent_reals_and_storage_types_are_distinct(self) -> None: + for field in ("content", "metadata"): + with self.subTest(field=field): + conn = self.database({field: ""}) + self.insert(conn) + previous = None + for value in (1, 1.0, math.nextafter(1.0, 2), b"1", "1", None): + conn.execute(f"UPDATE messages SET {field}=?", (value,)) + current = self.token(conn) + if previous is not None: + self.assertEqual(current.revision, previous.revision + 1) + self.assertFalse(current.is_append_only_since(previous)) + self.assert_fts_matches(conn) + previous = current + + def test_signed_zero_is_conservative_but_derived_only_is_clean(self) -> None: + for field in ("content", "metadata"): + with self.subTest(field=field): + conn = self.database({field: ""}) + self.insert(conn) + conn.execute(f"UPDATE messages SET {field}=?", (-0.0,)) + conn.commit() + self.assertEqual(math.copysign(1, conn.execute(f"SELECT {field} FROM messages").fetchone()[0]), -1) + old = self.token(conn) + changes = conn.total_changes + conn.execute("UPDATE messages SET episode_id=4, emotional_valence=0.75") + self.assertEqual(self.token(conn), old) + self.assertEqual(conn.total_changes - changes, 1) + for value in (0.0, 0.0, -0.0): + old = self.token(conn) + conn.execute(f"UPDATE messages SET {field}=?", (value,)) + self.assertFalse(self.token(conn).is_append_only_since(old)) + self.assert_fts_matches(conn) + + def test_generated_source_dependencies_are_observed(self) -> None: + for field in ("content", "metadata"): + for kind in ("VIRTUAL", "STORED"): + with self.subTest(field=field, kind=kind): + conn = self.database({field: f"GENERATED ALWAYS AS (seed) {kind}"}, extra="seed") + if field == "content": + conn.execute("INSERT INTO messages(id,seed) VALUES (1,'synthetic generated')") + else: + conn.execute("INSERT INTO messages(id,content,seed) VALUES (1,'synthetic fixed','synthetic generated')") + old = self.token(conn) + conn.execute("UPDATE messages SET seed='synthetic changed'") + self.assertFalse(self.token(conn).is_append_only_since(old)) + self.assert_fts_matches(conn) + conn.execute("UPDATE messages SET seed=?", (-0.0,)) + old = self.token(conn) + conn.execute("UPDATE messages SET episode_id=9") + self.assertFalse(self.token(conn).is_append_only_since(old)) + self.assert_fts_matches(conn) + + def test_all_unshadowed_rowid_aliases_are_single_corrections(self) -> None: + conn = self.database() + self.insert(conn) + for mid, alias in enumerate(("id", "rowid", "_rowid_", "oid"), 20): + old = self.token(conn) + conn.execute(f"UPDATE messages SET {alias}=?", (mid,)) + new = self.token(conn) + self.assertEqual(new.revision, old.revision + 1) + self.assertEqual(new.correction_count, old.correction_count + 1) + self.assertEqual(new.max_seen_message_id, mid) + self.assert_fts_matches(conn) + + def test_shadowed_aliases_do_not_invalidate_real_zero_sources(self) -> None: + conn = self.database({"metadata": ""}, extra="rowid REAL, _rowid_ REAL, oid REAL") + self.insert(conn) + conn.execute("UPDATE messages SET metadata=?", (-0.0,)) + conn.commit() + old = self.token(conn) + changes = conn.total_changes + conn.execute("UPDATE messages SET rowid=10, _rowid_=20, oid=30") + self.assertEqual(self.token(conn), old) + self.assertEqual(conn.total_changes - changes, 1) + conn.execute("UPDATE messages SET id=2") + self.assertFalse(self.token(conn).is_append_only_since(old)) + self.assert_fts_matches(conn) + + def test_default_noops_nulls_append_and_rollback(self) -> None: + conn = self.database() + self.insert(conn) + conn.commit() + old = self.token(conn) + changes = conn.total_changes + conn.execute("UPDATE messages SET content=content,id=id,directive=directive,metadata=metadata") + conn.execute("UPDATE messages SET episode_id=9,emotional_valence=0.25,embedding_separation=X'0102'") + self.assertEqual(self.token(conn), old) + self.assertEqual(conn.total_changes - changes, 2) + conn.execute("UPDATE messages SET sender=NULL") + null = self.token(conn) + self.assertFalse(null.is_append_only_since(old)) + conn.execute("UPDATE messages SET sender=NULL") + self.assertEqual(self.token(conn), null) + conn.rollback() + self.assertEqual(self.token(conn), old) + self.insert(conn, 2) + self.assertTrue(self.token(conn).is_append_only_since(old)) + self.assert_fts_matches(conn) + + +class TestUniqueReplacement(SourceFTSFixture): + def test_unique_replacement_matrix_and_trigger_order(self) -> None: + indexes = ( + "CREATE UNIQUE INDEX synthetic_unique ON messages(unique_key)", + "CREATE UNIQUE INDEX synthetic_unique ON messages(lower(unique_key))", + "CREATE UNIQUE INDEX synthetic_unique ON messages(unique_key) WHERE unique_key IS NOT NULL", + "CREATE UNIQUE INDEX synthetic_unique ON messages(unique_key,pair_key)", + ) + for index in indexes: + for recursive in (0, 1): + for reverse in (False, True): + for operation in ("insert", "update", "move"): + with self.subTest(index=index, recursive=recursive, reverse=reverse, operation=operation): + conn = self.database(extra="unique_key TEXT, pair_key TEXT") + conn.execute(index) + conn.execute(f"PRAGMA recursive_triggers={recursive}") + definitions = {**STORAGE._messages_fts_trigger_definitions(conn), + **STORAGE._maintenance_trigger_definitions(conn)} + for name in definitions: + conn.execute(f"DROP TRIGGER {name}") + ordered = list(definitions.values()) + for definition in (reversed(ordered) if reverse else ordered): + conn.execute(definition) + conn.executemany( + "INSERT INTO messages(id,content,unique_key,pair_key) VALUES (?,?,?,'shared')", + ((1, "synthetic victim", "A"), (2, "synthetic survivor", "B")), + ) + old = self.token(conn) + key = "a" if "lower(" in index else "A" + if operation == "insert": + conn.execute("INSERT OR REPLACE INTO messages(id,content,unique_key,pair_key) " + "VALUES (3,'synthetic replacement',?,'shared')", (key,)) + elif operation == "update": + conn.execute("UPDATE OR REPLACE messages SET unique_key=? WHERE id=2", (key,)) + else: + conn.execute("UPDATE OR REPLACE messages SET id=3,unique_key=? WHERE id=2", (key,)) + self.assertFalse(self.token(conn).is_append_only_since(old)) + self.assert_fts_matches(conn) + self.assertIsNone(conn.execute("SELECT rowid FROM messages_fts WHERE rowid=1").fetchone()) + + def test_conservative_unique_updates_and_appends_without_victims(self) -> None: + conn = self.database() + self.insert(conn) + old = self.token(conn) + conn.execute("CREATE UNIQUE INDEX synthetic_unique ON messages(episode_id)") + self.insert(conn, 2) + self.assertFalse(self.token(conn).is_append_only_since(old)) + old = self.token(conn) + conn.execute("UPDATE messages SET episode_id=7 WHERE id=2") + new = self.token(conn) + self.assertEqual(new.revision, old.revision + 1) + self.assertEqual(new.correction_count, old.correction_count + 1) + self.assertFalse(new.is_append_only_since(old)) + self.assert_fts_matches(conn) + conn.execute("DROP INDEX synthetic_unique") + old = self.token(conn) + self.insert(conn, 3) + self.assertTrue(self.token(conn).is_append_only_since(old)) + + def test_multiple_hidden_victims_and_same_id_replacement(self) -> None: + conn = self.database(extra="unique_a TEXT UNIQUE, unique_b TEXT UNIQUE") + conn.execute("PRAGMA recursive_triggers=0") + conn.executemany("INSERT INTO messages(id,content,unique_a,unique_b) VALUES (?,?,?,?)", + ((1, "synthetic first", "a", "x"), (2, "synthetic second", "b", "y"))) + old = self.token(conn) + conn.execute("INSERT OR REPLACE INTO messages(id,content,unique_a,unique_b) VALUES (3,'synthetic third','a','y')") + self.assertFalse(self.token(conn).is_append_only_since(old)) + self.assert_fts_matches(conn) + self.assertEqual(len(self.fts_rows(conn)), 1) + conn = self.database() + self.insert(conn) + old = self.token(conn) + conn.execute("INSERT OR REPLACE INTO messages(id,content) VALUES (1,'synthetic replaced')") + self.assertFalse(self.token(conn).is_append_only_since(old)) + self.assert_fts_matches(conn) + + +class TestMigrationAndContracts(SourceFTSFixture): + def install_legacy_update(self, conn: sqlite3.Connection) -> None: + conn.execute("DROP TRIGGER messages_au") + conn.execute("""CREATE TRIGGER messages_au AFTER UPDATE ON messages + WHEN old.content IS NOT new.content BEGIN + DELETE FROM messages_fts WHERE rowid=old.id; + INSERT INTO messages_fts(rowid,content,sender,recipient,category,modality) + VALUES(new.id,new.content,new.sender,new.recipient,new.category,new.modality); END""") + for name in ("messages_fts_cleanup_ai", "messages_fts_cleanup_au"): + conn.execute(f"DROP TRIGGER {name}") + + def test_fts_migration_repairs_stale_missing_and_orphan_content_once(self) -> None: + conn = self.database({"content": "TEXT COLLATE NOCASE"}) + self.insert(conn, 1, "synthetic before") + self.insert(conn, 2, "synthetic missing") + self.install_legacy_update(conn) + conn.execute("UPDATE messages SET content='SYNTHETIC BEFORE' WHERE id=1") + conn.execute("DELETE FROM messages_fts WHERE rowid=2") + conn.execute("INSERT INTO messages_fts(rowid,content) VALUES (99,'synthetic orphan')") + conn.commit() + self.assertNotEqual(self.fts_rows(conn), conn.execute( + "SELECT id,content,sender,recipient,category,modality FROM messages ORDER BY id").fetchall()) + STORAGE._migrate_messages_fts_trigger(conn) + self.assert_fts_matches(conn) + self.assertEqual(conn.execute("SELECT rowid FROM messages_fts WHERE messages_fts MATCH 'missing'").fetchall(), [(2,)]) + changes = conn.total_changes + schema = conn.execute("PRAGMA schema_version").fetchone()[0] + STORAGE._migrate_messages_fts_trigger(conn) + STORAGE._initialize_maintenance_tracking(conn) + self.assertEqual(conn.total_changes, changes) + self.assertEqual(conn.execute("PRAGMA schema_version").fetchone()[0], schema) + + def test_tracking_definition_migration_rotates_epoch_once(self) -> None: + conn = self.database() + self.insert(conn) + conn.commit() + old = self.token(conn) + conn.execute("DROP TRIGGER messages_maintenance_unique_au") + STORAGE._initialize_maintenance_tracking(conn) + new = self.token(conn) + self.assertNotEqual(old.epoch, new.epoch) + self.assertFalse(new.is_append_only_since(old)) + changes = conn.total_changes + STORAGE._initialize_maintenance_tracking(conn) + self.assertEqual(self.token(conn), new) + self.assertEqual(conn.total_changes, changes) + + def test_caller_rollback_restores_both_migrations(self) -> None: + conn = self.database({"content": "TEXT COLLATE NOCASE"}) + self.insert(conn) + self.install_legacy_update(conn) + conn.execute("DROP TRIGGER messages_maintenance_unique_au") + conn.commit() + old = self.token(conn) + old_sql = conn.execute("SELECT name,sql FROM sqlite_master WHERE type='trigger' ORDER BY name").fetchall() + old_fts = self.fts_rows(conn) + conn.execute("BEGIN") + conn.execute("UPDATE messages SET content='SYNTHETIC ORIGINAL'") + STORAGE._migrate_messages_fts_trigger(conn) + STORAGE._initialize_maintenance_tracking(conn) + self.assertTrue(conn.in_transaction) + self.assertNotEqual(self.token(conn).epoch, old.epoch) + self.assert_fts_matches(conn) + conn.rollback() + self.assertEqual(self.token(conn), old) + self.assertEqual(self.fts_rows(conn), old_fts) + self.assertEqual(conn.execute("SELECT name,sql FROM sqlite_master WHERE type='trigger' ORDER BY name").fetchall(), old_sql) + + def test_failed_fts_refill_restores_definitions_and_caller_writes(self) -> None: + conn = self.database() + self.insert(conn) + self.install_legacy_update(conn) + conn.commit() + old_sql = conn.execute("SELECT name,sql FROM sqlite_master WHERE type='trigger' ORDER BY name").fetchall() + old_fts = self.fts_rows(conn) + conn.execute("BEGIN") + conn.execute("UPDATE messages SET episode_id=7") + + def deny_refill(action: int, name: str | None, column: str | None, + database: str | None, trigger: str | None) -> int: + if action == sqlite3.SQLITE_INSERT and name == "messages_fts" and trigger is None: + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + + conn.set_authorizer(deny_refill) + try: + with self.assertRaises(sqlite3.DatabaseError): + STORAGE._migrate_messages_fts_trigger(conn) + finally: + conn.set_authorizer(allow_sqlite_operation) + self.assertTrue(conn.in_transaction) + self.assertEqual(conn.execute("SELECT episode_id FROM messages").fetchone()[0], 7) + self.assertEqual(self.fts_rows(conn), old_fts) + self.assertEqual(conn.execute("SELECT name,sql FROM sqlite_master WHERE type='trigger' ORDER BY name").fetchall(), old_sql) + + def test_unsupported_ids_remain_readable_but_cannot_issue_source_tokens(self) -> None: + for declaration, suffix in (("INTEGER PRIMARY KEY DESC", ""), + ("INTEGER PRIMARY KEY", " WITHOUT ROWID"), + ("TEXT PRIMARY KEY", "")): + with self.subTest(declaration=declaration, suffix=suffix): + conn = self.database({"id": declaration}, suffix=suffix, migrate=False) + with patch.object(STORAGE.sqlite3, "connect", return_value=conn), self.assertLogs(STORAGE.log, level="WARNING"): + self.assertIs(STORAGE.create_db(":memory:"), conn) + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], 0) + with self.assertRaises(MAINTENANCE.MaintenanceUnavailableError): + self.token(conn) + self.assertEqual(conn.execute("SELECT tracking_ready FROM maintenance_source_state").fetchone()[0], 0) + + def test_non_rowid_integer_primary_keys_accept_fts_incompatible_ids(self) -> None: + for declaration, suffix in (("INTEGER PRIMARY KEY DESC", ""), ("INTEGER PRIMARY KEY", " WITHOUT ROWID")): + with self.subTest(declaration=declaration, suffix=suffix): + conn = sqlite3.connect(":memory:") + self.addCleanup(conn.close) + conn.execute(f"CREATE TABLE messages(id {declaration},content TEXT){suffix}") + conn.execute("CREATE VIRTUAL TABLE messages_fts USING fts5(content)") + conn.execute("INSERT INTO messages VALUES (1.25,'synthetic real ID')") + self.assertEqual(conn.execute("SELECT typeof(id) FROM messages").fetchone()[0], "real") + with self.assertRaises(sqlite3.IntegrityError): + conn.execute("INSERT INTO messages_fts(rowid,content) VALUES (1.25,'synthetic real ID')") + if not suffix: + conn.execute("INSERT INTO messages VALUES (NULL,'synthetic null ID')") + conn.execute("INSERT INTO messages_fts(rowid,content) VALUES (NULL,'synthetic null ID')") + self.assertEqual(conn.execute("SELECT count(*) FROM messages_fts").fetchone()[0], 1) + self.assertEqual(conn.execute( + "SELECT count(*) FROM messages_fts f JOIN messages m ON m.id=f.rowid" + ).fetchone()[0], 0) + + def test_incomplete_source_schema_keeps_fts_usable(self) -> None: + conn = sqlite3.connect(":memory:") + self.addCleanup(conn.close) + conn.execute("CREATE TABLE messages (" + ", ".join( + name + " " + declaration for name, declaration in FIELDS.items() if name != "directive" + ) + ")") + conn.executescript(STORAGE._SCHEMA_SQL) + STORAGE._migrate_messages_fts_trigger(conn) + with self.assertLogs(STORAGE.log, level="WARNING"): + STORAGE._initialize_maintenance_tracking(conn) + with self.assertRaises(MAINTENANCE.MaintenanceUnavailableError): + self.token(conn) + self.insert(conn) + self.assert_fts_matches(conn) + + def test_incomplete_fts_schema_remains_readable_without_refill(self) -> None: + conn = sqlite3.connect(":memory:") + self.addCleanup(conn.close) + conn.execute("CREATE TABLE messages (" + ", ".join( + name + " " + declaration for name, declaration in FIELDS.items() if name != "recipient" + ) + ")") + with patch.object(STORAGE.sqlite3, "connect", return_value=conn), \ + patch.object(STORAGE, "_migrate_messages_schema", return_value=None), \ + self.assertLogs(STORAGE.log, level="WARNING"): + self.assertIs(STORAGE.create_db(":memory:"), conn) + self.assertEqual(conn.execute("SELECT count(*) FROM messages").fetchone()[0], 0) + with self.assertRaises(MAINTENANCE.MaintenanceUnavailableError): + self.token(conn) + + def test_default_definition_interface_still_matches_installed_triggers(self) -> None: + conn = self.database() + expected = STORAGE._maintenance_trigger_definitions() + self.assertEqual(expected, STORAGE._maintenance_trigger_definitions(conn)) + for name, sql in expected.items(): + installed = conn.execute("SELECT sql FROM sqlite_master WHERE name=?", (name,)).fetchone()[0] + self.assertEqual(" ".join(installed.split()), " ".join(sql.split())) + + def test_repeated_initialization_does_not_read_source_or_fts_contents(self) -> None: + conn = self.database() + self.insert(conn) + conn.commit() + + def deny_contents(action: int, name: str | None, column: str | None, + database: str | None, trigger: str | None) -> int: + if action == sqlite3.SQLITE_READ and name in ("messages", "messages_fts", "messages_fts_content"): + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + + conn.set_authorizer(deny_contents) + try: + STORAGE._migrate_messages_fts_trigger(conn) + STORAGE._initialize_maintenance_tracking(conn) + finally: + conn.set_authorizer(allow_sqlite_operation) + self.assertEqual(conn.execute("SELECT content FROM messages").fetchall(), [("synthetic original",)]) + + +class TestDefaultCost(SourceFTSFixture): + def statement_steps(self, conn: sqlite3.Connection, sql: str) -> int: + steps = 0 + + def step() -> int: + nonlocal steps + steps += 1 + return 0 + + conn.set_progress_handler(step, 1) + try: + conn.execute(sql).fetchall() + finally: + conn.set_progress_handler(None, 0) + return steps + + def test_default_append_derived_and_revision_read_do_not_scan_messages(self) -> None: + samples = [] + for count in (100, 10000): + conn = self.database() + conn.executemany("INSERT INTO messages(content) VALUES ('synthetic repeated source')", [()] * count) + conn.commit() + samples.append(tuple(self.statement_steps(conn, sql) for sql in ( + "INSERT INTO messages(content) VALUES ('synthetic appended source')", + "UPDATE messages SET episode_id=5 WHERE id=1", + "SELECT epoch,revision,insert_count,correction_count,max_seen_message_id,nonappend_revision,tracking_ready " + "FROM maintenance_source_state WHERE singleton=1", + ))) + for small, large in zip(*samples): + self.assertLessEqual(large, small * 3, samples) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-source-invalidation-748.py b/tests/test-source-invalidation-748.py new file mode 100644 index 00000000..83cd2969 --- /dev/null +++ b/tests/test-source-invalidation-748.py @@ -0,0 +1,704 @@ +"""A05 stage 1: real stdlib L0 code and disposable SQLite, no package startup. + +The two frozen references come from b97e586. Directive rows are removed only +from the reference input: all other formulas, ordering and returned values +must match exactly. No native dependencies, models or external data are read. +""" +from __future__ import annotations + +import ast +import random +import sqlite3 +import tempfile +import types +import unittest +import warnings +from pathlib import Path +from unittest.mock import patch + +SOURCE = Path(__file__).resolve().parents[1] / "truememory" + + +def _load_modules() -> tuple[types.ModuleType, types.ModuleType]: + style = types.ModuleType("a05_style") + exec(compile((SOURCE / "personality_style_vec.py").read_text(), "personality_style_vec.py", "exec"), style.__dict__) + tree = ast.parse((SOURCE / "personality.py").read_text()) + tree.body = [node for node in tree.body if not ( + isinstance(node, ast.ImportFrom) and (node.module or "").startswith("truememory.") + )] + personality = types.ModuleType("a05_personality") + for name in ("_l0_directive_filter", "_l0_source_schema", "_l0_transaction", "_l0_validate_source"): + if hasattr(style, name): + personality.__dict__[name] = getattr(style, name) + exec(compile(tree, "personality.py", "exec"), personality.__dict__) + return personality, style + + +SCHEMA = """ +CREATE TABLE messages ( + id INTEGER PRIMARY KEY, content TEXT, sender TEXT, recipient TEXT, + timestamp TEXT, category TEXT, modality TEXT, directive INTEGER +); +CREATE TABLE entity_profiles ( + entity TEXT PRIMARY KEY, message_count INTEGER, traits TEXT, + communication_style TEXT, topics TEXT, relationships TEXT, updated_at TEXT +); +CREATE TABLE entity_style_vectors ( + entity TEXT PRIMARY KEY, vector TEXT, message_count INTEGER, updated_at TEXT +); +CREATE TABLE sentinel (value TEXT); +""" + + +def _db(path: str = ":memory:", *, directives: bool = True) -> sqlite3.Connection: + conn = sqlite3.connect(path, timeout=0, cached_statements=0) + schema = SCHEMA if directives else SCHEMA.replace(", directive INTEGER", "") + conn.executescript(schema) + return conn + + +def _seed(conn: sqlite3.Connection) -> None: + conn.executemany( + "INSERT INTO messages VALUES (?, ?, ?, ?, ?, ?, ?, ?)", + [ + (9, "hey I write code and deploy the database", "Alice", "Bob", "2026-01-03", "work", "chat", 0), + (3, "hello I enjoy coffee every morning", "ALICE", "Bob", "2026-01-01", "routine", "chat", None), + (7, "I am worried about the server and customer pricing", "Bob", "Alice", "2026-01-03", "work", "chat", 0), + (11, "directive-only sentinel should never become a profile", "Directive", "Alice", "2026-01-01", "control", "chat", 1), + (12, "", "", "Bob", "2026-01-02", "", "chat", 0), + ], + ) + conn.commit() + + +def _allow(*_args: object) -> int: + return sqlite3.SQLITE_OK + + +LEGACY_PROFILES = '''def _get_all_messages(conn: sqlite3.Connection) -> list[dict]: + """Fetch all messages ordered by timestamp.""" + rows = conn.execute( + "SELECT id, content, sender, recipient, timestamp, category, modality " + "FROM messages ORDER BY timestamp" + ).fetchall() + return [ + { + "id": r[0], "content": r[1], "sender": r[2], + "recipient": r[3], "timestamp": r[4], + "category": r[5], "modality": r[6], + } + for r in rows + ] + +def build_entity_profiles(conn: sqlite3.Connection) -> dict: + """ + Analyze all messages and build personality profiles for key entities. + + For each entity (sender), extracts: + + - **message_count**: total messages sent. + - **topics**: frequent themes (startup, health, food, etc.). + - **communication_style**: average message length, emoji usage, + formality level, and typical greeting. + - **relationships**: who they message most and approximate topic focus + per recipient. + - **traits**: personality descriptors inferred from content analysis. + + Results are stored in the ``entity_profiles`` table and also returned + as a dict keyed by entity name. + + Args: + conn: Open database connection (from :func:`truememory.storage.create_db`). + + Returns: + ``{entity: profile_dict}`` for every sender who has at least one + message in the database. + """ + all_msgs = _get_all_messages(conn) + + # Group messages by sender (normalized to lowercase for case-insensitive matching) + by_sender: dict[str, list[dict]] = defaultdict(list) + for msg in all_msgs: + if msg["sender"]: + by_sender[msg["sender"].lower()].append(msg) + + profiles: dict[str, dict] = {} + + for sender, messages in by_sender.items(): + # Communication style + lengths = [len(m["content"]) for m in messages] + avg_length = sum(lengths) / len(lengths) if lengths else 0.0 + uses_emoji = any(_detect_emoji(m["content"]) for m in messages) + formality = _assess_formality(messages) + greeting = _find_typical_greeting(messages) + + comm_style = { + "avg_length": round(avg_length, 1), + "uses_emoji": uses_emoji, + "formality": formality, + "typical_greeting": greeting, + } + + # Relationships: who they talk to and rough topic per recipient + recipient_counts: dict[str, int] = defaultdict(int) + recipient_topics: dict[str, list[str]] = defaultdict(list) + for msg in messages: + recip = msg["recipient"] + if recip: + recipient_counts[recip] += 1 + # Quick topic tag for each message + lower = msg["content"].lower() + if any(w in lower for w in ("code", "deploy", "api", + "database", "bug", "server")): + recipient_topics[recip].append("technical") + elif any(w in lower for w in ("worried", "anxious", + "scared", "stress")): + recipient_topics[recip].append("emotional") + elif any(w in lower for w in ("dinner", "drinks", + "hang out", "watch")): + recipient_topics[recip].append("social") + elif any(w in lower for w in ("revenue", "investor", + "pricing", "customer")): + recipient_topics[recip].append("business") + + relationships = {} + for recip, count in sorted(recipient_counts.items(), + key=lambda x: x[1], reverse=True): + topic_freq: dict[str, int] = defaultdict(int) + for t in recipient_topics.get(recip, []): + topic_freq[t] += 1 + top_topic = max(topic_freq, key=topic_freq.get) if topic_freq else "general" + relationships[recip] = { + "message_count": count, + "primary_topic": top_topic, + } + + with _warnings_ctx(): + topics = _extract_topics(messages) + traits = _extract_traits(messages) + + profile = { + "message_count": len(messages), + "topics": topics, + "communication_style": comm_style, + "relationships": relationships, + "traits": traits, + } + profiles[sender] = profile + + # Store in database + now = datetime.now(timezone.utc).isoformat() + conn.execute( + """INSERT OR REPLACE INTO entity_profiles + (entity, message_count, traits, communication_style, + topics, relationships, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?)""", + ( + sender, + len(messages), + json.dumps(traits), + json.dumps(comm_style), + json.dumps(topics), + json.dumps(relationships), + now, + ), + ) + + conn.commit() + return profiles''' + +LEGACY_STYLE = '''def build_entity_style_vectors(conn: sqlite3.Connection) -> dict[str, list[float]]: + """Batch-build style vectors for every entity (sender) in the database. + + For each sender: + 1. Compute ``compute_style_vector(msg.content)`` for each message. + 2. Mean-pool all per-message vectors via ``mean_pool_vectors``. + 3. Store the result in the ``entity_style_vectors`` table. + + Args: + conn: Open database connection (from :func:`truememory.storage.create_db`). + + Returns: + ``{entity: vector}`` for every sender. + """ + conn.execute( + """CREATE TABLE IF NOT EXISTS entity_style_vectors ( + entity TEXT PRIMARY KEY, + vector TEXT, + message_count INTEGER DEFAULT 0, + updated_at TEXT + )""" + ) + + rows = conn.execute( + "SELECT sender, content FROM messages WHERE sender != '' ORDER BY sender, timestamp" + ).fetchall() + + from collections import defaultdict + by_sender: dict[str, list[str]] = defaultdict(list) + for sender, content in rows: + by_sender[sender.lower()].append(content) + + result: dict[str, list[float]] = {} + now = datetime.now(timezone.utc).isoformat() + + for sender, contents in by_sender.items(): + vecs = [compute_style_vector(c) for c in contents] + mean_vec = mean_pool_vectors(vecs) + result[sender] = mean_vec + + conn.execute( + """INSERT OR REPLACE INTO entity_style_vectors + (entity, vector, message_count, updated_at) + VALUES (?, ?, ?, ?)""", + (sender, json.dumps(mean_vec), len(contents), now), + ) + + conn.commit() + return result''' + +class TestSourceInvalidation748(unittest.TestCase): + def setUp(self) -> None: + self.personality, self.style = _load_modules() + self.temp = tempfile.TemporaryDirectory(prefix="a05-748-") + self.addCleanup(self.temp.cleanup) + self.connections: list[sqlite3.Connection] = [] + self.addCleanup(self._close) + + def _close(self) -> None: + for conn in self.connections: + conn.set_authorizer(_allow) + conn.close() + + def db(self, *, file: bool = False, directives: bool = True) -> sqlite3.Connection: + path = str(Path(self.temp.name) / f"source-{len(self.connections)}.db") if file else ":memory:" + conn = _db(path, directives=directives) + self.connections.append(conn) + if directives: + _seed(conn) + return conn + + def builders(self) -> list[tuple[types.ModuleType, str, str]]: + return [ + (self.personality, "build_entity_profiles", "entity_profiles"), + (self.style, "build_entity_style_vectors", "entity_style_vectors"), + ] + + def generation(self, conn: sqlite3.Connection, table: str, *, stamps: bool = True) -> list[tuple]: + columns = ( + "entity,message_count,traits,communication_style,topics,relationships" + if table == "entity_profiles" else "entity,vector,message_count" + ) + if stamps: + columns += ",updated_at" + return [tuple(row) for row in conn.execute(f"SELECT {columns} FROM {table} ORDER BY entity")] + + def reference(self, conn: sqlite3.Connection, kind: str) -> tuple[dict, list[tuple]]: + ref = _db() + try: + rows = conn.execute("SELECT * FROM messages WHERE directive = 0 OR directive IS NULL").fetchall() + ref.executemany("INSERT INTO messages VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [tuple(r) for r in rows]) + ref.commit() + module = self.personality if kind == "entity_profiles" else self.style + namespace = dict(module.__dict__) + exec(LEGACY_PROFILES if kind == "entity_profiles" else LEGACY_STYLE, namespace) + name = "build_entity_profiles" if kind == "entity_profiles" else "build_entity_style_vectors" + result = namespace[name](ref) + return result, self.generation(ref, kind, stamps=False) + finally: + ref.close() + + def assert_reference(self, conn: sqlite3.Connection, module: types.ModuleType, name: str, table: str) -> None: + expected, stored = self.reference(conn, table) + self.assertEqual(getattr(module, name)(conn), expected) + self.assertEqual(self.generation(conn, table, stamps=False), stored) + + def test_unchanged_formulas_and_tied_order_match_frozen_reference(self) -> None: + rng = random.Random(748) + phrases = ["hello friend coffee morning", "please deploy the code", "worried about pricing", "", "a", "gym every day"] + for size in (0, 1, 5, 51, 103): + with self.subTest(size=size): + conn = self.db() + conn.execute("DELETE FROM messages") + for index in range(size): + conn.execute("INSERT INTO messages VALUES (?, ?, ?, ?, ?, ?, ?, ?)", ( + index * 3 - 5, rng.choice(phrases), rng.choice(["Alice", "ALICE", "Bob", "Änne", ""]), + rng.choice(["Alice", "Bob", "BOB", ""]), rng.choice(["", "2026-01-01", "2026-01-03"]), + "fixture", "chat", rng.choice([0, 0, 1, None]), + )) + conn.commit() + for module, name, table in self.builders(): + self.assert_reference(conn, module, name, table) + self.assertFalse(conn.in_transaction) + + def test_edits_deletes_replacement_and_empty_match_fresh_reference(self) -> None: + changes = [ + "UPDATE messages SET content='hello new ordinary content', recipient='Carol' WHERE id=9", + "UPDATE messages SET sender='Carol', timestamp='2025-12-01' WHERE id=3", + "DELETE FROM messages WHERE sender='Bob'", + "INSERT OR REPLACE INTO messages VALUES (9,'replaced content','Dana','Carol','2025-01-01','','chat',0)", + "DELETE FROM messages", + ] + for module, name, table in self.builders(): + conn = self.db() + getattr(module, name)(conn) + for sql in changes: + with self.subTest(builder=name, change=sql.split()[0]): + conn.execute(sql) + conn.commit() + self.assert_reference(conn, module, name, table) + self.assertEqual(self.generation(conn, table), []) + + def test_directive_index_does_not_reorder_tied_ordinary_rows(self) -> None: + conn = self.db() + conn.execute("DELETE FROM messages") + conn.executescript( + "CREATE INDEX idx_messages_sender ON messages(sender);" + "CREATE INDEX idx_messages_timestamp ON messages(timestamp);" + "CREATE INDEX idx_messages_directive ON messages(directive);" + ) + rows = [ + (1, "hello one code", "Alice", "Bob", "2026-01-01", "", "", None), + (2, "hey two coffee", "Alice", "Carol", "2026-01-01", "", "", 0), + ] + [(100 + i, "ignored", "Control", "", "2026-01-01", "", "", 1) for i in range(100)] + conn.executemany("INSERT INTO messages VALUES (?, ?, ?, ?, ?, ?, ?, ?)", rows) + conn.commit() + for module, name, table in self.builders(): + self.assert_reference(conn, module, name, table) + + def test_directive_toggle_and_directive_only_generation(self) -> None: + for module, name, table in self.builders(): + conn = self.db() + self.assert_reference(conn, module, name, table) + self.assertNotIn("directive", dict((r[0], r) for r in self.generation(conn, table))) + for flag in (1, 0, None): + conn.execute("UPDATE messages SET directive=? WHERE sender != ''", (flag,)) + conn.commit() + self.assert_reference(conn, module, name, table) + if flag == 1: + self.assertEqual(self.generation(conn, table), []) + + def test_standalone_schema_without_maintenance_or_directive(self) -> None: + for module, name, table in self.builders(): + conn = self.db(directives=False) + conn.execute("INSERT INTO messages VALUES (1,'hello code','Alice','Bob','2026-01-01','','chat')") + conn.commit() + result = getattr(module, name)(conn) + self.assertEqual(set(result), {"alice"}) + conn.execute("DELETE FROM messages") + conn.commit() + self.assertEqual(getattr(module, name)(conn), {}) + self.assertEqual(self.generation(conn, table), []) + + def test_row_factory_and_autocommit_compatibility(self) -> None: + for module, name, table in self.builders(): + conn = self.db() + conn.row_factory = sqlite3.Row + conn.isolation_level = None + self.assert_reference(conn, module, name, table) + self.assertIsNone(conn.isolation_level) + self.assertFalse(conn.in_transaction) + + def test_style_builder_accepts_its_original_minimal_source_schema(self) -> None: + conn = sqlite3.connect(":memory:") + self.connections.append(conn) + conn.execute("CREATE TABLE messages (sender TEXT, content TEXT, timestamp TEXT)") + conn.execute("INSERT INTO messages VALUES ('Alice','hello ordinary content','2026-01-01')") + conn.commit() + result = self.style.build_entity_style_vectors(conn) + expected = self.style.mean_pool_vectors([ + self.style.compute_style_vector("hello ordinary content"), + ]) + self.assertEqual(result, {"alice": expected}) + self.assertEqual(conn.execute("SELECT message_count FROM entity_style_vectors").fetchone()[0], 1) + + def test_failed_source_read_does_not_leave_owned_transaction(self) -> None: + for module, name, _table in self.builders(): + conn = sqlite3.connect(":memory:") + self.connections.append(conn) + with self.assertRaises(sqlite3.OperationalError): + getattr(module, name)(conn) + self.assertFalse(conn.in_transaction) + + def test_style_table_creation_is_after_compute_and_atomic(self) -> None: + conn = self.db() + conn.execute("DROP TABLE entity_style_vectors") + original = self.style.compute_style_vector + observed = [] + def compute(text: str) -> list[float]: + observed.append(conn.execute("SELECT count(*) FROM sqlite_master WHERE name='entity_style_vectors'").fetchone()[0]) + self.assertFalse(conn.in_transaction) + return original(text) + with patch.object(self.style, "compute_style_vector", compute): + self.style.build_entity_style_vectors(conn) + self.assertTrue(observed) + self.assertEqual(set(observed), {0}) + self.assertTrue(self.generation(conn, "entity_style_vectors")) + + def test_publication_preserves_caller_transaction_and_rollback(self) -> None: + for module, name, table in self.builders(): + conn = self.db() + getattr(module, name)(conn) + before = self.generation(conn, table) + conn.execute("INSERT INTO sentinel VALUES ('uncommitted')") + conn.execute("UPDATE messages SET sender='Carol' WHERE sender='Bob'") + result = getattr(module, name)(conn) + self.assertIn("carol", result) + self.assertTrue(conn.in_transaction) + conn.rollback() + self.assertEqual(self.generation(conn, table), before) + self.assertEqual(conn.execute("SELECT count(*) FROM sentinel").fetchone()[0], 0) + self.assertEqual(conn.execute("SELECT sender FROM messages WHERE id=7").fetchone()[0], "Bob") + + def test_publication_retains_caller_writes_until_explicit_commit(self) -> None: + for module, name, table in self.builders(): + conn = self.db(file=True) + path = conn.execute("PRAGMA database_list").fetchone()[2] + other = sqlite3.connect(path, timeout=0) + self.connections.append(other) + conn.execute("INSERT INTO sentinel VALUES ('uncommitted')") + getattr(module, name)(conn) + self.assertEqual(other.execute("SELECT count(*) FROM sentinel").fetchone()[0], 0) + self.assertEqual(other.execute(f"SELECT count(*) FROM {table}").fetchone()[0], 0) + conn.commit() + self.assertEqual(other.execute("SELECT count(*) FROM sentinel").fetchone()[0], 1) + self.assertGreater(other.execute(f"SELECT count(*) FROM {table}").fetchone()[0], 0) + + def test_late_compute_failure_and_cancellation_leave_previous_generation(self) -> None: + for module, name, table in self.builders(): + for error_type in (RuntimeError, KeyboardInterrupt): + with self.subTest(builder=name, error=error_type.__name__): + conn = self.db() + getattr(module, name)(conn) + before = self.generation(conn, table) + conn.execute("UPDATE messages SET content=content || ' corrected'") + conn.commit() + conn.execute("INSERT INTO sentinel VALUES ('pending')") + function = "_extract_topics" if table == "entity_profiles" else "compute_style_vector" + original = getattr(module, function) + calls = 0 + error = error_type("synthetic computation failure") + def compute(*args: object, **kwargs: object) -> object: + nonlocal calls + calls += 1 + if calls == 3: + raise error + return original(*args, **kwargs) + # Profile fixture has two ordinary senders; add a third to fail late. + conn.execute("INSERT INTO messages VALUES (20,'new content','Carol','','2026-01-04','','chat',0)") + with patch.object(module, function, compute): + with self.assertRaises(error_type) as raised: + getattr(module, name)(conn) + self.assertIs(raised.exception, error) + self.assertGreaterEqual(calls, 3) + conn.commit() + self.assertEqual(self.generation(conn, table), before) + + def test_partial_insert_failure_rolls_back_replacement_not_caller(self) -> None: + for module, name, table in self.builders(): + for caller in (False, True): + with self.subTest(builder=name, caller=caller): + conn = self.db() + getattr(module, name)(conn) + before = self.generation(conn, table) + conn.execute("UPDATE messages SET content=content || ' corrected'") + conn.commit() + conn.execute(f"CREATE TRIGGER fail_l0 BEFORE INSERT ON {table} WHEN new.entity='bob' BEGIN SELECT RAISE(ABORT,'synthetic write failure'); END") + if caller: + conn.execute("INSERT INTO sentinel VALUES ('pending')") + with self.assertRaisesRegex(sqlite3.IntegrityError, "synthetic write failure"): + getattr(module, name)(conn) + self.assertEqual(conn.in_transaction, caller) + conn.commit() + self.assertEqual(self.generation(conn, table), before) + self.assertEqual(conn.execute("SELECT count(*) FROM sentinel").fetchone()[0], int(caller)) + + def test_publication_commit_or_release_failure_rolls_back(self) -> None: + for module, name, table in self.builders(): + for caller in (False, True): + with self.subTest(builder=name, caller=caller): + conn = self.db() + getattr(module, name)(conn) + before = self.generation(conn, table) + conn.execute("UPDATE messages SET content=content || ' corrected'") + conn.commit() + if caller: + conn.execute("INSERT INTO sentinel VALUES ('pending')") + writing = False + denied = False + def authorize(action: int, one: str | None, two: str | None, *_rest: object) -> int: + nonlocal writing, denied + if action == sqlite3.SQLITE_DELETE and one == table: + writing = True + commit = action == sqlite3.SQLITE_TRANSACTION and one == "COMMIT" + release = action == sqlite3.SQLITE_SAVEPOINT and one == "RELEASE" + if writing and not denied and (release if caller else commit): + denied = True + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + conn.set_authorizer(authorize) + try: + with self.assertRaises(sqlite3.DatabaseError): + getattr(module, name)(conn) + finally: + conn.set_authorizer(_allow) + self.assertTrue(denied) + self.assertEqual(conn.in_transaction, caller) + conn.commit() + self.assertEqual(self.generation(conn, table), before) + self.assertEqual(conn.execute("SELECT count(*) FROM sentinel").fetchone()[0], int(caller)) + + def test_rollback_failure_never_releases_partial_publication(self) -> None: + for module, name, table in self.builders(): + conn = self.db() + getattr(module, name)(conn) + before = self.generation(conn, table) + conn.execute(f"CREATE TRIGGER fail_l0 BEFORE INSERT ON {table} WHEN new.entity='bob' BEGIN SELECT RAISE(ABORT,'synthetic write failure'); END") + conn.execute("INSERT INTO sentinel VALUES ('pending')") + blocked = False + releases_after_failure = [] + def authorize(action: int, one: str | None, two: str | None, *_rest: object) -> int: + nonlocal blocked + if action == sqlite3.SQLITE_SAVEPOINT and one == "ROLLBACK": + blocked = True + return sqlite3.SQLITE_DENY + if blocked and action == sqlite3.SQLITE_SAVEPOINT and one == "RELEASE": + releases_after_failure.append(two) + return sqlite3.SQLITE_OK + conn.set_authorizer(authorize) + try: + with self.assertRaises(sqlite3.DatabaseError): + getattr(module, name)(conn) + finally: + conn.set_authorizer(_allow) + self.assertTrue(blocked) + self.assertTrue(conn.in_transaction) + self.assertEqual(releases_after_failure, []) + conn.rollback() + self.assertEqual(self.generation(conn, table), before) + self.assertEqual(conn.execute("SELECT count(*) FROM sentinel").fetchone()[0], 0) + + def test_foreground_source_changes_commit_during_compute_and_fence_publish(self) -> None: + changes = [ + "UPDATE messages SET content='concurrent new text' WHERE id=9", + "UPDATE messages SET sender='Carol' WHERE id=9", + "UPDATE messages SET timestamp='2025-01-01' WHERE id=9", + "UPDATE messages SET directive=1 WHERE id=9", + "DELETE FROM messages WHERE id=9", + "INSERT INTO messages VALUES (2,'backdated ordinary text','Alice','Bob','2024-01-01','','chat',0)", + "DELETE FROM messages", + "ALTER TABLE messages ADD COLUMN extra TEXT", + ] + for module, name, table in self.builders(): + for sql in changes: + with self.subTest(builder=name, mutation=sql.split()[0]): + conn = self.db(file=True) + getattr(module, name)(conn) + before = self.generation(conn, table) + path = conn.execute("PRAGMA database_list").fetchone()[2] + other = sqlite3.connect(path, timeout=0) + self.connections.append(other) + function = "_extract_topics" if table == "entity_profiles" else "compute_style_vector" + original = getattr(module, function) + calls = 0 + def compute(*args: object, **kwargs: object) -> object: + nonlocal calls + calls += 1 + self.assertFalse(conn.in_transaction) + if calls == 1: + other.execute(sql) + other.commit() + return original(*args, **kwargs) + with patch.object(module, function, compute): + with self.assertRaisesRegex(sqlite3.OperationalError, "L0 source changed"): + getattr(module, name)(conn) + self.assertGreater(calls, 0) + self.assertFalse(conn.in_transaction) + conn.commit() + self.assertEqual(self.generation(conn, table), before) + + def test_stale_caller_wal_snapshot_cannot_publish_or_restart(self) -> None: + for module, name, table in self.builders(): + conn = self.db(file=True) + conn.execute("PRAGMA journal_mode=WAL") + getattr(module, name)(conn) + before = self.generation(conn, table) + path = conn.execute("PRAGMA database_list").fetchone()[2] + other = sqlite3.connect(path, timeout=0) + self.connections.append(other) + conn.execute("BEGIN") + function = "_extract_topics" if table == "entity_profiles" else "compute_style_vector" + original = getattr(module, function) + changed = False + def compute(*args: object, **kwargs: object) -> object: + nonlocal changed + self.assertTrue(conn.in_transaction) + if not changed: + other.execute("UPDATE messages SET content='concurrent corrected source' WHERE id=9") + other.commit() + changed = True + return original(*args, **kwargs) + with patch.object(module, function, compute): + with self.assertRaises(sqlite3.OperationalError): + getattr(module, name)(conn) + self.assertTrue(changed) + self.assertTrue(conn.in_transaction) + self.assertEqual(self.generation(conn, table), before) + self.assertNotEqual(conn.execute("SELECT content FROM messages WHERE id=9").fetchone()[0], "concurrent corrected source") + conn.rollback() + self.assertEqual(conn.execute("SELECT content FROM messages WHERE id=9").fetchone()[0], "concurrent corrected source") + self.assertEqual(self.generation(conn, table), before) + + def test_validation_holds_writer_ownership(self) -> None: + for module, name, table in self.builders(): + conn = self.db(file=True) + path = conn.execute("PRAGMA database_list").fetchone()[2] + other = sqlite3.connect(path, timeout=0) + self.connections.append(other) + original = module._l0_validate_source + blocked = [] + def validate(*args: object, **kwargs: object) -> None: + original(*args, **kwargs) + try: + with self.assertRaises(sqlite3.OperationalError): + other.execute("UPDATE messages SET sender='Racing' WHERE id=9") + blocked.append(True) + finally: + other.rollback() + with patch.object(module, "_l0_validate_source", validate): + getattr(module, name)(conn) + self.assertEqual(blocked, [True]) + + def test_cancellation_inside_publication_preserves_caller_generation(self) -> None: + class CancelConnection(sqlite3.Connection): + cancel_table: str | None = None + + def executemany(self, sql: str, parameters: object) -> sqlite3.Cursor: + if self.cancel_table and f"INSERT INTO {self.cancel_table}" in sql: + rows = iter(parameters) + super().execute(sql, next(rows)) + raise KeyboardInterrupt("synthetic publication cancellation") + return super().executemany(sql, parameters) + + for module, name, table in self.builders(): + conn = sqlite3.connect(":memory:", factory=CancelConnection) + self.connections.append(conn) + conn.executescript(SCHEMA) + _seed(conn) + getattr(module, name)(conn) + before = self.generation(conn, table) + conn.execute("INSERT INTO sentinel VALUES ('pending')") + conn.execute("UPDATE messages SET content=content || ' changed'") + conn.cancel_table = table + with self.assertRaises(KeyboardInterrupt): + getattr(module, name)(conn) + self.assertTrue(conn.in_transaction) + conn.commit() + self.assertEqual(self.generation(conn, table), before) + self.assertEqual(conn.execute("SELECT count(*) FROM sentinel").fetchone()[0], 1) + + +if __name__ == "__main__": + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + unittest.main() diff --git a/tests/test-source-provenance-745.py b/tests/test-source-provenance-745.py new file mode 100644 index 00000000..27582df3 --- /dev/null +++ b/tests/test-source-provenance-745.py @@ -0,0 +1,395 @@ +"""Synthetic, inert actual-source controls for source provenance #745.""" + +from __future__ import annotations + +import ast +import contextlib +import datetime +import enum +import hashlib +import importlib.util +import json +import logging +import os +import sqlite3 +import sys +import types +import unittest +from dataclasses import asdict, dataclass, field +from pathlib import Path +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location("source_provenance_745_controls", ROOT / "truememory/ingest/provenance.py") +assert SPEC is not None and SPEC.loader is not None +PROVENANCE = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = PROVENANCE +SPEC.loader.exec_module(PROVENANCE) + + +def selected_source(relative: str, names: set[str], namespace: dict[str, object]) -> dict[str, object]: + filename = ROOT / relative + tree = ast.parse(filename.read_text(encoding="utf-8")) + selected = [ast.ImportFrom(module="__future__", names=[ast.alias(name="annotations")], level=0)] + selected.extend(node for node in tree.body if isinstance(node, (ast.FunctionDef, ast.ClassDef)) and node.name in names) + if {node.name for node in selected if isinstance(node, (ast.FunctionDef, ast.ClassDef))} != names: + raise AssertionError("actual-source control selection is incomplete") + exec(compile(ast.fix_missing_locations(ast.Module(body=selected, type_ignores=[])), str(filename), "exec"), namespace) + return namespace + + +EXTRACTOR = selected_source("truememory/ingest/extractor.py", {"ExtractedFact", "_chunk_transcript", "_dedupe_facts_by_content", "extract_facts_outcome"}, { + "dataclass": dataclass, "field": field, "_CHUNK_CHAR_BUDGET": 100, "_DEFAULT_MAX_CHUNKS": 20, + "log": logging.getLogger(__name__), "EXTRACTION_PROMPT": "{transcript}", "EXTRACTION_SYSTEM": "synthetic", + "_neutralize_delimiters": lambda value: value, "LLMError": RuntimeError, +}) +Fact = EXTRACTOR["ExtractedFact"] +STORAGE = selected_source("truememory/storage.py", {"_serialize_metadata", "_deserialize_metadata", "_row_to_dict", "insert_message", "update_message"}, {"json": json}) +TRANSCRIPT = selected_source("truememory/ingest/transcript.py", {"format_for_extraction"}, {"strip_truememory_blocks": lambda value: value}) +DEDUP = selected_source("truememory/ingest/dedup.py", {"DedupAction", "DedupDecision"}, {"Enum": enum.Enum, "dataclass": dataclass}) + + +class SyntheticEngine: + def __init__(self) -> None: + self.conn = sqlite3.connect(":memory:") + self.conn.execute("CREATE TABLE messages (id INTEGER PRIMARY KEY,content TEXT,sender TEXT,recipient TEXT,timestamp TEXT,category TEXT,modality TEXT,directive INTEGER,metadata TEXT)") + self.writes: list[dict[str, object]] = [] + + def add(self, **fields: object) -> dict[str, object]: + identity = STORAGE["insert_message"](self.conn, fields) + self.conn.commit() + self.writes.append(dict(fields)) + return self.get(identity) + + def get(self, identity: int, *, _source_snapshot: bool = False) -> dict[str, object] | None: + row = self.conn.execute("SELECT id,content,sender,recipient,timestamp,category,modality,directive,metadata FROM messages WHERE id=?", (identity,)).fetchone() + if row is None: + return None + result = STORAGE["_row_to_dict"](row) + if _source_snapshot: + result["_source_snapshot_sha256"] = hashlib.sha256( + json.dumps(list(row), ensure_ascii=True, allow_nan=False, separators=(",", ":")).encode("utf-8") + ).hexdigest() + return result + + def update(self, identity: int, *, _expected_source: dict[str, object] | None = None, + **fields: object) -> dict[str, object] | None: + if _expected_source is not None and _expected_source != self.get(identity, _source_snapshot=True): + raise sqlite3.OperationalError("synthetic source snapshot changed") + if not STORAGE["update_message"](self.conn, identity, **fields): + return None + self.writes.append(dict(fields)) + return self.get(identity) + + def close(self) -> None: + self.conn.close() + + +class SourceProvenanceControls(unittest.TestCase): + def setUp(self) -> None: + self.engine = SyntheticEngine() + self.gate = types.SimpleNamespace(threshold=0.3, w_novelty=0.25, w_salience=0.2, w_prediction_error=0.3, salience_floor=0.1, _pe_available=True, reset_batch=lambda: None) + self.decision = types.SimpleNamespace(should_encode=True, encoding_score=0.6, novelty=0.7, salience=0.8, prediction_error=0.3, reason="synthetic") + self.gate.evaluate = lambda content, category: self.decision + self.observation = PROVENANCE.gate_observation(self.gate, self.decision, True) + self.now = "2026-10-12T01:00:00+00:00" + self.invalidations: list[bool] = [] + self.runtime = types.ModuleType("truememory.tier_switch.runtime") + self.runtime.fact_operation = lambda memory: contextlib.nullcontext() + self.runtime.raise_if_serving_rejection = lambda error: None + self.shared = types.ModuleType("truememory.ingest.hooks._shared") + self.shared.invalidate_recall_cache = lambda: self.invalidations.append(True) + namespace = { + "contextlib": contextlib, "datetime": datetime, "sqlite3": sqlite3, "time": __import__("time"), + "field": field, "dataclass": dataclass, "log": logging.getLogger(__name__), "_safe_log": str, + "_dedup_store_lock": contextlib.nullcontext, "format_for_extraction": TRANSCRIPT["format_for_extraction"], + "DedupAction": DEDUP["DedupAction"], + "os": os, "_set_busy_timeout": lambda memory: None, + "EncodingGate": lambda **kwargs: self.gate, + } + namespace.update({name: getattr(PROVENANCE, name) for name in ("ProvenanceError", "SourceSegment", "fact_metadata", "gate_observation", "merge_metadata", "source_context", "source_timestamp")}) + self.pipeline_namespace = selected_source("truememory/ingest/pipeline.py", {"IngestionPipeline", "IngestionResult", "_invalidate_recall_after_batch"}, namespace) + self.pipeline = namespace["IngestionPipeline"].__new__(namespace["IngestionPipeline"]) + self.pipeline.memory = types.SimpleNamespace(_engine=self.engine) + self.pipeline._record_source_provenance = True + self.pipeline.user_id = "synthetic-account" + self.pipeline.gate = self.gate + self.pipeline.gate_enabled = True + self.pipeline.llm_config = None + self.pipeline.use_llm_dedup = False + self.patch = patch.dict(sys.modules, {self.runtime.__name__: self.runtime, self.shared.__name__: self.shared}) + self.patch.start() + + def tearDown(self) -> None: + self.patch.stop() + self.engine.close() + + def metadata(self, session: str = "synthetic-session", times: tuple[object, ...] = ("2020-01-02T10:00:00Z",), fact: object | None = None) -> dict[str, object]: + text = "User: synthetic source material for durable sample preference" + segments = tuple(PROVENANCE.SourceSegment(0, len(text), index, "human", value) for index, value in enumerate(times)) + context = PROVENANCE.source_context(text, session, segments) + return PROVENANCE.fact_metadata(context, fact or Fact("Prefers synthetic editor", "preference"), self.observation, self.now) + + def test_backfill_roundtrip_uses_source_time_and_separate_ingestion_time(self) -> None: + metadata = self.metadata() + self.pipeline._store_fact("Prefers synthetic editor", Fact("Prefers synthetic editor", "preference"), "synthetic-session", metadata=metadata) + row = self.engine.get(1) + self.assertEqual(row["timestamp"], "2020-01-02T10:00:00+00:00") + self.assertEqual(row["metadata"][PROVENANCE.NAMESPACE]["ingested_at"], self.now) + self.assertEqual(row["metadata"], metadata) + self.assertEqual(row["sender"], "synthetic-account") + self.assertEqual(row["content"], "[preference] Prefers synthetic editor") + + def test_unknown_invalid_naive_and_nonstring_times_are_not_ingestion_time(self) -> None: + for value in ("", "not-a-date", "2020-01-02T10:00:00", None, 2020): + with self.subTest(value=value): + metadata = self.metadata(times=(value,)) + self.assertEqual(PROVENANCE.source_timestamp(metadata), "") + self.assertEqual(PROVENANCE.source_intervals(metadata), ([], True)) + + def test_mixed_coverage_is_explicit_and_scalar_stays_unknown(self) -> None: + metadata = self.metadata(times=("2020-01-02T10:00:00Z", "")) + ranges, unknown = PROVENANCE.source_intervals(metadata) + self.assertEqual(ranges, [("2020-01-02T10:00:00+00:00", "2020-01-02T10:00:00+00:00")]) + self.assertTrue(unknown) + self.assertEqual(PROVENANCE.source_timestamp(metadata), "") + + def test_synthesized_scope_has_no_exact_speaker_or_supporting_message_claim(self) -> None: + citation = self.metadata(times=("2020-01-02T10:00:00Z", "2020-01-03T10:00:00Z"))[PROVENANCE.NAMESPACE]["citations"][0] + self.assertEqual(citation["scope"]["kind"], "extraction_input_scope") + self.assertEqual(citation["scope"]["message_count"], 2) + self.assertNotIn("speaker", citation) + self.assertNotIn("supporting_message_id", citation) + self.assertEqual(citation["extractor_role_assertion"], "user") + + def test_merge_preserves_disjoint_windows_and_unrelated_metadata(self) -> None: + first = {"external": {"preserve": True}, **self.metadata()} + merged, changed = PROVENANCE.merge_metadata(first, self.metadata("synthetic-later", ("2026-01-02T10:00:00Z",))) + self.assertTrue(changed) + self.assertEqual(merged["external"], first["external"]) + self.assertEqual(len(PROVENANCE.source_intervals(merged)[0]), 2) + self.assertEqual(first[PROVENANCE.NAMESPACE]["citations"], merged[PROVENANCE.NAMESPACE]["citations"][:1]) + + def test_retry_preserves_original_gate_observation_and_clock_without_write(self) -> None: + first = self.metadata() + later = self.metadata() + later[PROVENANCE.NAMESPACE]["ingested_at"] = "2030-01-01T00:00:00+00:00" + later[PROVENANCE.NAMESPACE]["last_ingested_at"] = "2030-01-01T00:00:00+00:00" + later[PROVENANCE.NAMESPACE]["citations"][0]["gate"] = {"status": "disabled"} + merged, changed = PROVENANCE.merge_metadata(first, later) + self.assertFalse(changed) + self.assertEqual(merged, first) + + def test_disabled_gate_does_not_claim_synthetic_ones_are_evaluated_scores(self) -> None: + disabled = PROVENANCE.gate_observation(self.gate, self.decision, False) + self.assertEqual(disabled["status"], "disabled") + self.assertIsNone(disabled["novelty"]) + self.assertIsNone(disabled["prediction_error"]) + self.assertNotIn("reason", disabled) + self.assertTrue(all(len(value) == 64 for value in self.observation["source_sha256"].values() if value is not None)) + self.gate._pe_available = False + self.assertEqual(PROVENANCE.gate_observation(self.gate, self.decision, True)["status"], "degraded_open") + + def test_update_merges_citations_atomically_with_content(self) -> None: + self.engine.add(content="Original synthetic fact", sender="synthetic-account", metadata={"external": 7, **self.metadata()}) + self.pipeline._update_fact(1, "Revised synthetic fact", Fact("Revised synthetic fact"), "synthetic-later", metadata=self.metadata("synthetic-later")) + row = self.engine.get(1) + self.assertEqual(row["content"], "Revised synthetic fact") + self.assertEqual(row["metadata"]["external"], 7) + self.assertEqual(len(row["metadata"][PROVENANCE.NAMESPACE]["citations"]), 2) + self.assertEqual(set(self.engine.writes[-1]), {"content", "metadata", "timestamp"}) + + def test_skip_adds_only_metadata_and_retry_is_noop(self) -> None: + self.engine.add(content="Original synthetic fact", metadata=self.metadata()) + incoming = self.metadata("synthetic-later") + self.assertTrue(self.pipeline._merge_fact_provenance(1, incoming)) + self.assertNotIn("content", self.engine.writes[-1]) + writes = len(self.engine.writes) + self.assertFalse(self.pipeline._merge_fact_provenance(1, incoming)) + self.assertEqual(len(self.engine.writes), writes) + + def test_citation_overflow_preserves_original_row_before_update(self) -> None: + metadata = self.metadata() + scope = metadata[PROVENANCE.NAMESPACE]["citations"][0]["scope"] + metadata[PROVENANCE.NAMESPACE]["citations"] = [{"id": f"{index:064x}", "scope": scope} for index in range(PROVENANCE.MAX_CITATIONS)] + self.engine.add(content="Original synthetic fact", metadata=metadata) + original = self.engine.get(1) + with self.assertRaises(PROVENANCE.ProvenanceError): + self.pipeline._update_fact(1, "Changed", Fact("Changed"), "overflow", metadata=self.metadata("overflow")) + self.assertEqual(self.engine.get(1), original) + + def test_byte_overflow_and_invalid_session_are_explicit_recoverable_errors(self) -> None: + metadata = self.metadata() + metadata[PROVENANCE.NAMESPACE]["padding"] = "x" * PROVENANCE.MAX_NAMESPACE_BYTES + with self.assertRaises(sqlite3.OperationalError): + PROVENANCE.merge_metadata({}, metadata) + with self.assertRaises(sqlite3.OperationalError): + PROVENANCE.source_context("synthetic", "x" * (PROVENANCE.MAX_SESSION_BYTES + 1)) + + def test_unknown_future_namespace_never_uses_legacy_timestamp(self) -> None: + self.assertIsNone(PROVENANCE.source_intervals({"external": 1})) + self.assertEqual(PROVENANCE.source_intervals({PROVENANCE.NAMESPACE: {"version": 2}}), ([], True)) + with self.assertRaises(PROVENANCE.ProvenanceError): + PROVENANCE.merge_metadata({PROVENANCE.NAMESPACE: {"version": 2}}, self.metadata()) + + def test_unsupported_client_never_silently_stamps_now(self) -> None: + writes: list[str] = [] + self.pipeline.memory = types.SimpleNamespace(add=lambda **fields: writes.append("called")) + with self.assertRaises(PROVENANCE.ProvenanceError): + self.pipeline._store_fact("synthetic fact", Fact("synthetic fact"), "session") + self.assertEqual(writes, []) + + def test_storage_failure_never_invokes_client_fallback(self) -> None: + def fail(**fields: object) -> None: + raise sqlite3.OperationalError("synthetic locked") + self.engine.add = fail + calls: list[bool] = [] + self.pipeline.memory.add = lambda **fields: calls.append(True) + with self.assertRaises(sqlite3.OperationalError): + self.pipeline._store_fact("synthetic fact", Fact("synthetic fact"), "session") + self.assertEqual(calls, []) + + def test_pipeline_skip_counter_and_invalidation_follow_actual_write(self) -> None: + fact = Fact("Synthetic reusable preference") + text = "User: synthetic durable preference with enough source context for extraction" + context = PROVENANCE.source_context(text, "session") + original = PROVENANCE.fact_metadata(context, fact, self.observation, self.now) + self.engine.add(content=fact.content, metadata=original) + self.pipeline_namespace["extract_facts_simple"] = lambda value: [fact] + self.pipeline_namespace["check_duplicate"] = lambda *args, **kwargs: DEDUP["DedupDecision"](DEDUP["DedupAction"].SKIP, fact.content, existing_id=1) + result = self.pipeline.ingest_text(text, "new-session") + self.assertEqual(result.facts_skipped_dedup, 1) + self.assertEqual(result.facts_provenance_updated, 1) + self.assertEqual(result.facts_updated, 0) + self.assertEqual(len(self.invalidations), 1) + retry = self.pipeline.ingest_text(text, "new-session") + self.assertEqual(retry.facts_skipped_dedup, 1) + self.assertEqual(retry.facts_provenance_updated, 0) + self.assertEqual(len(self.invalidations), 1) + + def test_pipeline_failed_skip_is_nonterminal(self) -> None: + fact = Fact("Synthetic reusable preference") + self.pipeline_namespace["extract_facts_simple"] = lambda value: [fact] + self.pipeline_namespace["check_duplicate"] = lambda *args, **kwargs: DEDUP["DedupDecision"](DEDUP["DedupAction"].SKIP, fact.content, existing_id=999) + result = self.pipeline.ingest_text("User: synthetic durable preference with enough source context for extraction", "session") + self.assertEqual(result.facts_skipped_dedup, 0) + self.assertEqual(result.facts_provenance_updated, 0) + self.assertEqual(result.trace[0]["action"], "storage_failed") + + def test_confirmed_provenance_write_invalidates_after_later_abort(self) -> None: + result = self.pipeline_namespace["IngestionResult"]() + with self.assertRaises(RuntimeError): + with self.pipeline_namespace["_invalidate_recall_after_batch"](result): + result.facts_provenance_updated = 1 + raise RuntimeError("synthetic later failure") + self.assertEqual(self.invalidations, [True]) + + def test_extraction_dedup_unions_spans_preserving_first_fact_and_order(self) -> None: + first, second, third = Fact("Same fact"), Fact("same FACT"), Fact("Different fact") + first.source_spans, second.source_spans = ((0, 12),), ((14, 26),) + selected = EXTRACTOR["_dedupe_facts_by_content"]([first, second, third]) + self.assertEqual(selected, [first, third]) + self.assertIs(selected[0], first) + self.assertEqual(first.source_spans, ((0, 12), (14, 26))) + + def test_actual_chunk_extraction_cites_only_attempted_chunks(self) -> None: + source = "User: " + "a" * 94 + "\n\nUser: " + "b" * 94 + "\n\nUser: " + "c" * 94 + calls: list[str] = [] + EXTRACTOR["complete"] = lambda config, prompt, system: calls.append(prompt) or "synthetic" + EXTRACTOR["_parse_extraction_response_outcome"] = lambda response, cap: types.SimpleNamespace(facts=[Fact("Repeated synthetic fact")], status="complete", salvaged=False, invalid_items=0, omitted_items=0, omitted_facts=0, error_categories=()) + EXTRACTOR["ExtractionOutcome"] = lambda *args: types.SimpleNamespace(facts=args[0], status=args[1], deferred_chunks=args[7]) + outcome = EXTRACTOR["extract_facts_outcome"](source, object(), max_chunks=2) + self.assertEqual(len(calls), 2) + self.assertEqual(outcome.status, "limited") + self.assertEqual(outcome.facts[0].source_spans, ((0, 100), (102, 202))) + self.assertEqual(outcome.deferred_chunks, 1) + + def test_actual_transcript_pipeline_keeps_formatted_text_and_source_window(self) -> None: + messages = [types.SimpleNamespace(role="human", content="I prefer synthetic durable editor settings for all sample projects", timestamp="2020-01-02T10:00:00Z"), types.SimpleNamespace(role="assistant", content="Synthetic observation", timestamp="2020-01-03T10:00:00Z")] + fact = Fact("Prefers synthetic editor", "preference") + extracted: list[str] = [] + self.pipeline_namespace["parse_transcript"] = lambda location: messages + self.pipeline_namespace["extract_facts_simple"] = lambda text: extracted.append(text) or [fact] + self.pipeline_namespace["check_duplicate"] = lambda *args, **kwargs: DEDUP["DedupDecision"](DEDUP["DedupAction"].ADD, fact.content) + result = self.pipeline.ingest_transcript("synthetic-no-file-read", "source-session") + self.assertEqual(result.facts_stored, 1) + self.assertEqual(extracted, [TRANSCRIPT["format_for_extraction"](messages)]) + self.assertEqual(self.engine.get(1)["timestamp"], "2020-01-03T10:00:00+00:00") + citation = self.engine.get(1)["metadata"][PROVENANCE.NAMESPACE]["citations"][0] + self.assertEqual(citation["scope"]["observed_roles"], ["assistant", "human"]) + self.assertEqual(citation["session_id"], "source-session") + + def test_legacy_contribution_remains_unknown_after_new_known_citation(self) -> None: + self.engine.add(content="Legacy synthetic fact", timestamp="2025-01-01T00:00:00Z", metadata={"external": 7}) + self.assertTrue(self.pipeline._merge_fact_provenance(1, self.metadata())) + row = self.engine.get(1) + self.assertTrue(row["metadata"][PROVENANCE.NAMESPACE]["legacy_unknown"]) + self.assertEqual(row["timestamp"], "") + self.assertEqual(row["metadata"]["external"], 7) + self.assertTrue(PROVENANCE.source_intervals(row["metadata"])[1]) + self.assertEqual(len(PROVENANCE.source_intervals(row["metadata"])[0]), 1) + + def test_malformed_recognized_coverage_never_becomes_known(self) -> None: + for field_name, value in (("version", True), ("legacy_unknown", "false"), ("timestamp_semantics", "fact_event_time")): + with self.subTest(field_name=field_name): + metadata = self.metadata() + metadata[PROVENANCE.NAMESPACE][field_name] = value + self.assertEqual(PROVENANCE.source_intervals(metadata), ([], True)) + for field_name, value in (("unknown_count", 1), ("unknown_count", False), ("status", "invented"), ("end", "2010-01-01T00:00:00Z")): + with self.subTest(field_name=field_name): + metadata = self.metadata() + metadata[PROVENANCE.NAMESPACE]["citations"][0]["scope"]["source_time"][field_name] = value + self.assertEqual(PROVENANCE.source_intervals(metadata), ([], True)) + metadata = self.metadata() + metadata[PROVENANCE.NAMESPACE]["citations"][0]["scope"]["kind"] = "precise_support" + self.assertEqual(PROVENANCE.source_intervals(metadata), ([], True)) + + def test_missing_scorer_source_is_explicitly_unavailable(self) -> None: + with patch.object(PROVENANCE, "_scorer_sources", return_value={"encoding_gate.py": None}): + observation = PROVENANCE.gate_observation(self.gate, self.decision, True) + self.assertEqual(observation["source_fingerprint_status"], "unavailable") + self.assertIsNone(observation["source_sha256"]["encoding_gate.py"]) + + def test_repeated_text_in_same_session_with_new_source_time_is_new_evidence(self) -> None: + first = self.metadata() + second = self.metadata(times=("2021-01-02T10:00:00Z",)) + merged, changed = PROVENANCE.merge_metadata(first, second) + self.assertTrue(changed) + self.assertEqual(len(merged[PROVENANCE.NAMESPACE]["citations"]), 2) + self.assertEqual(len(PROVENANCE.source_intervals(merged)[0]), 2) + + def test_enormous_score_is_unavailable_without_changing_gate_decision(self) -> None: + self.assertIsNone(PROVENANCE._number(10**1000)) + self.decision.novelty = 10**1000 + self.gate.threshold = 10**1000 + observed = PROVENANCE.gate_observation(self.gate, self.decision, True) + self.assertIsNone(observed["novelty"]) + self.assertIsNone(observed["settings"]["threshold"]) + self.assertTrue(observed["passed"]) + self.assertTrue(self.decision.should_encode) + + def test_invalid_span_and_source_encoding_fail_recoverably(self) -> None: + context = PROVENANCE.source_context("synthetic", "session") + for span in ((-1, 1), (0, 100), (True, 1)): + with self.subTest(span=span), self.assertRaises(sqlite3.OperationalError): + fact = Fact("Synthetic") + fact.source_spans = (span,) + PROVENANCE.fact_metadata(context, fact, self.observation, self.now) + with self.assertRaises(sqlite3.OperationalError): + PROVENANCE.source_context("synthetic", "\ud800") + + def test_constructor_enables_provenance_unconditionally(self) -> None: + initialized = self.pipeline_namespace["IngestionPipeline"]( + memory=types.SimpleNamespace(_engine=self.engine), llm_config=object(), user_id="synthetic") + self.assertTrue(initialized._record_source_provenance) + + def test_private_spans_do_not_expand_public_dataclass_schema(self) -> None: + fact = Fact("Synthetic fact") + fact.source_spans = ((0, 10),) + self.assertEqual(asdict(fact), {"content": "Synthetic fact", "category": "general", + "confidence": "medium", "source_role": "user"}) + self.assertEqual(fact.source_spans, ((0, 10),)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-source-update-fence-748.py b/tests/test-source-update-fence-748.py new file mode 100644 index 00000000..325c6f15 --- /dev/null +++ b/tests/test-source-update-fence-748.py @@ -0,0 +1,267 @@ +"""Synthetic actual-source controls for guarded source and field-only SEP writes.""" +from __future__ import annotations + +import ast +import contextlib +import copy +import json +import logging +import sqlite3 +import sys +import threading +import types +import unittest +from collections.abc import Iterator +from pathlib import Path +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] + + +def functions(relative: str, names: set[str], namespace: dict[str, object]) -> None: + path = ROOT / relative + nodes = [ast.ImportFrom(module="__future__", names=[ast.alias(name="annotations")], level=0)] + nodes.extend(node for node in ast.parse(path.read_text()).body + if isinstance(node, ast.FunctionDef) and node.name in names) + if {node.name for node in nodes if isinstance(node, ast.FunctionDef)} != names: + raise AssertionError("Missing actual source functions") + exec(compile(ast.fix_missing_locations(ast.Module(body=nodes, type_ignores=[])), str(path), "exec"), namespace) + + +class SourceUpdateFenceTests(unittest.TestCase): + def setUp(self) -> None: + self.conn = sqlite3.connect(":memory:") + self.conn.execute("CREATE TABLE messages(id INTEGER PRIMARY KEY,content TEXT,sender TEXT,recipient TEXT,timestamp TEXT,category TEXT,modality TEXT,directive INTEGER,metadata TEXT)") + self.conn.execute("INSERT INTO messages VALUES(1,'synthetic content','synthetic user','synthetic recipient','2020-01-01','general','text',0,'{\"citations\":[\"original\"]}')") + for table in ("vec_selected", "sep_selected", "sep_other"): + self.conn.execute(f"CREATE TABLE {table}(rowid INTEGER PRIMARY KEY,embedding BLOB)") + self.conn.execute(f"INSERT INTO {table} VALUES(1,?)", (b"original",)) + self.conn.commit() + storage = types.ModuleType("truememory.storage") + storage.__dict__.update({"json": json, "sqlite3": sqlite3, + "_OPTIONAL_SELECT_DEFAULTS": {"sender": "''", "recipient": "''", "timestamp": "''", + "category": "''", "modality": "''", "directive": "0", "metadata": "'{}'"}}) + functions("truememory/storage.py", {"_serialize_metadata", "_deserialize_metadata", "_row_to_dict", + "_message_columns", "select_message_cols", "get_message", "update_message"}, storage.__dict__) + writer = types.ModuleType("truememory.tier_switch.writer") + writer.capture_writer_selection = lambda conn: types.SimpleNamespace(selection=None) + + @contextlib.contextmanager + def transaction(conn: sqlite3.Connection, selection: object, **kwargs: object) -> Iterator[None]: + nested = conn.in_transaction + conn.execute("SAVEPOINT synthetic_update" if nested else "BEGIN IMMEDIATE") + try: + yield + conn.execute("RELEASE SAVEPOINT synthetic_update") if nested else conn.commit() + except Exception: + if nested: + conn.execute("ROLLBACK TO SAVEPOINT synthetic_update") + conn.execute("RELEASE SAVEPOINT synthetic_update") + else: + conn.rollback() + raise + + writer.writer_transaction = transaction + vector = types.ModuleType("truememory.vector_search") + + class PublicationChanged(RuntimeError): + pass + + vector.VectorPublicationChanged = PublicationChanged + vector._capture_rebuild_model = lambda conn: (object(), (7, "synthetic-model", 3)) + self.encoded: list[str] = [] + self.encoding_error = False + self.publication_error = False + self.after_encode = None + + def encode(model: object, texts: list[str]) -> list[bytes]: + self.encoded.extend(texts) + if self.encoding_error: + raise ValueError("synthetic encode failure") + if self.after_encode is not None: + self.after_encode() + return [text.encode() for text in texts] + + vector._encode_with_mps_fallback = encode + functions("truememory/vector_search.py", {"_build_sep_text"}, vector.__dict__) + vector.serialize_f32 = lambda value: value + vector._write_foreground_embedder_metadata_no_commit = lambda *args, **kwargs: None + + @contextlib.contextmanager + def publication(conn: sqlite3.Connection, identity: object, memory_id: int, + *, selection: object) -> Iterator[tuple[str, str]]: + if self.publication_error: + raise PublicationChanged("synthetic publication failure") + with transaction(conn, selection): + yield "vec_selected", "sep_selected" + + vector._foreground_vector_publication = publication + runtime = types.ModuleType("truememory.tier_switch.runtime") + runtime.raise_if_serving_rejection = lambda exc: None + package = types.ModuleType("truememory") + package.__path__ = [] + self.modules = patch.dict(sys.modules, {"truememory": package, storage.__name__: storage, + writer.__name__: writer, vector.__name__: vector, runtime.__name__: runtime}) + self.modules.start() + path = ROOT / "truememory/engine.py" + original = next(node for node in ast.parse(path.read_text()).body + if isinstance(node, ast.ClassDef) and node.name == "TrueMemoryEngine") + methods = [copy.deepcopy(node) for node in original.body + if isinstance(node, ast.FunctionDef) and node.name in {"update", "get"}] + for method in methods: + method.decorator_list = [] + selected = ast.ClassDef(name="SelectedEngine", bases=[], keywords=[], body=methods, decorator_list=[]) + namespace = {"sqlite3": sqlite3, "logger": logging.getLogger("source-update-control"), + "update_message": storage.update_message, "get_message": storage.get_message} + nodes = [ast.ImportFrom(module="__future__", names=[ast.alias(name="annotations")], level=0), selected] + exec(compile(ast.fix_missing_locations(ast.Module(body=nodes, type_ignores=[])), str(path), "exec"), namespace) + self.engine = namespace["SelectedEngine"]() + self.engine.conn = self.conn + self.engine._has_vectors = False + self.engine._ensure_connection = lambda: None + self.engine._write_lock = threading.RLock() + self.consolidations: list[bool] = [] + self.engine._maybe_auto_consolidate = lambda: self.consolidations.append(True) + self.changed_error = PublicationChanged + + def tearDown(self) -> None: + self.modules.stop() + self.conn.close() + + def source(self) -> tuple: + return self.conn.execute("SELECT * FROM messages WHERE id=1").fetchone() + + def vector(self, table: str) -> bytes: + return self.conn.execute(f"SELECT embedding FROM {table} WHERE rowid=1").fetchone()[0] + + def test_matched_snapshot_and_default_shape(self) -> None: + self.assertNotIn("_source_snapshot_sha256", self.engine.get(1)) + expected = self.engine.get(1, _source_snapshot=True) + result = self.engine.update(1, metadata={"citations": ["original", "new"]}, _expected_source=expected) + self.assertEqual(result["metadata"], {"citations": ["original", "new"]}) + self.assertNotIn("_source_snapshot_sha256", result) + self.assertEqual(self.encoded, []) + self.assertFalse(self.conn.in_transaction) + + def test_each_concurrent_relevant_field_rejects_before_mutation(self) -> None: + for column, value in {"content": "new content", "sender": "other user", "recipient": "other recipient", + "timestamp": "2025-01-01", "category": "other", "modality": "image", "directive": 1, + "metadata": '{"citations":["external"]}'}.items(): + with self.subTest(column=column): + expected = self.engine.get(1, _source_snapshot=True) + self.conn.execute(f"UPDATE messages SET {column}=? WHERE id=1", (value,)) + self.conn.commit() + before = self.source() + with self.assertRaises(sqlite3.OperationalError): + self.engine.update(1, metadata={"citations": ["lost"]}, _expected_source=expected) + self.assertEqual(self.source(), before) + self.assertEqual(self.vector("sep_selected"), b"original") + self.assertFalse(self.conn.in_transaction) + + def test_raw_malformed_metadata_cannot_alias_deserialized_empty(self) -> None: + self.conn.execute("UPDATE messages SET metadata='invalid-first' WHERE id=1") + self.conn.commit() + expected = self.engine.get(1, _source_snapshot=True) + self.conn.execute("UPDATE messages SET metadata='invalid-second' WHERE id=1") + self.conn.commit() + self.assertEqual(expected["metadata"], self.engine.get(1)["metadata"]) + with self.assertRaises(sqlite3.OperationalError): + self.engine.update(1, metadata={}, _expected_source=expected) + self.assertEqual(self.source()[-1], "invalid-second") + + def test_missing_and_typed_tampered_snapshot_reject(self) -> None: + expected = self.engine.get(1, _source_snapshot=True) + for tampered in ({}, {**expected, "directive": 0}, {**expected, "_source_snapshot_sha256": "bad"}): + with self.subTest(tampered=tampered): + with self.assertRaises(sqlite3.OperationalError): + self.engine.update(1, category="changed", _expected_source=tampered) + self.assertEqual(self.source()[5], "general") + self.conn.execute("DELETE FROM messages WHERE id=1") + self.conn.commit() + with self.assertRaises(sqlite3.OperationalError): + self.engine.update(1, category="changed", _expected_source=expected) + + def test_unconditional_update_compatibility(self) -> None: + self.assertEqual(self.engine.update(1, category="legacy")["category"], "legacy") + self.assertIsNone(self.engine.update(999, category="missing")) + + def test_unsupported_raw_snapshot_is_controlled_and_preserved(self) -> None: + self.conn.execute("UPDATE messages SET metadata=? WHERE id=1", (b"synthetic opaque metadata",)) + self.conn.commit() + before = self.source() + self.assertEqual(self.engine.get(1)["metadata"], {}) + with self.assertRaises(sqlite3.OperationalError): + self.engine.get(1, _source_snapshot=True) + self.assertEqual(self.source(), before) + + def test_timestamp_only_replaces_selected_SEP_and_keeps_primary(self) -> None: + self.engine._has_vectors = True + expected = self.engine.get(1, _source_snapshot=True) + result = self.engine.update(1, timestamp="2025-01-01", _expected_source=expected) + self.assertEqual(result["timestamp"], "2025-01-01") + self.assertEqual(len(self.encoded), 1) + self.assertIn("2025-01-01", self.encoded[0]) + self.assertIn("synthetic content", self.encoded[0]) + self.assertEqual(self.vector("sep_selected"), self.encoded[0].encode()) + self.assertEqual(self.vector("vec_selected"), b"original") + self.assertEqual(self.vector("sep_other"), b"original") + self.assertFalse(self.conn.in_transaction) + + def test_sender_recipient_changes_refresh_SEP(self) -> None: + self.engine._has_vectors = True + self.engine.update(1, sender="next user", recipient="next recipient") + self.assertIn("next user", self.encoded[0]) + self.assertIn("next recipient", self.encoded[0]) + self.assertEqual(self.vector("vec_selected"), b"original") + + def test_metadata_or_identical_timestamp_does_not_encode(self) -> None: + self.engine._has_vectors = True + self.engine.update(1, metadata={"extra": True}) + self.engine.update(1, timestamp="2020-01-01") + self.assertEqual(self.encoded, []) + self.assertEqual(self.vector("sep_selected"), b"original") + + def test_field_only_encode_failure_preserves_source_and_vectors(self) -> None: + self.engine._has_vectors = self.encoding_error = True + before = self.source() + with self.assertRaises(sqlite3.OperationalError): + self.engine.update(1, timestamp="2025-01-01") + self.assertEqual(self.source(), before) + self.assertEqual(self.vector("sep_selected"), b"original") + self.assertEqual(self.consolidations, []) + + def test_field_only_publication_failure_rolls_back_source(self) -> None: + self.engine._has_vectors = self.publication_error = True + before = self.source() + with self.assertRaises(self.changed_error): + self.engine.update(1, timestamp="2025-01-01", metadata={"changed": True}) + self.assertEqual(self.source(), before) + self.assertEqual(self.vector("sep_selected"), b"original") + self.assertEqual(self.vector("vec_selected"), b"original") + self.assertFalse(self.conn.in_transaction) + self.assertEqual(self.consolidations, []) + + def test_source_change_during_field_only_encoding_is_rejected(self) -> None: + self.engine._has_vectors = True + def mutate() -> None: + self.conn.execute("UPDATE messages SET content='external newer content' WHERE id=1") + self.conn.commit() + self.after_encode = mutate + with self.assertRaises(sqlite3.OperationalError): + self.engine.update(1, timestamp="2025-01-01") + self.assertEqual(self.source()[1], "external newer content") + self.assertEqual(self.source()[4], "2020-01-01") + self.assertEqual(self.vector("sep_selected"), b"original") + + def test_content_publication_failure_keeps_historical_committed_source(self) -> None: + self.engine._has_vectors = self.publication_error = True + with self.assertRaisesRegex(self.changed_error, "Source update committed"): + self.engine.update(1, content="replacement synthetic content") + self.assertEqual(self.source()[1], "replacement synthetic content") + self.assertEqual(self.vector("vec_selected"), b"original") + self.assertEqual(self.consolidations, [True]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-style-getter-freshness-748.py b/tests/test-style-getter-freshness-748.py new file mode 100644 index 00000000..15f5ec43 --- /dev/null +++ b/tests/test-style-getter-freshness-748.py @@ -0,0 +1,148 @@ +"""Bounded tracked style getter controls with actual source and in-memory SQLite.""" +from __future__ import annotations + +import hashlib +import json +import runpy +import sqlite3 +import unittest +from pathlib import Path + +BASE = runpy.run_path(str(Path(__file__).with_name("test-l0-style-maintenance-747.py"))) +STORAGE, STYLE, MAINTENANCE = BASE["STORAGE"], BASE["STYLE"], BASE["MAINTENANCE"] + + +class StyleGetterFreshnessTests(unittest.TestCase): + def db(self, *, certified: bool = True) -> sqlite3.Connection: + conn = sqlite3.connect(":memory:", cached_statements=0) + self.addCleanup(conn.close) + conn.executescript(STORAGE._SCHEMA_SQL) + STORAGE._initialize_maintenance_tracking(conn) + conn.execute("INSERT INTO messages(content,sender,recipient,timestamp) VALUES('synthetic original style','Synthetic','Other','2020-01-01')") + conn.commit() + if certified: + result = MAINTENANCE.run_style_maintenance(conn) + self.assertEqual(result.outcome, "success") + else: + STYLE.build_entity_style_vectors(conn) + return conn + + def rows(self, conn: sqlite3.Connection) -> list[tuple]: + return conn.execute("SELECT * FROM entity_style_vectors").fetchall() + + def test_proof_fingerprints_match_actual_storage_contract(self) -> None: + definitions = STORAGE._maintenance_trigger_definitions() + definitions.update(STORAGE._STYLE_OUTPUT_TRIGGERS) + hashes = {name: hashlib.sha256(" ".join(sql.split()).encode()).hexdigest() + for name, sql in definitions.items()} + self.assertEqual(STYLE._STYLE_READ_TRIGGER_SHA256, hashes) + + def test_current_generation_and_case_normalization_remain_exact(self) -> None: + conn = self.db() + expected = json.loads(self.rows(conn)[0][1]) + self.assertEqual(STYLE.get_entity_style_vector(conn, "SYNTHETIC"), expected) + self.assertIsNone(STYLE.get_entity_style_vector(conn, "missing")) + self.assertFalse(conn.in_transaction) + + def test_every_source_correction_hides_old_rows_without_erasing(self) -> None: + edits = {"content": "synthetic changed", "sender": "Changed", "recipient": "Changed", + "timestamp": "2021-01-01", "category": "changed", "modality": "changed", + "directive": 1, "metadata": '{"synthetic":true}'} + for column, value in edits.items(): + with self.subTest(column=column): + conn = self.db(); before = self.rows(conn) + conn.execute(f"UPDATE messages SET {column}=?", (value,)); conn.commit() + self.assertEqual(MAINTENANCE.observe_style(conn).health["freshness"], "correction_pending") + self.assertIsNone(STYLE.get_entity_style_vector(conn, "Synthetic")) + self.assertEqual(self.rows(conn), before) + self.assertFalse(conn.in_transaction) + + def test_append_pending_prior_valid_generation_remains_readable(self) -> None: + conn = self.db(); expected = STYLE.get_entity_style_vector(conn, "Synthetic") + conn.execute("INSERT INTO messages(content,sender) VALUES('synthetic appended','Synthetic')"); conn.commit() + self.assertEqual(MAINTENANCE.observe_style(conn).health["freshness"], "append_pending") + self.assertEqual(STYLE.get_entity_style_vector(conn, "Synthetic"), expected) + + def test_successful_repair_restores_current_exact_generation(self) -> None: + conn = self.db() + conn.execute("UPDATE messages SET content='synthetic corrected',sender='Changed'"); conn.commit() + self.assertIsNone(STYLE.get_entity_style_vector(conn, "Synthetic")) + self.assertEqual(MAINTENANCE.run_style_maintenance(conn).outcome, "success") + self.assertIsNone(STYLE.get_entity_style_vector(conn, "Synthetic")) + self.assertEqual(STYLE.get_entity_style_vector(conn, "CHANGED"), json.loads(self.rows(conn)[0][1])) + + def test_direct_artifact_mutation_invalidates_the_generation(self) -> None: + conn = self.db(); conn.execute("UPDATE entity_style_vectors SET vector='[123]'"); conn.commit() + before = self.rows(conn) + self.assertIsNone(STYLE.get_entity_style_vector(conn, "Synthetic")) + self.assertEqual(self.rows(conn), before) + + def test_epoch_reset_and_missing_checkpoint_do_not_revive_old_rows(self) -> None: + for statement in ("UPDATE maintenance_source_state SET epoch='synthetic changed epoch'", + "DELETE FROM maintenance_layers WHERE layer='style_vectors'", + "DROP TABLE maintenance_layers"): + with self.subTest(statement=statement): + conn = self.db(); before = self.rows(conn); conn.execute(statement); conn.commit() + self.assertIsNone(STYLE.get_entity_style_vector(conn, "Synthetic")) + self.assertEqual(self.rows(conn), before) + + def test_missing_or_replaced_tracking_and_output_triggers_are_unavailable(self) -> None: + for name in STYLE._STYLE_READ_TRIGGER_SHA256: + with self.subTest(name=name): + conn = self.db(); before = self.rows(conn) + conn.execute(f"DROP TRIGGER {name}"); conn.commit() + self.assertIsNone(STYLE.get_entity_style_vector(conn, "Synthetic")) + self.assertEqual(self.rows(conn), before) + + def test_untracked_legacy_and_direct_builder_payloads_remain_exact(self) -> None: + conn = self.db(certified=False) + self.assertEqual(STYLE.get_entity_style_vector(conn, "Synthetic"), json.loads(self.rows(conn)[0][1])) + legacy = sqlite3.connect(":memory:"); self.addCleanup(legacy.close) + legacy.execute("CREATE TABLE entity_style_vectors(entity TEXT PRIMARY KEY,vector TEXT)") + legacy.execute("INSERT INTO entity_style_vectors VALUES('synthetic','[1]')"); legacy.commit() + self.assertEqual(STYLE.get_entity_style_vector(legacy, "SYNTHETIC"), [1]) + + def test_borrowed_source_change_is_hidden_and_caller_rollback_preserved(self) -> None: + conn = self.db(); expected = STYLE.get_entity_style_vector(conn, "Synthetic") + conn.execute("UPDATE messages SET content='synthetic pending caller change'") + self.assertIsNone(STYLE.get_entity_style_vector(conn, "Synthetic")) + self.assertTrue(conn.in_transaction) + conn.rollback() + self.assertEqual(STYLE.get_entity_style_vector(conn, "Synthetic"), expected) + + def test_bounded_inspection_never_reads_source_or_changes_schema(self) -> None: + conn = self.db(); expected = STYLE.get_entity_style_vector(conn, "Synthetic"); seen = [] + def authorize(action: int, table: str | None, *_args: object) -> int: + seen.append((action, table)) + if action == sqlite3.SQLITE_READ and table == "messages": + return sqlite3.SQLITE_DENY + if action in (sqlite3.SQLITE_INSERT, sqlite3.SQLITE_UPDATE, sqlite3.SQLITE_DELETE, + sqlite3.SQLITE_CREATE_TABLE, sqlite3.SQLITE_CREATE_TRIGGER, sqlite3.SQLITE_DROP_TABLE): + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + conn.set_authorizer(authorize) + try: + self.assertEqual(STYLE.get_entity_style_vector(conn, "Synthetic"), expected) + finally: + conn.set_authorizer(None) + self.assertNotIn((sqlite3.SQLITE_READ, "messages"), seen) + self.assertFalse(conn.in_transaction) + + def test_shadowed_tables_and_unknown_output_triggers_are_unavailable(self) -> None: + for statement in ("CREATE TEMP TABLE messages(id INTEGER)", + "CREATE TRIGGER synthetic_foreign AFTER UPDATE ON entity_style_vectors BEGIN SELECT 1; END"): + with self.subTest(statement=statement): + conn = self.db(); conn.execute(statement); conn.commit() + self.assertIsNone(STYLE.get_entity_style_vector(conn, "Synthetic")) + + def test_incompatible_dependency_and_hash_never_claim_current(self) -> None: + for statement in ("UPDATE metadata SET value='1' WHERE key='style_vec_hash_version'", + "UPDATE maintenance_layers SET successful_dependency='{}' WHERE layer='style_vectors'", + "UPDATE maintenance_layers SET successful_coverage='unverified' WHERE layer='style_vectors'"): + with self.subTest(statement=statement): + conn = self.db(); conn.execute(statement); conn.commit() + self.assertIsNone(STYLE.get_entity_style_vector(conn, "Synthetic")) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-surprise-index-transactions-750.py b/tests/test-surprise-index-transactions-750.py new file mode 100644 index 00000000..2da18b56 --- /dev/null +++ b/tests/test-surprise-index-transactions-750.py @@ -0,0 +1,424 @@ +"""Surprise-index transaction regressions using SQLite and synthetic messages.""" +from __future__ import annotations + +import ast +from collections.abc import Callable +from pathlib import Path +import sqlite3 +import tempfile +import threading +import types +import unittest +from unittest.mock import patch + + +SOURCE = Path(__file__).resolve().parents[1] / "truememory" / "predictive.py" + + +def load_predictive() -> types.ModuleType: + # Execute the production stdlib module without importing the package, + # whose initialization can load optional native retrieval dependencies. + module = types.ModuleType("synthetic_predictive_750") + exec(compile(ast.parse(SOURCE.read_text()), str(SOURCE), "exec"), module.__dict__) + return module + + +class SurpriseIndexTransactions(unittest.TestCase): + def setUp(self) -> None: + directory = tempfile.TemporaryDirectory(prefix="tm-surprise-750-") + self.addCleanup(directory.cleanup) + self.path = Path(directory.name) / "synthetic.sqlite" + self.conn = sqlite3.connect(self.path, timeout=0.5, check_same_thread=False) + self.addCleanup(self.conn.close) + self.conn.execute("PRAGMA journal_mode=WAL") + self.conn.execute("PRAGMA foreign_keys=ON") + self.conn.executescript(""" + CREATE TABLE messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + content TEXT NOT NULL, + timestamp TEXT DEFAULT '' + ); + CREATE TABLE notes (id INTEGER PRIMARY KEY, value TEXT); + CREATE TABLE surprise_scores ( + message_id INTEGER PRIMARY KEY REFERENCES messages(id) ON DELETE CASCADE, + surprise REAL NOT NULL DEFAULT 0.0, + fact_count INTEGER NOT NULL DEFAULT 0, + new_fact_count INTEGER NOT NULL DEFAULT 0 + ); + """) + self.conn.executemany( + "INSERT INTO messages(content,timestamp) VALUES (?,?)", + [ + ("Meridian launched in Austin and raised $1.5M.", "2026-02-02"), + ("Meridian raised $1.5M.", "2026-02-03"), + ("okay", "2026-02-03"), + ], + ) + self.conn.commit() + self.predictive = load_predictive() + self.predictive.build_surprise_index(self.conn) + + def other(self) -> sqlite3.Connection: + connection = sqlite3.connect(self.path, timeout=0.5) + connection.execute("PRAGMA foreign_keys=ON") + self.addCleanup(connection.close) + return connection + + def stored(self, conn: sqlite3.Connection | None = None) -> list[tuple]: + return (conn or self.conn).execute( + "SELECT message_id,surprise,fact_count,new_fact_count " + "FROM surprise_scores ORDER BY message_id" + ).fetchall() + + def reference(self) -> tuple[dict[int, float], list[tuple]]: + rows = self.conn.execute( + "SELECT id,content,timestamp FROM messages ORDER BY timestamp,id" + ).fetchall() + expected, stored = {}, [] + for index, (msg_id, content, _timestamp) in enumerate(rows): + # Independent prefix definition of chronological surprise. Small + # fixtures can recompute the whole prefix instead of sharing the + # production accumulator implementation. + prefix_facts = set().union(*( + self.predictive.extract_facts(previous[1]) for previous in rows[:index] + )) + facts = self.predictive.extract_facts(content) + score = self.predictive.compute_surprise_score(content, prefix_facts) + expected[msg_id] = score + stored.append((msg_id, round(score, 4), len(facts), len(facts - prefix_facts))) + return expected, sorted(stored) + + def test_chronological_reference_after_add_update_delete_and_backdate(self) -> None: + mutations = [ + ("SELECT 1", ()), + ("INSERT INTO messages(content,timestamp) VALUES (?,?)", + ("Orion moved to Denver and raised $2M.", "2026-03-01")), + ("UPDATE messages SET content=? WHERE id=1", + ("Meridian launched in Boston and raised $3M.",)), + ("DELETE FROM messages WHERE id=2", ()), + ("INSERT INTO messages(content,timestamp) VALUES (?,?)", + ("Orion raised $2M.", "2026-01-01")), + ("UPDATE messages SET timestamp=? WHERE id=1", ("2025-12-01",)), + ] + for sql, values in mutations: + with self.subTest(sql=sql): + self.conn.execute(sql, values) + self.conn.commit() + expected, stored = self.reference() + returned = self.predictive.build_surprise_index(self.conn) + self.assertEqual(list(returned), list(expected)) + self.assertEqual(returned, expected) + self.assertEqual(self.stored(), stored) + + def test_full_precision_return_and_four_decimal_storage(self) -> None: + with patch.object(self.predictive, "compute_surprise_score", return_value=0.123456789): + scores = self.predictive.build_surprise_index(self.conn) + self.assertTrue(all(score == 0.123456789 for score in scores.values())) + self.assertTrue(all(row[1] == 0.1235 for row in self.stored())) + + def during_compute( + self, action: Callable[[sqlite3.Connection], None], + ) -> tuple[list[object], bool]: + entered, release = threading.Event(), threading.Event() + outcomes: list[object] = [] + original = self.predictive.compute_surprise_score + paused = False + + def score(content: str, facts: set) -> float: + nonlocal paused + if not paused: + paused = True + entered.set() + if not release.wait(3): + raise RuntimeError("Synthetic compute watchdog expired") + return original(content, facts) + + def run() -> None: + try: + outcomes.append(self.predictive.build_surprise_index(self.conn)) + except BaseException as error: + outcomes.append(error) + + with patch.object(self.predictive, "compute_surprise_score", side_effect=score): + worker = threading.Thread(target=run) + worker.start() + try: + self.assertTrue(entered.wait(2), "Production compute path was not entered") + held_transaction = self.conn.in_transaction + action(self.other()) + finally: + release.set() + worker.join(3) + self.assertFalse(worker.is_alive(), "Synthetic worker did not stop") + self.assertEqual(len(outcomes), 1) + return outcomes, held_transaction + + def test_foreground_writer_completes_while_compute_is_paused(self) -> None: + def write(other: sqlite3.Connection) -> None: + other.execute("INSERT INTO notes VALUES (1,'synthetic foreground write')") + other.commit() + + isolation = self.conn.isolation_level + outcomes, held_transaction = self.during_compute(write) + self.assertFalse(held_transaction) + self.assertIsInstance(outcomes[0], dict) + self.assertEqual(self.conn.execute("SELECT COUNT(*) FROM notes").fetchone()[0], 1) + self.assertEqual(self.conn.isolation_level, isolation) + + def test_concurrent_update_delete_and_backdated_insert_reject_stale_output(self) -> None: + mutations = [ + ("UPDATE messages SET content=? WHERE id=1", ("Corrected synthetic content",)), + ("UPDATE messages SET timestamp=? WHERE id=1", ("2024-01-01",)), + ("DELETE FROM messages WHERE id=2", ()), + ("INSERT INTO messages(content,timestamp) VALUES (?,?)", + ("Backdated synthetic evidence", "2020-01-01")), + ("INSERT INTO messages(content,timestamp) VALUES (?,?)", + ("Later synthetic evidence", "2030-01-01")), + ] + for sql, values in mutations: + with self.subTest(sql=sql): + previous: list[tuple] = [] + + def write(other: sqlite3.Connection) -> None: + other.execute(sql, values) + other.commit() + previous.extend(self.stored(other)) + + outcomes, held_transaction = self.during_compute(write) + self.assertFalse(held_transaction) + self.assertIsInstance(outcomes[0], sqlite3.OperationalError) + self.assertIn("source changed", str(outcomes[0])) + self.assertIn("retry", str(outcomes[0])) + self.conn.commit() # The engine's later commit cannot publish failed output. + self.assertEqual(self.stored(), previous) + expected, stored = self.reference() + self.assertEqual(self.predictive.build_surprise_index(self.conn), expected) + self.assertEqual(self.stored(), stored) + + def test_mid_compute_failure_preserves_previous_index_after_commit(self) -> None: + previous = self.stored() + original = self.predictive.compute_surprise_score + calls = 0 + + def fail(content: str, facts: set) -> float: + nonlocal calls + calls += 1 + if calls == 2: + raise RuntimeError("Synthetic compute failure") + return original(content, facts) + + with patch.object(self.predictive, "compute_surprise_score", side_effect=fail): + with self.assertRaisesRegex(RuntimeError, "Synthetic compute failure"): + self.predictive.build_surprise_index(self.conn) + self.conn.commit() + self.assertEqual(self.stored(), previous) + + def fail_second_insert(self) -> None: + self.conn.execute(""" + CREATE TEMP TRIGGER fail_surprise_insert BEFORE INSERT ON surprise_scores + WHEN NEW.message_id = 2 + BEGIN SELECT RAISE(ABORT, 'synthetic insert failure'); END + """) + + def test_mid_write_failure_restores_complete_previous_index(self) -> None: + previous = self.stored() + self.fail_second_insert() + with self.assertRaisesRegex(sqlite3.IntegrityError, "synthetic insert failure"): + self.predictive.build_surprise_index(self.conn) + self.assertFalse(self.conn.in_transaction) + self.conn.commit() + self.assertEqual(self.stored(), previous) + + def test_final_source_validation_holds_writer_ownership(self) -> None: + connection = self.conn + other = self.other() + other.execute("PRAGMA busy_timeout=20") + observed = [] + + class Witness: + source_reads = 0 + + def execute(self, sql: str, *args: object) -> sqlite3.Cursor: + if sql.startswith("SELECT id, content, timestamp FROM messages"): + self.source_reads += 1 + if self.source_reads == 2: + try: + other.execute("UPDATE messages SET content='Stale writer' WHERE id=1") + except sqlite3.OperationalError: + observed.append("writer blocked") + else: + other.rollback() + observed.append("writer admitted") + return connection.execute(sql, *args) + + def executemany(self, sql: str, values: list[tuple]) -> sqlite3.Cursor: + return connection.executemany(sql, values) + + self.predictive.build_surprise_index(Witness()) + self.assertEqual(observed, ["writer blocked"]) + other.execute("UPDATE messages SET content='Writer resumes' WHERE id=1") + other.commit() + + def test_mid_write_cancellation_rolls_back_before_caller_commit(self) -> None: + connection = self.conn + previous = self.stored() + + class Interrupted: + def execute(self, sql: str, *args: object) -> sqlite3.Cursor: + return connection.execute(sql, *args) + + def executemany(self, sql: str, values: list[tuple]) -> sqlite3.Cursor: + connection.execute(sql, values[0]) + raise KeyboardInterrupt("Synthetic cancellation") + + with self.assertRaisesRegex(KeyboardInterrupt, "Synthetic cancellation"): + self.predictive.build_surprise_index(Interrupted()) + self.conn.commit() + self.assertEqual(self.stored(), previous) + + def test_release_denial_restores_old_index_before_later_commit(self) -> None: + previous = self.stored() + self.conn.execute("UPDATE messages SET content='okay' WHERE id=1") + self.conn.commit() + self.assertNotEqual(self.reference()[1], previous) + for caller_owned in (False, True): + with self.subTest(caller_owned=caller_owned): + if caller_owned: + self.conn.execute("INSERT INTO notes VALUES (1,'caller release test')") + releases = [] + + def deny_first_release( + action: int, operation: str | None, name: str | None, + _database: str | None, _source: str | None, + ) -> int: + if (action == sqlite3.SQLITE_SAVEPOINT and operation == "RELEASE" + and name == "truememory_surprise_publish"): + releases.append(operation) + if len(releases) == 1: + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + + self.conn.set_authorizer(deny_first_release) + try: + with self.assertRaises(sqlite3.DatabaseError): + self.predictive.build_surprise_index(self.conn) + finally: + # Disabling with None is supported only on Python 3.11+. + self.conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.assertEqual(len(releases), 2) + self.assertEqual(self.conn.in_transaction, caller_owned) + self.assertEqual(self.stored(), previous) + self.conn.commit() + other = self.other() + self.assertEqual(self.stored(other), previous) + self.assertEqual(other.execute("SELECT COUNT(*) FROM notes").fetchone()[0], int(caller_owned)) + + def test_release_cancellation_restores_old_index_before_later_commit(self) -> None: + connection = self.conn + previous = self.stored() + self.conn.execute("UPDATE messages SET content='okay' WHERE id=1") + self.conn.commit() + self.assertNotEqual(self.reference()[1], previous) + for caller_owned in (False, True): + with self.subTest(caller_owned=caller_owned): + if caller_owned: + self.conn.execute("INSERT INTO notes VALUES (1,'caller cancellation test')") + + class InterruptedRelease: + releases = 0 + + def execute(self, sql: str, *args: object) -> sqlite3.Cursor: + if sql == "RELEASE SAVEPOINT truememory_surprise_publish": + self.releases += 1 + if self.releases == 1: + raise KeyboardInterrupt("Synthetic release cancellation") + return connection.execute(sql, *args) + + def executemany(self, sql: str, values: list[tuple]) -> sqlite3.Cursor: + return connection.executemany(sql, values) + + interrupted = InterruptedRelease() + with self.assertRaisesRegex(KeyboardInterrupt, "Synthetic release cancellation"): + self.predictive.build_surprise_index(interrupted) + self.assertEqual(interrupted.releases, 2) + self.assertEqual(self.conn.in_transaction, caller_owned) + self.assertEqual(self.stored(), previous) + self.conn.commit() + other = self.other() + self.assertEqual(self.stored(other), previous) + self.assertEqual(other.execute("SELECT COUNT(*) FROM notes").fetchone()[0], int(caller_owned)) + + def test_caller_transaction_success_remains_uncommitted_and_can_rollback(self) -> None: + previous = self.stored() + self.conn.execute("INSERT INTO notes VALUES (1,'uncommitted caller work')") + self.conn.execute("UPDATE messages SET content='Changed caller evidence' WHERE id=1") + self.predictive.build_surprise_index(self.conn) + self.assertTrue(self.conn.in_transaction) + other = self.other() + self.assertEqual(other.execute("SELECT COUNT(*) FROM notes").fetchone()[0], 0) + self.assertEqual(self.stored(other), previous) + self.conn.rollback() + self.assertEqual(self.stored(), previous) + self.assertEqual(self.conn.execute("SELECT COUNT(*) FROM notes").fetchone()[0], 0) + + def test_caller_transaction_failure_keeps_unrelated_writes_for_caller_commit(self) -> None: + previous = self.stored() + self.fail_second_insert() + self.conn.execute("INSERT INTO notes VALUES (1,'caller owns this write')") + with self.assertRaisesRegex(sqlite3.IntegrityError, "synthetic insert failure"): + self.predictive.build_surprise_index(self.conn) + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.stored(), previous) + self.conn.commit() + other = self.other() + self.assertEqual(other.execute("SELECT COUNT(*) FROM notes").fetchone()[0], 1) + self.assertEqual(self.stored(other), previous) + + def test_stale_caller_wal_snapshot_is_not_restarted_or_committed(self) -> None: + self.conn.execute("BEGIN") + source = self.conn.execute("SELECT content FROM messages WHERE id=1").fetchone() + other = self.other() + other.execute("UPDATE messages SET content='Concurrent corrected evidence' WHERE id=1") + other.commit() + previous = self.stored(other) + with self.assertRaises(sqlite3.OperationalError): + self.predictive.build_surprise_index(self.conn) + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.conn.execute("SELECT content FROM messages WHERE id=1").fetchone(), source) + self.conn.rollback() + self.assertEqual(self.stored(other), previous) + expected, stored = self.reference() + self.assertEqual(self.predictive.build_surprise_index(self.conn), expected) + self.assertEqual(self.stored(), stored) + + def test_empty_corpus_clears_existing_orphan_scores(self) -> None: + self.conn.execute("DELETE FROM messages") + self.conn.commit() + self.conn.execute("PRAGMA foreign_keys=OFF") + self.conn.execute("INSERT INTO surprise_scores VALUES (999,0.5,1,1)") + self.conn.commit() + self.conn.execute("PRAGMA foreign_keys=ON") + self.assertEqual(self.predictive.build_surprise_index(self.conn), {}) + self.assertEqual(self.stored(), []) + + def test_schema_creation_nests_in_caller_transaction(self) -> None: + self.conn.execute("DROP TABLE surprise_scores") + self.conn.execute("INSERT INTO notes VALUES (1,'caller schema test')") + self.predictive.build_surprise_index(self.conn) + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.other().execute("SELECT COUNT(*) FROM notes").fetchone()[0], 0) + self.conn.rollback() + self.assertIsNone(self.conn.execute( + "SELECT name FROM sqlite_master WHERE name='surprise_scores'" + ).fetchone()) + + def test_schema_helper_never_commits_unrelated_caller_work(self) -> None: + self.conn.execute("INSERT INTO notes VALUES (1,'caller helper test')") + self.predictive._ensure_surprise_table(self.conn) + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.other().execute("SELECT COUNT(*) FROM notes").fetchone()[0], 0) + self.conn.rollback() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-telemetry-disclosure-760.py b/tests/test-telemetry-disclosure-760.py new file mode 100644 index 00000000..f771b0ba --- /dev/null +++ b/tests/test-telemetry-disclosure-760.py @@ -0,0 +1,203 @@ +"""Verify the disclosed telemetry payload using synthetic identity and transport.""" +from __future__ import annotations + +import hashlib +import json +import os +from contextlib import AbstractContextManager +from pathlib import Path +import sys +import tempfile +import types +import unittest +from typing import TypeVar +from unittest.mock import Mock, patch + + +ROOT = Path(__file__).resolve().parents[1] +SOURCE = ROOT / "truememory" / "telemetry.py" +SYNTHETIC_EMAIL = "synthetic@example.invalid" +SYNTHETIC_UUID = "00000000-0000-4000-8000-000000000760" +T = TypeVar("T") + + +def load_telemetry() -> types.ModuleType: + # Loading this one file avoids package startup, models and live MCP config. + module = types.ModuleType("synthetic_telemetry_760") + exec(compile(SOURCE.read_text(encoding="utf-8"), str(SOURCE), "exec"), module.__dict__) + return module + + +class TelemetryDisclosure(unittest.TestCase): + def setUp(self) -> None: + directory = tempfile.TemporaryDirectory(prefix="synthetic-telemetry-760-") + self.addCleanup(directory.cleanup) + self.home = Path(directory.name) + self.module = load_telemetry() + self.module.Path = Mock(wraps=Path) + self.module.Path.home.return_value = self.home + self.module.get_device_id = Mock(return_value="synthetic-device") + self.module._save_user_id = Mock() + self.module._get_version = Mock(return_value="0.0.760") + self.module.uuid = types.SimpleNamespace(uuid4=Mock(return_value=SYNTHETIC_UUID)) + self.module.sys = types.SimpleNamespace(platform="synthetic-os") + self.module.platform = types.SimpleNamespace( + machine=lambda: "synthetic-arch", python_version=lambda: "3.10.0", + ) + self.module.time = types.SimpleNamespace( + time=lambda: 1000.0, monotonic=lambda: 100.0, + ) + # Record requested threads, but execute neither transport nor flush loop. + self.thread_factory = Mock() + self.module.threading = types.SimpleNamespace(Thread=self.thread_factory) + response = types.SimpleNamespace(json=lambda: {}) + self.post = Mock(return_value=response) + transport = types.ModuleType("httpx") + transport.post = self.post + self.enter(patch.dict(sys.modules, {"httpx": transport})) + self.enter(patch.dict(os.environ, {"TRUEMEMORY_TELEMETRY": ""})) + + def enter(self, context: AbstractContextManager[T]) -> T: + result = context.__enter__() + self.addCleanup(context.__exit__, None, None, None) + return result + + def config(self, **extra: object) -> dict: + return {"user_id": SYNTHETIC_UUID, "tier": "base", **extra} + + def assert_disabled(self) -> None: + config = self.config(email=SYNTHETIC_EMAIL) + self.assertIsNone(self.module.init(config)) + self.module.track("synthetic-event", {"synthetic": True}) + self.module.identify(SYNTHETIC_EMAIL, {"tier": "base"}) + + @self.module.tracked("tool_synthetic") + def synthetic_tool(value: str) -> str: + return value + + self.assertEqual(synthetic_tool("synthetic-result"), "synthetic-result") + self.assertIsNone(self.module._flush_sync()) + self.assertFalse(self.module.is_enabled()) + self.assertEqual(self.module._session_events, []) + self.post.assert_not_called() + self.thread_factory.assert_not_called() + self.module.get_device_id.assert_not_called() + self.module._save_user_id.assert_not_called() + self.module.uuid.uuid4.assert_not_called() + + def test_session_start_exact_disclosed_fields_and_transport(self) -> None: + self.module.init(self.config( + email=SYNTHETIC_EMAIL, + api_key="fake-secret-marker", + memory="synthetic-memory-marker", + path="synthetic-path-marker", + )) + expected = { + "event": "session_start", "user_id": SYNTHETIC_UUID, + "timestamp": 1000.0, + "properties": { + "tier": "base", "version": "0.0.760", "platform": "synthetic-os", + "arch": "synthetic-arch", "python": "3.10.0", + "device_id": "synthetic-device", "email": SYNTHETIC_EMAIL, + }, + } + self.assertEqual(self.module._session_events, [expected]) + self.assertEqual(self.thread_factory.call_count, 2) + self.module._flush_sync() + self.post.assert_called_once_with( + "https://telemetry-api-production-c2a3.up.railway.app/v1/events", + json={"events": [expected]}, timeout=3, + ) + self.assertEqual(self.module._session_events, []) + + def test_email_is_included_on_each_configured_session_start(self) -> None: + config = self.config(email=SYNTHETIC_EMAIL) + self.module.init(config) + self.module.init(config) + self.assertEqual(len(self.module._session_events), 2) + for event in self.module._session_events: + self.assertEqual(event["properties"]["email"], SYNTHETIC_EMAIL) + self.assertEqual(event["user_id"], SYNTHETIC_UUID) + self.module.uuid.uuid4.assert_not_called() + + def test_unconfigured_email_is_absent(self) -> None: + self.module.init(self.config()) + props = self.module._session_events[0]["properties"] + self.assertEqual(set(props), {"tier", "version", "platform", "arch", "python", "device_id"}) + + def test_generated_user_uuid_is_persisted_and_reused(self) -> None: + config = {"tier": "edge"} + self.module.init(config) + self.assertEqual(config["user_id"], SYNTHETIC_UUID) + self.module._save_user_id.assert_called_once_with(config) + self.module.init(config) + self.module.uuid.uuid4.assert_called_once() + self.module._save_user_id.assert_called_once() + self.assertEqual({event["user_id"] for event in self.module._session_events}, {SYNTHETIC_UUID}) + + def test_environment_disable_prevents_enqueue_identity_and_transport(self) -> None: + for value in ("off", "false", "0", "no", "OFF", "False", "NO"): + with self.subTest(value=value), patch.dict(os.environ, {"TRUEMEMORY_TELEMETRY": value}): + self.module._enabled = None + self.assert_disabled() + self.module.Path.home.assert_not_called() + + def test_config_disable_prevents_enqueue_identity_and_transport(self) -> None: + config_dir = self.home / ".truememory" + config_dir.mkdir() + (config_dir / "config.json").write_text('{"telemetry": false}', encoding="utf-8") + self.assert_disabled() + + def test_tool_events_exclude_inputs_outputs_and_exception_text(self) -> None: + self.module.init(self.config()) + self.module._session_events.clear() + + @self.module.tracked("tool_search") + def search(query: str, **options: str) -> dict: + return {"content": "synthetic-result-marker", "query": query, **options} + + @self.module.tracked("tool_store") + def store(content: str) -> None: + raise ValueError("synthetic-exception-marker " + content) + + result = search("synthetic-query-marker", path="synthetic-path-marker", api_key="fake-key-marker") + self.assertEqual(result["content"], "synthetic-result-marker") + with self.assertRaisesRegex(ValueError, "synthetic-exception-marker"): + store("synthetic-memory-marker") + self.module._flush_sync() + events = self.post.call_args.kwargs["json"]["events"] + self.assertEqual(events, [ + {"event": "tool_search", "user_id": SYNTHETIC_UUID, "timestamp": 1000.0, + "properties": {"latency_ms": 0.0, "success": True}}, + {"event": "tool_store", "user_id": SYNTHETIC_UUID, "timestamp": 1000.0, + "properties": {"latency_ms": 0.0, "success": False}}, + ]) + self.assertNotIn("-marker", json.dumps(events)) + + def test_identity_event_has_email_tier_and_common_envelope(self) -> None: + self.module.init(self.config()) + self.module._session_events.clear() + self.module.identify(SYNTHETIC_EMAIL, {"tier": "base"}) + self.assertEqual(self.module._session_events, [{ + "event": "identify", "user_id": SYNTHETIC_UUID, "timestamp": 1000.0, + "properties": {"email": SYNTHETIC_EMAIL, "tier": "base"}, + }]) + + def test_device_hash_is_stable_across_fresh_module_instances(self) -> None: + raw = "synthetic-machine-identifier-760" + expected = hashlib.sha256(raw.encode()).hexdigest()[:16] + for _ in range(2): + module = load_telemetry() + module.sys = types.SimpleNamespace(platform="linux") + machine_file = Mock() + machine_file.exists.return_value = True + machine_file.read_text.return_value = raw + module.Path = Mock(return_value=machine_file) + self.assertEqual(module.get_device_id(), expected) + self.assertEqual(module.get_device_id(), expected) + module.Path.assert_called_once_with("/etc/machine-id") + machine_file.read_text.assert_called_once() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-throttle-sampling-742.py b/tests/test-throttle-sampling-742.py new file mode 100644 index 00000000..5ab3395d --- /dev/null +++ b/tests/test-throttle-sampling-742.py @@ -0,0 +1,392 @@ +"""Bounded admission tests with synthetic sensors, clocks, and no model imports.""" +from __future__ import annotations + +import ast +import copy +import subprocess +import threading +import types +import unittest +from pathlib import Path +from unittest.mock import Mock, patch + + +SOURCE = Path(__file__).resolve().parents[1] / "truememory" / "tier_switch" + + +def load_source(name: str, dependencies: dict | None = None) -> types.ModuleType: + """Execute production code, replacing only package imports and host RAM reads.""" + source = ast.parse((SOURCE / f"{name}.py").read_text(encoding="utf-8")) + source.body = [node for node in source.body if not ( + isinstance(node, ast.ImportFrom) and (node.module or "").startswith("truememory.") + or isinstance(node, ast.Import) and any(alias.name == "psutil" for alias in node.names) + )] + module = types.ModuleType("synthetic_" + name) + module.__dict__.update(dependencies or {}) + exec(compile(ast.fix_missing_locations(source), name + ".py", "exec"), module.__dict__) + return module + + +class FakeClock: + def __init__(self) -> None: + self.now = -1.0 + self.wall = 5000.0 + self.wall_reads = 0 + self.sleeps: list[float] = [] + + def monotonic(self) -> float: + return self.now + + def time(self) -> float: + self.wall_reads += 1 + return self.wall + + def sleep(self, seconds: float) -> None: + self.sleeps.append(seconds) + self.now += seconds + self.wall += seconds + + +def readings(status: str = "ok") -> dict: + return { + "mps_level": {"used_gb": 1.0, "ratio": 0.1, "status": status}, + "growth_rate": {"slope_gb_per_20s": 0.0, "slope_pct": 0.0, "status": status}, + "thermal": {"scheduler_limit": 100, "status": status}, + } + + +class TestThrottleSampling(unittest.TestCase): + def setUp(self) -> None: + self.clock = FakeClock() + self.sensors = load_source("sensors") + self.state = load_source("state_machine") + self.throttler_module = load_source("throttler", { + "psutil": types.SimpleNamespace(virtual_memory=lambda: types.SimpleNamespace(total=32 * 1024**3)), + "GrowthRateTracker": self.sensors.GrowthRateTracker, + "read_mps_memory": self.sensors.read_mps_memory, + "read_thermal_pressure": self.sensors.read_thermal_pressure, + "ThrottlerStateMachine": self.state.ThrottlerStateMachine, + }) + for module in (self.sensors, self.state, self.throttler_module): + module.time = self.clock + self.throttler = self.throttler_module.DynamicThrottler("mps") + self.observe = Mock(side_effect=lambda: readings()) + self.throttler._read_all_channels = self.observe + + def sample_at(self, seconds: float, snapshot: dict | None = None) -> tuple[int, dict]: + self.clock.now = seconds + self.observe.side_effect = lambda: copy.deepcopy(snapshot if snapshot is not None else readings()) + return self.throttler.before_batch() + + def warmup_before_first_sample(self) -> None: + for _ in range(4): + self.throttler.before_batch() + self.observe.assert_not_called() + + def test_original_fifteen_admissions_have_only_existing_short_pacing(self) -> None: + for _ in range(15): + size, _metrics = self.throttler.before_batch() + self.assertEqual(size, 1) + self.assertEqual(self.observe.call_count, 3) + self.assertEqual(len(self.clock.sleeps), 15) + self.assertAlmostEqual(sum(self.clock.sleeps), 15 * (0.05 + 1 * 0.02)) + self.assertLess(max(self.clock.sleeps), 1.0) + self.assertEqual(len(self.throttler._samples), 1) + self.assertEqual(self.clock.wall_reads, 0) + + def test_ramp_requires_three_distinct_fresh_observations_spanning_twenty_seconds(self) -> None: + self.warmup_before_first_sample() + self.assertEqual(self.sample_at(0)[0], 1) + self.assertEqual(self.sample_at(9)[0], 1) + self.assertEqual(self.sample_at(10)[0], 1) + self.assertEqual(self.sample_at(20)[0], 3) + self.assertEqual(self.throttler.state_machine.good_streak, 0) + self.assertEqual(len(self.throttler._samples), 0) + self.assertEqual(self.throttler.state_machine.last_ramp_time, 20) + + def test_pending_ramp_collects_one_due_observation_without_waiting_for_more_batches(self) -> None: + for _ in range(15): + self.throttler.before_batch() + initial_samples = self.observe.call_count + self.clock.now += 10 + self.throttler.before_batch() + self.assertEqual(self.observe.call_count, initial_samples + 1) + self.assertEqual(self.throttler.batch_size, 1) + self.clock.now += 10 + self.throttler.before_batch() + self.assertEqual(self.throttler.batch_size, 3) + + def test_idle_gap_discards_old_good_streak_and_cannot_catch_up_to_a_ramp(self) -> None: + self.warmup_before_first_sample() + self.sample_at(0) + self.sample_at(10) + self.assertEqual(self.sample_at(100)[0], 1) + self.assertEqual(self.observe.call_count, 3) + self.assertEqual(len(self.throttler._samples), 1) + self.assertEqual(self.throttler.state_machine.good_streak, 1) + self.assertEqual(self.sample_at(110)[0], 1) + self.assertEqual(self.sample_at(120)[0], 3) + + def test_unknown_warning_and_critical_clear_ramp_evidence(self) -> None: + for status, expected_size in (("unknown", 8), ("warning", 6), ("critical", 4)): + with self.subTest(status=status): + self.setUp() + self.throttler.batch_size = 8 + self.clock.now = -2 + self.warmup_before_first_sample() + self.sample_at(0) + self.sample_at(10) + snapshot = readings() + snapshot["thermal"] = {"status": status} + size, metrics = self.sample_at(20, snapshot) + self.assertEqual(size, expected_size) + self.assertEqual(metrics["sensor_status"]["thermal"], status) + self.assertEqual(len(self.throttler._samples), 0) + self.assertEqual(self.throttler.state_machine.good_streak, 0) + + def test_ramp_and_backoff_cooldowns_keep_existing_120_second_threshold(self) -> None: + state = self.throttler.state_machine + self.warmup_before_first_sample() + self.sample_at(0) + self.sample_at(10) + self.sample_at(20) + for seconds in range(30, 131, 10): + self.sample_at(seconds) + self.clock.now = 139.99 + self.clock.wall += 10000 + self.assertFalse(state.should_ramp_check()) + self.clock.now = 140 + self.clock.wall -= 20000 + self.assertTrue(state.should_ramp_check()) + self.throttler.on_oom() + for seconds in range(150, 251, 10): + self.sample_at(seconds) + self.clock.now = 259.99 + self.assertFalse(state.should_ramp_check()) + self.clock.now = 260 + self.assertTrue(state.should_ramp_check()) + self.assertEqual(self.clock.wall_reads, 0) + + def test_ordinary_ten_second_admissions_ramp_with_fresh_bounded_history(self) -> None: + increases: list[tuple[float, int]] = [] + previous = 1 + for index in range(120): + size, metrics = self.sample_at(index * 10) + if size > previous: + increases.append((index * 10, size)) + previous = size + self.assertLessEqual(len(self.throttler._samples), 3) + self.assertLessEqual(self.throttler.batch_size, 16) + if index >= 4: + self.assertFalse(metrics["sample_stale"]) + self.assertLess(metrics["sample_age_seconds"], 1) + self.assertEqual(increases, [(60, 3), (180, 5), (300, 7), (420, 9), + (540, 11), (660, 13), (780, 15), (900, 16)]) + self.assertEqual(self.observe.call_count, 116) + self.assertLessEqual(max(self.clock.sleeps), 0.05 + 16 * 0.02) + + def test_unsupported_channels_are_visible_and_no_telemetry_cannot_enable_ramp(self) -> None: + self.warmup_before_first_sample() + unsupported = {channel: {"status": "unsupported", "required": False} for channel in readings()} + for seconds in (0, 10, 20, 140): + size, metrics = self.sample_at(seconds, unsupported) + self.assertEqual(size, 1) + self.assertEqual(metrics["good_streak"], 0) + self.assertEqual(set(metrics["sensor_status"].values()), {"unsupported"}) + self.assertIsNone(metrics["thermal_limit"]) + + def test_cpu_can_use_real_thermal_evidence_without_probing_mps(self) -> None: + throttler = self.throttler_module.DynamicThrottler("cpu") + self.throttler_module.read_mps_memory = Mock(side_effect=AssertionError("MPS must not be probed")) + self.throttler_module.read_thermal_pressure = Mock(return_value={"status": "ok", "scheduler_limit": 100}) + for seconds in range(0, 61, 10): + self.clock.now = seconds + size, metrics = throttler.before_batch() + self.assertEqual(size, 3) + self.throttler_module.read_mps_memory.assert_not_called() + self.assertEqual(metrics["sensor_status"], { + "mps_level": "unsupported", "growth_rate": "unsupported", "thermal": "ok", + }) + + def test_adaptive_applicability_preserves_pending_mps_and_darwin_policy(self) -> None: + for platform in ("linux", "win32", "darwin"): + for device in ("cpu", "cuda:0", "mps"): + with self.subTest(platform=platform, device=device): + throttler = self.throttler_module.DynamicThrottler(device) + throttler._budget_snapshot = Mock(return_value=None) + with patch.object(self.throttler_module, "sys", types.SimpleNamespace(platform=platform)): + self.assertEqual(throttler.adaptive_applicable, platform == "darwin" or device == "mps") + + def test_published_process_budget_reactivates_policy_for_either_non_mps_model(self) -> None: + for device in ("cpu", "cuda:0"): + with self.subTest(device=device): + throttler = self.throttler_module.DynamicThrottler(device) + throttler._budget_snapshot = Mock(return_value=None) + with patch.object(self.throttler_module, "sys", types.SimpleNamespace(platform="linux")): + self.assertFalse(throttler.adaptive_applicable) + throttler._budget_snapshot.return_value = object() + self.assertTrue(throttler.adaptive_applicable) + + def test_sensor_errors_are_unknown_and_do_not_reuse_previous_healthy_value(self) -> None: + throttler = self.throttler_module.DynamicThrottler("mps") + self.throttler_module.read_mps_memory = Mock(side_effect=RuntimeError("synthetic sensor failure")) + self.throttler_module.read_thermal_pressure = Mock(side_effect=OSError("synthetic unavailable sensor")) + for _ in range(5): + size, metrics = throttler.before_batch() + self.assertEqual(size, 1) + self.assertEqual(set(metrics["sensor_status"].values()), {"unknown"}) + self.assertIsNone(metrics["mps_used_gb"]) + self.assertIsNone(metrics["thermal_limit"]) + + def test_single_flight_sampler_does_not_hold_state_lock_or_delay_other_admissions(self) -> None: + started, release = threading.Event(), threading.Event() + errors: list[Exception] = [] + + def blocked_sensor() -> dict: + started.set() + if not release.wait(timeout=2): + raise RuntimeError("synthetic sampler was not released") + return readings() + + def admit() -> None: + try: + self.throttler.before_batch() + except Exception as error: + errors.append(error) + + self.warmup_before_first_sample() + self.observe.side_effect = blocked_sensor + observer = threading.Thread(target=admit) + observer.start() + try: + self.assertTrue(started.wait(timeout=2)) + size, metrics = self.throttler.before_batch() + self.assertEqual(size, 1) + self.assertTrue(metrics["sample_pending"]) + self.assertTrue(metrics["sample_stale"]) + self.throttler.after_batch(2, 0.1) + self.assertEqual(self.throttler.items_processed, 2) + self.assertEqual(self.observe.call_count, 1) + finally: + release.set() + observer.join(timeout=3) + self.assertFalse(observer.is_alive()) + self.assertEqual(errors, []) + self.assertFalse(self.throttler._sample_lock.locked()) + + def test_oom_during_sensor_probe_invalidates_prefault_ramp_evidence(self) -> None: + self.throttler.batch_size = 8 + self.clock.now = -2 + self.warmup_before_first_sample() + self.sample_at(0) + self.sample_at(10) + self.clock.now = 20 + + def fault_during_probe() -> dict: + self.throttler.on_oom() + return readings() + + self.observe.side_effect = fault_during_probe + size, _metrics = self.throttler.before_batch() + self.assertEqual(size, 4) + self.assertEqual(self.throttler.state_machine.state, self.state.ThrottlerStateMachine.BACKOFF) + self.assertEqual(self.throttler.state_machine.good_streak, 0) + self.assertEqual(len(self.throttler._samples), 0) + self.assertEqual(self.throttler._last_sample_time, 10) + + def test_sampler_exception_releases_single_flight_gate(self) -> None: + self.warmup_before_first_sample() + self.observe.side_effect = ValueError("synthetic unexpected observation failure") + with self.assertRaises(ValueError): + self.throttler.before_batch() + self.assertFalse(self.throttler._sample_lock.locked()) + self.observe.side_effect = lambda: readings() + self.assertEqual(self.throttler.before_batch()[0], 1) + + def test_concurrent_size_changes_cannot_bypass_pacing_or_backoff(self) -> None: + for initial, changed, expected in ((1, 8, 1), (8, 4, 4)): + with self.subTest(initial=initial, changed=changed): + self.throttler.batch_size = initial + + def change_during_pacing(seconds: float) -> None: + self.assertAlmostEqual(seconds, 0.05 + initial * 0.02) + self.throttler.batch_size = changed + + with patch.object(self.clock, "sleep", side_effect=change_during_pacing): + size, metrics = self.throttler.before_batch() + self.assertEqual(size, expected) + self.assertEqual(metrics["batch_size"], expected) + + def test_growth_uses_monotonic_time_and_needs_two_valid_observations(self) -> None: + tracker = self.sensors.GrowthRateTracker(12.0) + self.clock.now = 0 + self.assertEqual(tracker.update(5.0)["status"], "unknown") + self.clock.now = 20 + self.clock.wall -= 10000 + self.assertEqual(tracker.update(5.7)["status"], "warning") + self.assertEqual(tracker.update(5.8)["status"], "unknown") + self.assertEqual(self.clock.wall_reads, 0) + + def test_bounded_thermal_probe_returns_unknown_on_timeout_or_invalid_output(self) -> None: + for result in ( + subprocess.TimeoutExpired("pmset", 0.25), + subprocess.CompletedProcess([], 1, "CPU_Scheduler_Limit = 100\n"), + subprocess.CompletedProcess([], 0, ""), + subprocess.CompletedProcess([], 0, "CPU_Scheduler_Limit = invalid\n"), + subprocess.CompletedProcess([], 0, "CPU_Scheduler_Limit = 101\n"), + subprocess.CompletedProcess([], 0, "CPU_Scheduler_Limit = 100\nCPU_Speed_Limit = invalid\n"), + subprocess.CompletedProcess([], 0, "CPU_Scheduler_Limit = 100\nCPU_Speed_Limit\n"), + ): + with self.subTest(result=type(result).__name__), patch.object(self.sensors.sys, "platform", "darwin"): + with patch.object(self.sensors.subprocess, "run") as run: + if isinstance(result, Exception): + run.side_effect = result + else: + run.return_value = result + observed = self.sensors.read_thermal_pressure() + self.assertEqual(observed["status"], "unknown") + self.assertIsNone(observed["scheduler_limit"]) + self.assertEqual(run.call_args.kwargs["timeout"], 0.25) + + def test_thermal_uses_most_restrictive_limit_independent_of_field_order(self) -> None: + for speed_limit, status in ((100, "ok"), (85, "warning"), (60, "critical")): + fields = ["CPU_Scheduler_Limit = 100", f"CPU_Speed_Limit = {speed_limit}"] + for lines in (fields, list(reversed(fields))): + with self.subTest(lines=lines), patch.object(self.sensors.sys, "platform", "darwin"): + result = subprocess.CompletedProcess([], 0, "\n".join(lines)) + with patch.object(self.sensors.subprocess, "run", return_value=result): + observed = self.sensors.read_thermal_pressure() + self.assertEqual(observed, {"scheduler_limit": speed_limit, "status": status}) + + def test_malformed_thermal_field_cannot_hide_valid_backoff_evidence(self) -> None: + fields = ("CPU_Scheduler_Limit", "CPU_Speed_Limit") + malformed_suffixes = (" = invalid", " = 101", " = -1", " =", " = = 60", "") + for valid_limit, status, expected_batch in ((60, "critical", 4), (85, "warning", 6), (100, "unknown", 8)): + for valid_field, invalid_field in (fields, tuple(reversed(fields))): + for suffix in malformed_suffixes: + lines = [f"{valid_field} = {valid_limit}", invalid_field + suffix] + for ordered in (lines, list(reversed(lines))): + with self.subTest(lines=ordered), patch.object(self.sensors.sys, "platform", "darwin"): + result = subprocess.CompletedProcess([], 0, "\n".join(ordered)) + with patch.object(self.sensors.subprocess, "run", return_value=result): + observed = self.sensors.read_thermal_pressure() + expected_limit = valid_limit if status != "unknown" else None + self.assertEqual(observed, {"scheduler_limit": expected_limit, "status": status}) + state = self.state.ThrottlerStateMachine(8, 16, 2) + state.good_streak = 2 + snapshot = readings() + snapshot["thermal"] = observed + self.assertEqual(state.safety_check(snapshot), expected_batch) + self.assertEqual(state.good_streak, 0) + + def test_unsupported_thermal_platforms_never_spawn_pmset(self) -> None: + for platform in ("linux", "win32"): + with self.subTest(platform=platform), patch.object(self.sensors.sys, "platform", platform): + with patch.object(self.sensors.subprocess, "run") as run: + observed = self.sensors.read_thermal_pressure() + run.assert_not_called() + self.assertEqual(observed, {"scheduler_limit": None, "status": "unsupported", "required": False}) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-tier-activation-journal-795.py b/tests/test-tier-activation-journal-795.py new file mode 100644 index 00000000..647be2c1 --- /dev/null +++ b/tests/test-tier-activation-journal-795.py @@ -0,0 +1,645 @@ +"""Database journal faults with synthetic in-memory SQL and no model imports.""" + +from __future__ import annotations + +import builtins +import dataclasses +import json +import os +import runpy +import sqlite3 +import struct +import sys +import types +import unittest +from pathlib import Path +from unittest.mock import patch + +BASE = runpy.run_path(str(Path(__file__).with_name("test-tier-job-identity-795.py"))) +ROOT = Path(__file__).resolve().parents[1] +TABLES = ("vec_messages_custom", "vec_messages_sep_custom") + + +class Rows: + def __init__(self, values: list[tuple]) -> None: + self.values = values + + def fetchone(self): + return self.values[0] if self.values else None + + +class JournalConnection(BASE["MockFileConnection"]): + fake_vec_schema = True + vec_declaration: str | None = None + armed = False + fault_contains: str | None = None + fault: BaseException | None = None + commit_mode = "" + rollback_mode = "" + commits = 0 + rollbacks = 0 + record_payload_reads = 0 + + def execute(self, sql: str, parameters: tuple = ()): + if sql == "SELECT value FROM main.metadata WHERE key=?" and parameters[0] in { + "tier_selected_v1", "tier_activation_v1", + }: + self.record_payload_reads += 1 + if self.armed and self.fault_contains is not None and self.fault_contains in sql: + raise self.fault + if (self.fake_vec_schema and sql == "SELECT sql FROM main.sqlite_master WHERE name=? COLLATE NOCASE" + and parameters[0] in TABLES): + ddl = self.vec_declaration or ( + f'CREATE VIRTUAL TABLE "{parameters[0]}" USING vec0(embedding float[2] distance_metric=cosine)' + ) + return Rows([(ddl,)]) + return super().execute(sql, parameters) + + def commit(self) -> None: + self.commits += 1 + if self.armed and self.commit_mode == "before": + raise self.fault + if self.armed and self.commit_mode == "retained": + return + super().commit() + if self.armed and self.commit_mode == "after": + raise self.fault + + def rollback(self) -> None: + self.rollbacks += 1 + if self.armed and self.rollback_mode == "before": + raise self.fault + if self.armed and self.rollback_mode == "retained": + return + super().rollback() + if self.armed and self.rollback_mode == "after": + raise self.fault + + +class TestActivationJournal(unittest.TestCase): + native_pair = False + + def setUp(self) -> None: + self.fixture = BASE["TestTierJobIdentity"]() + self.fixture.setUp() + self.addCleanup(self.fixture.doCleanups) + self.modules = self.fixture.modules + + def safe_import(name, globals=None, locals=None, fromlist=(), level=0): + if name in {"torch", "numpy", "sentence_transformers", "model2vec", "sqlite_vec"}: + raise AssertionError("Journal imports must stay model/native free") + if name == "psutil": + return types.SimpleNamespace() + if name.startswith("truememory."): + short = name.removeprefix("truememory.") + if short in self.modules: + return self.modules[short] + raise AssertionError("Unexpected application import: " + name) + return builtins.__import__(name, globals, locals, fromlist, level) + + for name in ("tier_config", "tier_switch.cache", "tier_switch.source", "tier_switch.activation"): + module = types.ModuleType("synthetic_activation_" + name.replace(".", "_")) + sys.modules[module.__name__] = module + module.__dict__["__builtins__"] = dict(vars(builtins), __import__=safe_import) + path = ROOT / "truememory" / (name.replace(".", "/") + ".py") + exec(compile(path.read_text(encoding="utf-8"), str(path), "exec"), module.__dict__) + self.modules[name] = module + self.api = self.modules["tier_switch.activation"] + self.job_api = self.modules["tier_switch.job"] + self.source = self.modules["tier_switch.source"] + self.conn = BASE["CONNECT"](":memory:", factory=JournalConnection) + self.addCleanup(self.conn.close) + self.conn.executescript(""" + CREATE TABLE messages(id INTEGER PRIMARY KEY, content TEXT, sender TEXT, recipient TEXT, timestamp TEXT); + CREATE TABLE metadata(key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT); + CREATE TABLE vector_cache_registry ( + tier_group TEXT PRIMARY KEY, vec_table TEXT NOT NULL, sep_table TEXT NOT NULL, + last_embedded_id INTEGER DEFAULT 0, vector_count INTEGER DEFAULT 0, + model_name TEXT, embedding_dim INTEGER DEFAULT 256, last_updated REAL, created REAL + ); + CREATE TABLE old_serving_pair(embedding BLOB); + INSERT INTO old_serving_pair(rowid,embedding) VALUES(9,x'0102'); + INSERT INTO metadata(key,value) VALUES('embed_model','old-synthetic'),('embed_dim','7'); + """) + if self.native_pair: + import sqlite_vec + self.conn.fake_vec_schema = False + self.conn.enable_load_extension(True) + try: + sqlite_vec.load(self.conn) + finally: + self.conn.enable_load_extension(False) + for name in TABLES: + self.conn.execute(f'CREATE VIRTUAL TABLE "{name}" USING vec0(embedding float[2] distance_metric=cosine)') + else: + for name in TABLES: + self.conn.execute(f'CREATE TABLE "{name}"(embedding BLOB)') + self.modules["rebuild_source"].ensure_rebuild_tracking(self.conn) + self.target = self.modules["embedding_target"].EmbeddingTarget("custom", "synthetic/encoder", 2, "custom") + self.job = self.job_api.select_tier_job(self.conn, self.target) + self.reranker = "synthetic/reranker" + + def stage(self, *, expected=None, reranker=None): + return self.api.stage_activation_intent( + self.conn, self.job, expected_generation=expected, reranker_id=reranker or self.reranker, + ) + + def build(self, ids=(-3, 0, 5), *, intent=None): + if ids: + self.conn.executemany("INSERT INTO messages VALUES(?,?,?,?,?)", [ + (mid, f"synthetic-{mid}", "sender", "recipient", "2026-01-01") for mid in ids + ]) + self.conn.commit() + intent = self.stage() if intent is None else intent + plan = self.source.plan_tier_source( + self.conn, model_id=self.target.model_id, dimension=self.target.dimension, targets=self.target.tables, + ) + plan = self.source.initialize_tier_source(self.conn, plan) + while plan.manifest.consumed < plan.manifest.total: + page = self.source.read_tier_page(self.conn, plan, size=2) + count = len(page.rows) + plan, pending = self.source.publish_tier_prefix( + self.conn, plan, page, completion=[struct.pack("2f", 1, 0)] * count, + separation=[struct.pack("2f", 0, 1)] * count, + ) + self.assertEqual(pending.rows, ()) + completion = self.source.finish_tier_source(self.conn, plan) + self.assertTrue(completion.current_source_complete) + return intent, completion.plan + + def commit(self, intent, plan): + return self.api.commit_certified_selection(self.conn, intent, self.job, plan) + + def marker(self): + return self.conn.execute(f"SELECT * FROM {self.job_api._SELECTION_TABLE}").fetchone() + + def assert_old(self) -> None: + self.assertEqual(self.conn.execute("SELECT value FROM main.metadata WHERE key='embed_model'").fetchone(), ("old-synthetic",)) + self.assertEqual(self.conn.execute("SELECT value FROM main.metadata WHERE key='embed_dim'").fetchone(), ("7",)) + self.assertIsNone(self.conn.execute("SELECT value FROM main.metadata WHERE key='tier_selected_v1'").fetchone()) + self.assertEqual(self.conn.execute("SELECT * FROM old_serving_pair").fetchall(), [(b"\x01\x02",)]) + + def test_owned_certification_exact_records_one_commit_and_job_retirement(self) -> None: + intent, plan = self.build() + before = self.conn.commits + selected = self.commit(intent, plan) + self.assertEqual(self.conn.commits, before + 1) + self.assertFalse(self.conn.in_transaction) + self.assertIsNone(self.marker()) + self.assertEqual(selected.target, self.target) + self.assertEqual(selected.reranker_id, self.reranker) + self.assertEqual(selected.tables, TABLES) + self.assertEqual((selected.vector_count, selected.cursor), (3, 5)) + self.assertFalse(selected.config_acknowledged) + self.assertEqual(self.api.read_activation_state(self.conn).selection, selected) + self.assertEqual(self.api.read_activation_state(self.conn).intent.state, "db_selected") + self.assertEqual(self.conn.execute("SELECT vec_table,sep_table,last_embedded_id,vector_count,model_name,embedding_dim FROM vector_cache_registry").fetchone(), + (*TABLES, 5, 3, "synthetic/encoder", 2)) + self.assertEqual(self.conn.execute("SELECT * FROM old_serving_pair").fetchall(), [(b"\x01\x02",)]) + self.assertEqual(self.conn.execute("SELECT value FROM metadata WHERE key='embed_model'").fetchone(), ("synthetic/encoder",)) + self.assertEqual(self.conn.execute("SELECT value FROM metadata WHERE key='embed_dim'").fetchone(), ("2",)) + new_job = self.job_api.select_tier_job(self.conn, self.target) + self.assertNotEqual(new_job.job_id, self.job.job_id) + self.assertEqual(self.marker()[1], new_job.job_id) + with self.assertRaises(self.job_api.TierJobSelectionError): + self.conn.execute("BEGIN IMMEDIATE") + try: + self.job_api._retire_selected_tier_job_in_writer(self.conn, self.job) + finally: + self.conn.rollback() + self.assertEqual(self.marker()[1], new_job.job_id) + + def test_empty_source_is_certified_without_fabricated_last_id(self) -> None: + intent, plan = self.build(ids=()) + result = self.commit(intent, plan) + self.assertEqual((result.vector_count, result.cursor), (0, None)) + self.assertEqual(self.conn.execute("SELECT last_embedded_id,vector_count FROM vector_cache_registry").fetchone(), (0, 0)) + + def test_stage_is_idempotent_and_conflicting_live_intent_refuses(self) -> None: + intent = self.stage() + before = self.conn.total_changes + self.assertEqual(self.stage(), intent) + self.assertEqual(self.conn.total_changes, before) + with self.assertRaises(self.api.TierActivationError): + self.stage(reranker="synthetic/other") + self.assertEqual(self.api.read_activation_state(self.conn).intent, intent) + self.assert_old() + + def test_replacement_job_may_replace_abandoned_intent_only_after_binding(self) -> None: + first = self.stage() + self.job_api.end_selected_tier_job(self.conn, self.job, outcome="failed") + self.job = self.job_api.select_tier_job(self.conn, self.target, previous_job_id=self.job.job_id) + second = self.stage() + self.assertNotEqual(first.intent_id, second.intent_id) + self.assertEqual(second.job_id, self.job.job_id) + self.assert_old() + + def test_config_acknowledgement_is_generation_cas_and_no_runtime_claim(self) -> None: + intent, plan = self.build() + selected = self.commit(intent, plan) + with self.assertRaises(self.api.TierActivationError): + self.api.acknowledge_config(self.conn, generation="0" * 32) + self.assertFalse(self.api.read_activation_state(self.conn).selection.config_acknowledged) + acked = self.api.acknowledge_config(self.conn, generation=selected.generation) + self.assertTrue(acked.config_acknowledged) + before = self.conn.total_changes + self.assertEqual(self.api.acknowledge_config(self.conn, generation=selected.generation), acked) + self.assertEqual(self.conn.total_changes, before) + self.assertNotIn("runtime", dataclasses.asdict(acked)) + self.job = self.job_api.select_tier_job(self.conn, self.target) + self.stage(expected=selected.generation) + with self.assertRaises(self.api.TierActivationError): + self.api.acknowledge_config(self.conn, generation=selected.generation) + + def test_missing_or_wrong_expected_generation_never_overwrites_selection(self) -> None: + with self.assertRaises(self.api.TierActivationError): + self.stage(expected="0" * 32) + intent, plan = self.build() + selected = self.commit(intent, plan) + self.job = self.job_api.select_tier_job(self.conn, self.target) + for expected in (None, "0" * 32, True): + with self.subTest(expected=expected), self.assertRaises(self.api.TierActivationError): + self.stage(expected=expected) + self.assertEqual(self.api.read_activation_state(self.conn).selection, selected) + + def test_final_writer_rejects_changed_selected_generation(self) -> None: + intent, plan = self.build() + previous = self.commit(intent, plan) + self.api.acknowledge_config(self.conn, generation=previous.generation) + self.job = self.job_api.select_tier_job(self.conn, self.target) + intent = self.stage(expected=previous.generation) + plan = self.source.plan_tier_source(self.conn, model_id=self.target.model_id, + dimension=2, targets=TABLES) + changed = dataclasses.replace(previous, generation="0" * 32) + self.conn.execute("UPDATE metadata SET value=? WHERE key='tier_selected_v1'", (self.api._dump(changed),)) + self.conn.commit() + marker = self.marker() + with self.assertRaisesRegex(self.api.TierActivationError, "Selected generation changed"): + self.commit(intent, plan) + self.assertEqual(self.marker(), marker) + raw = self.conn.execute("SELECT value FROM main.metadata WHERE key='tier_selected_v1'").fetchone()[0] + self.assertEqual(self.api._parse(raw, self.api.TierSelection), changed) + with self.assertRaises(self.api.TierActivationError): + self.api.read_activation_state(self.conn) + + def test_staged_intent_must_match_current_selection_or_absence(self) -> None: + intent = self.stage() + malformed = dataclasses.replace(intent, expected_generation="0" * 32) + self.conn.execute("UPDATE metadata SET value=? WHERE key='tier_activation_v1'", (self.api._dump(malformed),)) + self.conn.commit() + with self.assertRaisesRegex(self.api.TierActivationError, "Selected generation changed"): + self.api.read_activation_state(self.conn) + self.conn.execute("UPDATE metadata SET value=? WHERE key='tier_activation_v1'", (self.api._dump(intent),)) + self.conn.commit() + intent, plan = self.build(intent=intent) + selected = self.commit(intent, plan) + for expected in (None, "0" * 32, selected.generation): + malformed = dataclasses.replace(intent, expected_generation=expected) + self.conn.execute("UPDATE metadata SET value=? WHERE key='tier_activation_v1'", (self.api._dump(malformed),)) + self.conn.commit() + with self.subTest(expected=expected), self.assertRaises(self.api.TierActivationError): + self.api.read_activation_state(self.conn) + self.assertNotEqual(selected.generation, "0" * 32) + + def test_new_intent_preserves_pending_config_acknowledgement_then_retries(self) -> None: + intent, plan = self.build() + selected = self.commit(intent, plan) + previous_state = self.api.read_activation_state(self.conn) + self.job = self.job_api.select_tier_job(self.conn, self.target) + before = self.conn.total_changes + with self.assertRaisesRegex(self.api.TierActivationError, "awaits config acknowledgement"): + self.stage(expected=selected.generation) + self.assertEqual(self.conn.total_changes, before) + self.assertEqual(self.api.read_activation_state(self.conn), previous_state) + acked = self.api.acknowledge_config(self.conn, generation=selected.generation) + self.assertTrue(acked.config_acknowledged) + next_intent = self.stage(expected=selected.generation) + self.assertEqual(next_intent.expected_generation, selected.generation) + self.assertNotEqual(next_intent.intent_id, intent.intent_id) + + def test_frozen_custom_identity_never_reresolves_configuration(self) -> None: + intent, plan = self.build() + with patch.object(self.modules["tier_config"], "get_tier_config", side_effect=AssertionError("No config reads")): + result = self.commit(intent, plan) + self.assertEqual(result.target, self.target) + self.assertEqual(result.reranker_id, self.reranker) + + def test_idempotent_intent_does_not_invalidate_existing_source_plan(self) -> None: + intent, plan = self.build() + before = self.conn.total_changes + self.assertEqual(self.stage(), intent) + self.assertEqual(self.conn.total_changes, before) + self.assertEqual(self.commit(intent, plan).manifest_generation, plan.manifest.generation) + + def test_borrowed_transactions_and_read_only_helpers_never_end_caller_work(self) -> None: + intent, plan = self.build() + self.conn.execute("BEGIN IMMEDIATE") + before = self.conn.total_changes + self.job_api.check_selected_tier_job_in_writer(self.conn, self.job) + self.source.require_current_completion_in_writer(self.conn, plan) + self.assertEqual(self.conn.total_changes, before) + self.assertTrue(self.conn.in_transaction) + for call in (lambda: self.stage(), lambda: self.commit(intent, plan), + lambda: self.api.read_activation_state(self.conn), + lambda: self.api.acknowledge_config(self.conn, generation="0" * 32)): + with self.assertRaises(self.api.TierActivationError): + call() + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.conn.total_changes, before) + self.conn.rollback() + for call in (lambda: self.job_api.check_selected_tier_job_in_writer(self.conn, self.job), + lambda: self.source.require_current_completion_in_writer(self.conn, plan)): + with self.assertRaises((self.job_api.TierJobSelectionError, self.source.TierSourceUntrusted)): + call() + + def test_post_stage_unrelated_status_write_invalidates_plan(self) -> None: + intent, plan = self.build() + self.conn.execute("INSERT INTO metadata VALUES('unrelated','status',NULL)") + self.conn.commit() + with self.assertRaises(self.source.TierSourceUntrusted): + self.commit(intent, plan) + self.assert_old() + self.assertIsNotNone(self.marker()) + + def test_writer_rechecks_source_marker_receipt_and_target(self) -> None: + cases = ( + "INSERT INTO messages VALUES(8,'tail','sender','recipient','time')", + "UPDATE messages SET content='corrected' WHERE id=0", + f"UPDATE {self.job_api._SELECTION_TABLE} SET state='cancelled'", + "UPDATE metadata SET value='{}' WHERE key LIKE 'tier_pair_receipt_v1:%'", + "UPDATE vec_messages_custom SET embedding=x'0000000000000000' WHERE rowid=0", + ) + for mutation in cases: + with self.subTest(mutation=mutation): + fixture = TestActivationJournal() + fixture.setUp() + try: + intent, plan = fixture.build() + fixture.conn.execute(mutation) + fixture.conn.commit() + with self.assertRaises((fixture.api.TierActivationError, fixture.source.TierSourceUntrusted, + fixture.job_api.TierJobSelectionError)): + fixture.commit(intent, plan) + fixture.assert_old() + finally: + fixture.doCleanups() + + def test_revalidation_rejects_same_width_corrupted_pair(self) -> None: + intent, plan = self.build() + self.conn.execute("UPDATE vec_messages_custom SET embedding=? WHERE rowid=0", (struct.pack("2f", 0, 0),)) + self.conn.commit() + # Bypass only the cheap change counter to exercise the independent final + # digest audit; no forged receipt is supplied. + plan = dataclasses.replace(plan, changes=self.conn.total_changes) + with self.assertRaises(self.source.TierSourceUntrusted): + self.commit(intent, plan) + self.assert_old() + + def test_incomplete_fabricated_and_wrong_identity_certificates_refuse(self) -> None: + intent, plan = self.build() + altered = ( + dataclasses.replace(plan, action="resume"), + dataclasses.replace(plan, digest="0" * 64), + dataclasses.replace(plan, manifest=dataclasses.replace(plan.manifest, dimension=3)), + dataclasses.replace(plan, manifest=dataclasses.replace(plan.manifest, model="synthetic/wrong")), + dataclasses.replace(plan, manifest=dataclasses.replace(plan.manifest, complete=False)), + ) + for current in altered: + with self.subTest(plan=current.manifest.dimension), self.assertRaises((self.api.TierActivationError, self.source.TierSourceUntrusted)): + self.commit(intent, current) + self.assert_old() + with self.assertRaises(self.api.TierActivationError): + self.commit(dataclasses.replace(intent, reranker_id="synthetic/changed"), plan) + + def test_schema_dimension_metric_and_ordinary_table_refuse(self) -> None: + intent, plan = self.build() + for ddl in ( + "CREATE TABLE vec_messages_custom(embedding BLOB)", + "CREATE VIRTUAL TABLE vec_messages_custom USING vec0(embedding float[3] distance_metric=cosine)", + "CREATE VIRTUAL TABLE vec_messages_custom USING vec0(embedding float[2] distance_metric=l2)", + ): + self.conn.vec_declaration = ddl + with self.subTest(ddl=ddl), self.assertRaises(self.api.TierActivationError): + self.commit(intent, plan) + self.assert_old() + + def test_trigger_side_effects_and_temp_case_aliases_refuse_before_writes(self) -> None: + for ddl in ( + "CREATE TRIGGER bad_metadata AFTER UPDATE ON metadata BEGIN UPDATE messages SET content='changed'; END", + "CREATE TRIGGER bad_registry AFTER INSERT ON vector_cache_registry BEGIN DELETE FROM messages; END", + "CREATE INDEX bad_index ON metadata(value)", + "CREATE TEMP TABLE METADATA(key TEXT,value TEXT)", + "CREATE TEMP TABLE VECTOR_CACHE_REGISTRY(tier_group TEXT)", + ): + with self.subTest(ddl=ddl): + fixture = TestActivationJournal() + fixture.setUp() + try: + intent, plan = fixture.build() + fixture.conn.execute(ddl) + before = fixture.conn.total_changes + with self.assertRaises(fixture.api.TierActivationError): + fixture.commit(intent, plan) + self.assertEqual(fixture.conn.total_changes, before) + fixture.assert_old() + finally: + fixture.doCleanups() + + def test_marker_incoming_foreign_key_refuses_even_with_enforcement_off(self) -> None: + for enforcement in (False, True): + with self.subTest(enforcement=enforcement): + fixture = TestActivationJournal() + fixture.setUp() + try: + fixture.conn.execute(f"PRAGMA foreign_keys={int(enforcement)}") + fixture.conn.execute(f"CREATE TABLE marker_child(id INTEGER REFERENCES {fixture.job_api._SELECTION_TABLE.upper()}(singleton) ON DELETE CASCADE)") + fixture.conn.execute("INSERT INTO marker_child VALUES(1)") + fixture.conn.commit() + intent, plan = fixture.build() + with self.assertRaises(fixture.job_api.TierJobSelectionError): + fixture.commit(intent, plan) + self.assertEqual(fixture.conn.execute("SELECT * FROM marker_child").fetchall(), [(1,)]) + self.assertIsNotNone(fixture.marker()) + fixture.assert_old() + finally: + fixture.doCleanups() + + def test_registry_and_metadata_upserts_do_not_delete_parent_rows(self) -> None: + self.conn.execute("PRAGMA foreign_keys=ON") + self.conn.executescript(""" + INSERT INTO vector_cache_registry(tier_group,vec_table,sep_table) VALUES('custom','old','old_sep'); + CREATE TABLE metadata_child(parent TEXT REFERENCES metadata(key) ON DELETE CASCADE); + INSERT INTO metadata_child VALUES('embed_model'); + CREATE TABLE registry_child(parent TEXT REFERENCES vector_cache_registry(tier_group) ON DELETE CASCADE); + INSERT INTO registry_child VALUES('custom'); + """) + intent, plan = self.build() + self.commit(intent, plan) + self.assertEqual(self.conn.execute("SELECT * FROM metadata_child").fetchall(), [("embed_model",)]) + self.assertEqual(self.conn.execute("SELECT * FROM registry_child").fetchall(), [("custom",)]) + + def test_marker_retirement_bounds_table_names_before_materialization(self) -> None: + self.conn.execute('CREATE TABLE "' + 'x' * 600 + '"(id INTEGER)') + intent, plan = self.build() + with self.assertRaisesRegex(self.job_api.TierJobSelectionError, "schema exceeds"): + self.commit(intent, plan) + self.assertIsNotNone(self.marker()) + self.assert_old() + + def test_each_publication_write_failure_rolls_back_marker_registry_and_journal(self) -> None: + for fault in ("DELETE FROM main.truememory_tier_selected_job_v1", "INSERT INTO vector_cache_registry", + "INSERT INTO main.metadata"): + with self.subTest(fault=fault): + fixture = TestActivationJournal() + fixture.setUp() + try: + intent, plan = fixture.build() + marker = fixture.marker() + failure = sqlite3.OperationalError("synthetic statement failure") + fixture.conn.armed, fixture.conn.fault_contains, fixture.conn.fault = True, fault, failure + with self.assertRaises(sqlite3.OperationalError) as caught: + fixture.commit(intent, plan) + self.assertIs(caught.exception, failure) + fixture.conn.armed = False + self.assertFalse(fixture.conn.in_transaction) + self.assertEqual(fixture.marker(), marker) + fixture.assert_old() + self.assertEqual(fixture.api.read_activation_state(fixture.conn).intent.state, "staged") + finally: + fixture.doCleanups() + + def test_commit_before_after_and_noop_never_fake_success(self) -> None: + for mode in ("before", "after", "retained"): + with self.subTest(mode=mode): + fixture = TestActivationJournal() + fixture.setUp() + try: + intent, plan = fixture.build() + failure = BASE["Cancelled"]("synthetic commit cancellation") + fixture.conn.armed, fixture.conn.commit_mode, fixture.conn.fault = True, mode, failure + with self.assertRaises((BASE["Cancelled"], fixture.api.TierActivationError)): + fixture.commit(intent, plan) + fixture.conn.armed = False + self.assertFalse(fixture.conn.in_transaction) + state = fixture.api.read_activation_state(fixture.conn) + if mode == "after": + self.assertIsNotNone(state.selection) + self.assertFalse(state.selection.config_acknowledged) + self.assertIsNone(fixture.marker()) + else: + fixture.assert_old() + self.assertIsNotNone(fixture.marker()) + finally: + fixture.doCleanups() + + def test_uncertain_rollback_propagates_without_followup_status_sql(self) -> None: + for mode in ("before", "after", "retained"): + with self.subTest(mode=mode): + fixture = TestActivationJournal() + fixture.setUp() + try: + intent, plan = fixture.build() + fixture.conn.armed = True + fixture.conn.fault_contains = "INSERT INTO main.metadata" + fixture.conn.fault = MemoryError("synthetic rollback failure") + fixture.conn.rollback_mode = mode + before = fixture.conn.commits + with self.assertRaises((MemoryError, fixture.api.TierActivationError)): + fixture.commit(intent, plan) + self.assertEqual(fixture.conn.commits, before) + self.assertEqual(fixture.conn.in_transaction, mode != "after") + if fixture.conn.in_transaction: + with self.assertRaises(fixture.api.TierActivationError): + fixture.api.read_activation_state(fixture.conn) + fixture.conn.armed = False + fixture.conn.rollback() + fixture.assert_old() + self.assertIsNotNone(fixture.marker()) + finally: + fixture.doCleanups() + + def test_read_snapshot_cleanup_never_retries_uncertain_rollback(self) -> None: + self.stage() + for mode in ("before", "after", "retained"): + with self.subTest(mode=mode): + failure = sqlite3.OperationalError("synthetic read rollback failure") + self.conn.armed, self.conn.rollback_mode, self.conn.fault = True, mode, failure + before = self.conn.rollbacks + try: + with self.assertRaises((sqlite3.OperationalError, self.api.TierActivationError)) as caught: + self.api.read_activation_state(self.conn) + self.assertEqual(self.conn.rollbacks, before + 1) + self.assertEqual(self.conn.in_transaction, mode != "after") + if mode != "retained": + self.assertIs(caught.exception, failure) + finally: + self.conn.armed = False + self.conn.rollback() + self.assert_old() + + def test_oversized_and_nul_tail_records_reject_before_payload_read(self) -> None: + self.stage() + for value in ("x" * 20000, "{}\x00" + "x" * 20000, b"{}"): + self.conn.execute("UPDATE metadata SET value=? WHERE key='tier_activation_v1'", (value,)) + self.conn.commit() + before = self.conn.record_payload_reads + with self.assertRaises(self.api.TierActivationError): + self.api.read_activation_state(self.conn) + self.assertEqual(self.conn.record_payload_reads, before) + + def test_strict_json_fields_types_and_frozen_identity(self) -> None: + intent = self.stage() + valid = json.loads(self.api._dump(intent)) + malformed = ["[]", "{}", '{"version":1,"version":1}', self.api._dump(intent) + "\x00"] + for key, value in (("version", True), ("unexpected", 1), ("expected_generation", False), ("state", "complete")): + item = dict(valid) + item[key] = value + malformed.append(json.dumps(item)) + for field, value in (("dimension", True), ("model_id", "x" * 513), ("tier", "pro")): + item = dict(valid, target=dict(valid["target"], **{field: value})) + malformed.append(json.dumps(item)) + for raw in malformed: + self.conn.execute("UPDATE metadata SET value=? WHERE key='tier_activation_v1'", (raw,)) + self.conn.commit() + with self.subTest(raw=raw[:30]), self.assertRaises(self.api.TierActivationError): + self.api.read_activation_state(self.conn) + with self.assertRaises(dataclasses.FrozenInstanceError): + intent.reranker_id = "changed" + + def test_selected_record_counts_tables_hashes_and_ack_are_strict(self) -> None: + intent, plan = self.build() + selected = self.commit(intent, plan) + valid = json.loads(self.api._dump(selected)) + for field, value in (("vector_count", True), ("vector_count", -1), ("vector_count", 2**63), + ("cursor", None), ("cursor", 2**63), ("config_acknowledged", 1), + ("tables", list(reversed(TABLES))), ("pair_digest", "wrong"), + ("job_id", "0" * 32), ("config_acknowledged", True)): + self.conn.execute("UPDATE metadata SET value=? WHERE key='tier_selected_v1'", (json.dumps(dict(valid, **{field: value})),)) + self.conn.commit() + with self.subTest(field=field, value=value), self.assertRaises(self.api.TierActivationError): + self.api.read_activation_state(self.conn) + + def test_registry_default_commits_and_additive_mode_preserves_borrowed_transaction(self) -> None: + registry = self.modules["tier_switch.cache"].VectorCacheRegistry + before = self.conn.commits + registry.set(self.conn, "custom", model_name="synthetic/encoder", embedding_dim=2) + self.assertEqual(self.conn.commits, before + 1) + self.conn.execute("BEGIN") + registry.set(self.conn, "custom", model_name="synthetic/other", embedding_dim=3, commit=False) + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + self.assertEqual(self.conn.execute("SELECT model_name,embedding_dim FROM vector_cache_registry").fetchone(), ("synthetic/encoder", 2)) + + +@unittest.skipUnless(os.environ.get("TRUEMEMORY_TEST_NATIVE_VEC") == "1", "Native vec0 gate belongs to GPUBox") +class TestNativeActivationJournal(unittest.TestCase): + def test_actual_vec0_certification_and_job_retirement(self) -> None: + fixture = TestActivationJournal() + fixture.native_pair = True + fixture.setUp() + self.addCleanup(fixture.doCleanups) + fixture.test_owned_certification_exact_records_one_commit_and_job_retirement() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-tier-activation-policy-795.py b/tests/test-tier-activation-policy-795.py new file mode 100644 index 00000000..d4705fb2 --- /dev/null +++ b/tests/test-tier-activation-policy-795.py @@ -0,0 +1,458 @@ +"""Policy/projection faults using synthetic config and in-memory SQLite only.""" +from __future__ import annotations + +import builtins +import dataclasses +import json +import runpy +import sys +import tempfile +import threading +import types +import unittest +from pathlib import Path +from unittest.mock import patch + +BASE = runpy.run_path(str(Path(__file__).with_name("test-tier-activation-journal-795.py"))) +ROOT = Path(__file__).resolve().parents[1] +RERANKER = "Alibaba-NLP/gte-reranker-modernbert-base" +LEGACY = ("vec_messages", "vec_messages_sep") + + +class TestActivationPolicy(unittest.TestCase): + def setUp(self) -> None: + self.fixture = BASE["TestActivationJournal"]() + self.addCleanup(self.fixture.doCleanups) + self.fixture.setUp() + self.api = self.fixture.api + self.conn = self.fixture.conn + self.jobs = self.fixture.job_api + self.source = self.fixture.source + self.Target = self.fixture.modules["embedding_target"].EmbeddingTarget + self.base = self.Target("base", "qwen3_256", 256, "basepro") + self.pro = self.Target("pro", "qwen3_256", 256, "basepro") + self.guard = self.api.ConfigGuard(True, "base", "a" * 32) + for table in (*LEGACY, *self.base.tables): + self.conn.execute(f'CREATE TABLE "{table}"(embedding BLOB)') + self.conn.execute("UPDATE metadata SET value='qwen3_256' WHERE key='embed_model'") + self.conn.execute("UPDATE metadata SET value='256' WHERE key='embed_dim'") + self.conn.commit() + schema = self.api._schema_sql + + def fake_native_schema(conn, table): + actual = schema(conn, table) + if table in (*LEGACY, *self.base.tables): + return f'CREATE VIRTUAL TABLE "{table}" USING vec0(embedding float[256] distance_metric=cosine)' + return actual + + self.schema_patch = patch.object(self.api, "_schema_sql", fake_native_schema) + self.schema_patch.start() + self.addCleanup(self.schema_patch.stop) + + projection = types.ModuleType("synthetic_policy_projection") + sys.modules[projection.__name__] = projection + self.addCleanup(sys.modules.pop, projection.__name__, None) + + def safe_import(name, globals=None, locals=None, fromlist=(), level=0): + if name == "truememory.tier_switch.activation": + return self.api + if name.startswith("truememory") or name in {"torch", "numpy", "sqlite_vec", "sentence_transformers"}: + raise AssertionError("Projection must stay model/native free") + return builtins.__import__(name, globals, locals, fromlist, level) + + projection.__dict__["__builtins__"] = dict(vars(builtins), __import__=safe_import) + path = ROOT / "truememory/tier_switch/projection.py" + exec(compile(path.read_text(encoding="utf-8"), str(path), "exec"), projection.__dict__) + self.projection = projection + + def stage(self): + return self.api.stage_activation_intent( + self.conn, self.fixture.job, expected_generation=None, reranker_id="synthetic/reranker", + expected_config=self.guard, status_id=17, + ) + + def publish(self, intent=None, **kwargs): + arguments = dict(expected_selection_generation=None, expected_intent_id=intent.intent_id if intent else None, + target=self.pro, reranker_id=RERANKER, expected_config=self.guard, legacy_pair=LEGACY) + arguments.update(kwargs) + return self.api.commit_config_only_transition(self.conn, **arguments) + + def terminal_marker(self): + self.jobs.end_selected_tier_job(self.conn, self.fixture.job, outcome="cancelled") + + def test_legacy_policy_changes_only_journal_and_exact_marker(self): + intent = self.stage() + before = self.conn.execute("SELECT key,value FROM metadata WHERE key!='tier_activation_v1' ORDER BY key").fetchall() + statements = [] + self.conn.set_trace_callback(statements.append) + result = self.publish(intent) + self.conn.set_trace_callback(None) + self.assertIs(type(result), self.api.LegacyTierPolicy) + self.assertEqual(result.tables, LEGACY) + self.assertEqual(result.target, self.pro) + self.assertFalse(result.config_acknowledged) + self.assertIsNone(self.api.read_activation_state(self.conn).selection) + self.assertEqual(self.api.read_activation_state(self.conn).legacy_policy, result) + self.assertEqual(before, self.conn.execute("SELECT key,value FROM metadata WHERE key!='tier_activation_v1' ORDER BY key").fetchall()) + self.assertEqual(self.jobs.read_selected_job_marker(self.conn).state, "cancelled") + self.assertFalse(any("FROM MAIN.MESSAGES" in sql.upper() or "FROM MAIN.VEC_MESSAGES" in sql.upper() for sql in statements)) + self.assertFalse(any("DELETE" in sql.upper() for sql in statements)) + + def test_pending_policy_ack_cas_and_roundtrip(self): + self.terminal_marker() + result = self.publish() + with self.assertRaises(self.api.TierActivationError): + self.api.acknowledge_policy_config(self.conn, generation="f" * 32) + with self.assertRaises(self.api.TierActivationError): + self.publish(expected_intent_id=result.intent_id, target=self.base, + expected_config=self.api.ConfigGuard(True, "pro", result.generation)) + acknowledged = self.api.acknowledge_policy_config(self.conn, generation=result.generation) + self.assertTrue(acknowledged.config_acknowledged) + self.assertEqual(acknowledged, self.api.acknowledge_policy_config(self.conn, generation=result.generation)) + second = self.publish(expected_intent_id=result.intent_id, target=self.base, + expected_config=self.api.ConfigGuard(True, "pro", result.generation)) + self.assertNotEqual(result.generation, second.generation) + self.assertEqual(second.target, self.base) + self.assertIsNone(self.api.read_activation_state(self.conn).selection) + + def test_v2_rebuild_retains_acknowledged_legacy_policy_and_old_v1_bytes(self): + old = self.fixture.stage() + raw = self.api._dump(old) + self.assertEqual(json.loads(raw)["version"], 1) + self.assertNotIn("expected_config", json.loads(raw)) + self.assertEqual(self.api._dump(self.api._parse(raw, self.api.ActivationIntent)), raw) + self.jobs.end_selected_tier_job(self.conn, self.fixture.job, outcome="cancelled") + policy = self.publish(old) + policy = self.api.acknowledge_policy_config(self.conn, generation=policy.generation) + job = self.jobs.select_tier_job(self.conn, self.fixture.target, previous_job_id=self.fixture.job.job_id) + guard = self.api.ConfigGuard(True, "pro", policy.generation) + staged = self.api.stage_activation_intent(self.conn, job, expected_generation=None, + reranker_id="synthetic/reranker", expected_config=guard, status_id=23) + self.assertEqual(staged.previous_policy, policy) + self.assertEqual(self.api.read_activation_state(self.conn).legacy_policy, policy) + self.assertEqual(json.loads(self.api._dump(staged))["kind"], "rebuild") + self.assertEqual(self.api._parse(self.api._dump(staged), self.api.ActivationIntent), staged) + self.assertEqual(self.api.stage_activation_intent(self.conn, job, expected_generation=None, + reranker_id="synthetic/reranker", expected_config=guard, status_id=23), staged) + with self.assertRaises(self.api.TierActivationError): + self.api.stage_activation_intent(self.conn, job, expected_generation=None, reranker_id="synthetic/reranker") + + def test_exact_superseded_job_and_rollback(self): + intent = self.stage() + original = self.jobs.read_selected_job_marker(self.conn) + self.conn.execute(f"UPDATE {self.jobs._SELECTION_TABLE} SET job_id=?", ("b" * 32,)) + self.conn.commit() + with self.assertRaises(self.jobs.TierJobSelectionError): + self.publish(intent) + self.assertEqual(self.jobs.read_selected_job_marker(self.conn).job_id, "b" * 32) + self.conn.execute(f"UPDATE {self.jobs._SELECTION_TABLE} SET job_id=?", (original.job_id,)) + self.conn.commit() + self.conn.fault_contains = "INSERT INTO main.metadata" + self.conn.fault = MemoryError("synthetic journal write") + self.conn.armed = True + with self.assertRaises(MemoryError): + self.publish(intent) + self.conn.armed = False + self.assertEqual(self.jobs.read_selected_job_marker(self.conn), original) + self.assertEqual(self.api.read_activation_state(self.conn).intent, intent) + + def test_commit_after_success_is_pending_readback_not_false_rollback(self): + intent = self.stage() + self.conn.armed = True + self.conn.commit_mode = "after" + self.conn.fault = TimeoutError("synthetic post-commit") + before = self.conn.rollbacks + with self.assertRaises(TimeoutError): + self.publish(intent) + self.conn.armed = False + self.assertEqual(self.conn.rollbacks, before) + state = self.api.read_activation_state(self.conn) + self.assertIsNotNone(state.legacy_policy) + self.assertFalse(state.legacy_policy.config_acknowledged) + self.assertEqual(self.jobs.read_selected_job_marker(self.conn).state, "cancelled") + + def test_certified_policy_keeps_certificate_without_recertifying_vectors(self): + self.terminal_marker() + self.fixture.target = self.base + self.fixture.job = self.jobs.select_tier_job(self.conn, self.base, previous_job_id=self.fixture.job.job_id) + self.fixture.reranker = RERANKER + intent, plan = self.fixture.build(ids=()) + selected = self.fixture.commit(intent, plan) + selected = self.api.acknowledge_config(self.conn, generation=selected.generation) + statements = [] + self.conn.set_trace_callback(statements.append) + result = self.publish(expected_selection_generation=selected.generation, expected_intent_id=selected.intent_id, + expected_config=self.api.ConfigGuard(True, "base", selected.generation), legacy_pair=None) + self.conn.set_trace_callback(None) + for field in dataclasses.fields(selected): + if field.name not in {"generation", "intent_id", "target", "config_acknowledged"}: + self.assertEqual(getattr(result, field.name), getattr(selected, field.name)) + self.assertNotEqual(result.generation, selected.generation) + self.assertEqual(result.target, self.pro) + self.assertIsNone(self.api.read_activation_state(self.conn).legacy_policy) + self.assertFalse(any("FROM MAIN.MESSAGES" in sql.upper() for sql in statements)) + self.assertTrue(self.api.acknowledge_config(self.conn, generation=result.generation).config_acknowledged) + + def test_strict_invalid_policy_shapes_and_no_new_certificate(self): + self.terminal_marker() + bad = [dict(target=self.fixture.target), dict(reranker_id="synthetic/other"), + dict(legacy_pair=("vec_messages_custom", "vec_messages_sep_custom")), + dict(expected_config=None), dict(target=self.base), dict(expected_intent_id="b" * 32), + dict(expected_selection_generation="b" * 32)] + for fields in bad: + with self.subTest(fields=list(fields)), self.assertRaises(self.api.TierActivationError): + self.publish(**fields) + self.assertIsNone(self.api.read_activation_state(self.conn).intent) + self.assertIsNone(self.api.read_activation_state(self.conn).selection) + + def test_legacy_registry_metadata_and_native_schema_rejection(self): + self.terminal_marker() + self.conn.execute("UPDATE metadata SET value='model2vec' WHERE key='embed_model'") + self.conn.commit() + with self.assertRaises(self.api.TierActivationError): + self.publish() + self.conn.execute("UPDATE metadata SET value='qwen3_256' WHERE key='embed_model'") + self.conn.execute("INSERT INTO vector_cache_registry(tier_group,vec_table,sep_table,model_name) VALUES('basepro','wrong','wrong','qwen3_256')") + self.conn.commit() + with self.assertRaises(self.api.TierActivationError): + self.publish() + self.schema_patch.stop() + with self.assertRaises(self.api.TierActivationError): + self.publish() + + def test_strict_nested_policy_and_nul_tail_byte_bound(self): + self.terminal_marker() + result = self.publish() + raw = self.conn.execute("SELECT value FROM metadata WHERE key='tier_activation_v1'").fetchone()[0] + value = json.loads(raw) + for field, invalid in (("result_generation", True), ("state", []), ("tables", [[], []]), + ("expected_config", {"tier": "base"}), ("unknown", 1)): + altered = dict(value, **{field: invalid}) + with self.subTest(field=field), self.assertRaises(self.api.TierActivationError): + self.api._parse(json.dumps(altered), self.api.ActivationIntent) + self.conn.execute("UPDATE metadata SET value=? WHERE key='tier_activation_v1'", (raw + "\x00" + "x" * 17000,)) + self.conn.commit() + before = self.conn.record_payload_reads + with self.assertRaises(self.api.TierActivationError): + self.api.read_activation_state(self.conn) + self.assertEqual(self.conn.record_payload_reads, before) + self.assertEqual(result.target, self.pro) + + def test_progress_is_bounded_manifest_read_and_borrows_snapshot(self): + intent, plan = self.fixture.build() + statements = [] + self.conn.set_trace_callback(statements.append) + self.conn.execute("BEGIN") + marker = self.jobs.read_selected_job_marker(self.conn) + progress = self.source.read_tier_progress(self.conn, model_id=self.fixture.target.model_id, + dimension=2, targets=self.fixture.target.tables) + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + self.conn.set_trace_callback(None) + self.assertEqual((progress.processed, progress.total, progress.outputs, progress.cursor, progress.complete), (3, 3, 3, 5, True)) + self.assertEqual((progress.tracker, progress.source_epoch, progress.source_schema_signature), + (marker.tracker, marker.source_epoch, marker.source_schema_signature)) + self.assertEqual(marker.job_id, intent.job_id) + self.assertEqual(progress.generation, plan.manifest.generation) + self.assertFalse(any("FROM MAIN.MESSAGES" in sql.upper() or "FROM MAIN.VEC_MESSAGES" in sql.upper() for sql in statements)) + + def test_progress_missing_half_receipt_and_read_cleanup_failure(self): + _, plan = self.fixture.build() + self.conn.execute("DELETE FROM metadata WHERE key=?", (self.source._receipt_key(plan.manifest.targets),)) + self.conn.commit() + with self.assertRaises(self.source.TierSourceUntrusted): + self.source.read_tier_progress(self.conn, model_id=self.fixture.target.model_id, dimension=2, targets=plan.manifest.targets) + self.assertFalse(self.conn.in_transaction) + self.conn.armed = True + self.conn.rollback_mode = "before" + self.conn.fault = MemoryError("synthetic cleanup") + before = self.conn.rollbacks + with self.assertRaises(MemoryError): + self.source.read_tier_progress(self.conn, model_id=self.fixture.target.model_id, dimension=2, targets=plan.manifest.targets) + self.assertEqual(self.conn.rollbacks, before + 1) + self.assertTrue(self.conn.in_transaction) + self.conn.armed = False + self.conn.rollback() + + def test_config_guard_seed_mirror_preserves_fresh_independent_fields(self): + self.terminal_marker() + with tempfile.TemporaryDirectory(prefix="synthetic-policy-") as directory: + config = Path(directory) / "config.json" + config.write_text(json.dumps({"tier": "base", "synthetic_setting": 1})) + guard = self.projection.capture_config_guard(config_path=config) + self.assertEqual(guard.tier, "base") + self.assertEqual(len(guard.generation), 32) + self.assertEqual(self.projection.capture_config_guard(config_path=config), guard) + result = self.publish(expected_config=guard) + self.projection.patch_config_fields({"synthetic_setting": 2, "other": True}, config_path=config) + mirrored = self.projection.mirror_activation(result, expected_config=guard, config_path=config) + self.assertEqual((mirrored.tier, mirrored.generation), ("pro", result.generation)) + self.assertEqual(json.loads(config.read_text(encoding="utf-8")), {"tier": "pro", "tier_activation_generation": result.generation, + "synthetic_setting": 2, "other": True}) + self.assertEqual(self.projection.mirror_activation(result, expected_config=None, config_path=config), mirrored) + self.assertFalse(self.api.read_activation_state(self.conn).legacy_policy.config_acknowledged) + + def test_config_unknown_and_concurrent_legacy_edit_refuse_without_write(self): + self.terminal_marker() + result = self.publish() + with tempfile.TemporaryDirectory(prefix="synthetic-policy-") as directory: + config = Path(directory) / "config.json" + config.write_text('{"tier":"base"}') + original = config.read_bytes() + with self.assertRaises(self.projection.ConfigProjectionConflict): + self.projection.mirror_activation(result, expected_config=None, config_path=config) + self.assertEqual(config.read_bytes(), original) + guard = self.projection.capture_config_guard(config_path=config) + config.write_text('{"tier":"edge"}') + edited = config.read_bytes() + with self.assertRaises(self.projection.ConfigProjectionConflict): + self.projection.mirror_activation(result, expected_config=guard, config_path=config) + self.assertEqual(config.read_bytes(), edited) + for field in ("tier", "tier_activation_generation"): + with self.assertRaises(self.projection.ConfigProjectionConflict): + self.projection.patch_config_fields({field: "synthetic"}, config_path=config) + + def test_config_lock_failure_strict_refuses_old_best_effort_remains(self): + with tempfile.TemporaryDirectory(prefix="synthetic-policy-") as directory: + config = Path(directory) / "config.json" + with patch.dict(self.projection.__dict__["__builtins__"], {"open": lambda *args, **kwargs: (_ for _ in ()).throw(PermissionError("synthetic lock"))}): + with self.projection.ConfigFileLock(config.with_suffix(".lock")): + pass + with self.assertRaises(self.projection.ConfigProjectionConflict): + self.projection.capture_config_guard(config_path=config) + self.assertFalse(config.exists()) + + def test_config_invalid_or_oversized_never_overwritten(self): + with tempfile.TemporaryDirectory(prefix="synthetic-policy-") as directory: + config = Path(directory) / "config.json" + for raw in (b'null', b'{"tier":"base","tier":"pro"}', b'{"tier":"base"}\x00' + b'x' * (1024 * 1024)): + config.write_bytes(raw) + with self.subTest(length=len(raw)), self.assertRaises(self.projection.ConfigProjectionConflict): + self.projection.capture_config_guard(config_path=config) + self.assertEqual(config.read_bytes(), raw) + + + def test_policy_refuses_temp_alias_trigger_and_stale_config_generation(self): + intent = self.stage() + self.conn.execute("CREATE TEMP TABLE METADATA(key TEXT, value TEXT)") + with self.assertRaises(self.api.TierActivationError): + self.publish(intent) + self.conn.execute("DROP TABLE temp.METADATA") + self.conn.execute("CREATE TRIGGER journal_side_effect AFTER UPDATE ON metadata BEGIN INSERT INTO old_serving_pair VALUES(x'01'); END") + with self.assertRaises(self.api.TierActivationError): + self.publish(intent) + self.conn.execute("DROP TRIGGER journal_side_effect") + result = self.publish(intent) + self.api.acknowledge_policy_config(self.conn, generation=result.generation) + with self.assertRaises(self.api.TierActivationError): + self.publish(expected_intent_id=result.intent_id, target=self.base, + expected_config=self.api.ConfigGuard(True, "pro", "b" * 32)) + self.assertEqual(self.api.read_activation_state(self.conn).legacy_policy.generation, result.generation) + + def test_corrupt_previous_policy_rejected_before_projection(self): + self.terminal_marker() + result = self.publish() + self.api.acknowledge_policy_config(self.conn, generation=result.generation) + job = self.jobs.select_tier_job(self.conn, self.fixture.target, previous_job_id=self.fixture.job.job_id) + intent = self.api.stage_activation_intent(self.conn, job, expected_generation=None, reranker_id="synthetic/ranker", + expected_config=self.api.ConfigGuard(True, "pro", result.generation), status_id=1) + for replacement in (None, "b" * 32): + value = json.loads(self.api._dump(intent)) + value["expected_config"]["generation"] = replacement + self.conn.execute("UPDATE metadata SET value=? WHERE key='tier_activation_v1'", (json.dumps(value),)) + self.conn.commit() + with self.assertRaises(self.api.TierActivationError): + self.api.read_activation_state(self.conn) + + def test_progress_oversized_nul_tail_never_materializes_payload(self): + _, plan = self.fixture.build() + key = self.source.manifest_key(plan.manifest.targets) + self.conn.execute("UPDATE metadata SET value=value||? WHERE key=?", ("\x00" + "x" * 66000, key)) + self.conn.commit() + statements = [] + self.conn.set_trace_callback(statements.append) + with self.assertRaises(self.source.TierSourceUntrusted): + self.source.read_tier_progress(self.conn, model_id=self.fixture.target.model_id, dimension=2, targets=plan.manifest.targets) + self.conn.set_trace_callback(None) + self.assertFalse(any(sql.startswith("SELECT value FROM main.metadata") for sql in statements)) + + def test_config_independent_patch_and_mirror_share_one_lock(self): + self.terminal_marker() + with tempfile.TemporaryDirectory(prefix="synthetic-policy-") as directory: + config = Path(directory) / "config.json" + config.write_text('{"tier":"base"}') + guard = self.projection.capture_config_guard(config_path=config) + result = self.publish(expected_config=guard) + barrier = threading.Barrier(2) + errors = [] + def patcher(): + try: + barrier.wait(timeout=2) + self.projection.patch_config_fields({"independent": 73}, config_path=config) + except BaseException as error: + errors.append(error) + thread = threading.Thread(target=patcher) + thread.start() + barrier.wait(timeout=2) + self.projection.mirror_activation(result, expected_config=guard, config_path=config) + thread.join(timeout=2) + self.assertFalse(thread.is_alive()) + self.assertEqual(errors, []) + self.assertEqual(json.loads(config.read_text(encoding="utf-8")), {"tier":"pro", "tier_activation_generation":result.generation, "independent":73}) + + def test_config_replace_after_success_error_preserves_pending_reconciliation(self): + self.terminal_marker() + with tempfile.TemporaryDirectory(prefix="synthetic-policy-") as directory: + config = Path(directory) / "config.json" + config.write_text('{"tier":"base"}') + guard = self.projection.capture_config_guard(config_path=config) + result = self.publish(expected_config=guard) + original = self.projection._replace + def fail_after(src, dst): + original(src, dst) + raise OSError("synthetic after replace") + with patch.object(self.projection, "_replace", fail_after), self.assertRaises(OSError): + self.projection.mirror_activation(result, expected_config=guard, config_path=config) + self.assertFalse(self.api.read_activation_state(self.conn).legacy_policy.config_acknowledged) + self.assertEqual(self.projection.mirror_activation(result, expected_config=guard, config_path=config).generation, result.generation) + + + +class TestIdentityErrorWire(unittest.TestCase): + def setUp(self): + fixtures = runpy.run_path(str(Path(__file__).with_name("test-tier-serving-receipt-795.py"))) + self.fixture = fixtures["TestServingReceipts"]() + self.addCleanup(self.fixture.doCleanups) + self.fixture.setUp() + + def test_identity_error_roundtrip_native_errors_still_generic(self): + fixture = self.fixture + target_module = sys.modules.get("truememory.embedding_target") + self.assertIsNotNone(target_module) + requests = [({"op": "embed", "expected_target": fixture.edge.to_wire()}, target_module.EmbeddingTargetError("synthetic identity")), + ({"op": "embed_target_v1"}, target_module.EmbeddingTargetError("synthetic custom identity")), + ({"op": "rerank", "expected_model_name": "synthetic/ranker"}, fixture.ms._ServingIdentityMismatch("synthetic reranker")), + ({"op": "embed", "expected_target": fixture.edge.to_wire()}, RuntimeError("synthetic native unavailable")), + ({"op": ["malformed"]}, ValueError("synthetic ordinary invalid request"))] + for request, error in requests: + replies = [] + server = fixture.server + server._recv_exact = lambda *args: json.dumps(request).encode() + server._send_response = lambda conn, response: replies.append(response) + server.handle_request = lambda request: (_ for _ in ()).throw(error) + server._serve_client(types.SimpleNamespace(settimeout=lambda _: None), types.SimpleNamespace(length=1, frame_expires_at=None)) + self.assertEqual(len(replies), 1) + response = replies[0] + self.assertFalse(response["ok"]) + if isinstance(error, (target_module.EmbeddingTargetError, fixture.ms._ServingIdentityMismatch)): + self.assertEqual(response["error_code"], "serving_identity_mismatch") + with self.assertRaises(fixture.client.ServingIdentityMismatchError): + fixture.client._check_model_response(response, request) + else: + self.assertNotIn("error_code", response) + with self.assertRaises(RuntimeError): + fixture.client._check_model_response(response, request) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-tier-active-initialization-795.py b/tests/test-tier-active-initialization-795.py new file mode 100644 index 00000000..bc83c729 --- /dev/null +++ b/tests/test-tier-active-initialization-795.py @@ -0,0 +1,488 @@ +"""Live automatic-worker initialization receipts, with stdlib and memory SQLite.""" +from __future__ import annotations + +import contextlib +import ast +import logging +import os +from pathlib import Path +import runpy +import re +import sqlite3 +import threading +import types +import unittest +import uuid +import weakref +from typing import NamedTuple +from unittest.mock import Mock, patch + +BOUNDARIES = runpy.run_path(str(Path(__file__).with_name("test-tier-public-boundaries-795.py"))) +definitions = BOUNDARIES["definitions"] +enter_context = BOUNDARIES["enter_context"] + + +class TestActiveInitialization(unittest.TestCase): + def setUp(self): + self.fixture = BOUNDARIES["TestEngineReconnectReceipt"]() + self.addCleanup(self.fixture.doCleanups) + self.fixture.setUp() + self.first = self.fixture.engine() + self.api = self.fixture.api + self.closed_fds = [] + self.worker_conn = self.fixture.memory_connection() + self.entered, self.release = threading.Event(), threading.Event() + thread_api = types.SimpleNamespace(Thread=threading.Thread, Lock=threading.Lock, + Event=threading.Event, local=threading.local) + names = {"MaintenanceBusyError", "MaintenanceOwnership", "MaintenanceRequest", "MaintenanceCoordinator", + "MaintenanceReport", "LayerResult", "_StyleOpenRejected", "_style_error_category", + "canonical_database_path", "_bind_owner", "maintenance_owner", "_acquire_owner", "_release_owner", + "_after_fork", "get_coordinator", "_valid_initialization_receipt", "_active_initialization_receipt"} + self.maintenance = definitions("maintenance.py", names, self.fixture.namespace( + Path=Path, re=re, os=types.SimpleNamespace(getpid=os.getpid, close=self.closed_fds.append, PathLike=os.PathLike, fspath=os.fspath), + uuid=uuid, weakref=weakref, threading=thread_api, sqlite3=sqlite3, NamedTuple=NamedTuple, + contextmanager=contextlib.contextmanager, _registry_lock=threading.Lock(), + _coordinators=weakref.WeakValueDictionary(), _held_fds=set(), _held_paths={}, _owner_waiters={}, + _thread_owners=threading.local(), try_file_lock=lambda path: 71, + create_db=lambda path: self.worker_conn, logger=logging.getLogger("synthetic-active-receipt"))) + self.fixture.modules["maintenance"] = self.maintenance + storage = ast.parse((BOUNDARIES["ROOT"] / "truememory/storage.py").read_text(encoding="utf-8")) + self.fixture.modules["storage"].DEFAULT_BUSY_TIMEOUT_MS = next( + ast.literal_eval(node.value) for node in storage.body if isinstance(node, ast.Assign) + and any(isinstance(target, ast.Name) and target.id == "DEFAULT_BUSY_TIMEOUT_MS" for target in node.targets)) + self.coordinator = self.maintenance.get_coordinator(self.first.db_path) + self.fixture.engine_namespace["_HAS_HYBRID"] = True + self.maintenance.run_engine_maintenance = self.work + enter_context(self.fixture, patch.object(Path, "exists", return_value=False)) + self.addCleanup(self.finish) + + def work(self, conn, coordinator, **kwargs): + self.entered.set() + if not self.release.wait(3): + raise AssertionError("Synthetic automatic worker was not released") + return None + + def finish(self): + self.release.set() + self.coordinator.wait(3) + + def start(self, receipt=True): + values = {"_initialization_receipt": self.first._reconnect_receipt} if receipt else {} + self.assertTrue(self.coordinator.request_layers(**values)) + self.assertTrue(self.entered.wait(2)) + self.assertEqual(self.coordinator.status[0], "running") + + def fresh(self): + engine = self.first.__class__() + engine.conn, engine.db_path = None, self.first.db_path + engine._init_lock = threading.Lock() + engine._write_lock = self.api.ConnectionWriteLock(engine) + engine.ready = engine._runtime_initialized = engine._has_vectors = False + engine._has_hybrid = True + engine._runtime_vector_connection = engine._runtime_vector_generation = None + engine._reconnect_receipt = None + return engine + + def record(self): + with self.maintenance._active_initialization_receipt(self.first.db_path) as value: + return value + + def test_extension_unavailable_fts_worker_retains_validated_automatic_admission(self): + self.fixture.engine_namespace["_HAS_VECTOR"] = True + self.fixture.conn.enable_load_extension = None + operation = types.SimpleNamespace(key=self.api._legacy_key(), _database=("file", str(self.first.db_path))) + self.first._capture_reconnect_receipt(operation) + self.assertIsNotNone(self.first._reconnect_receipt) + self.assertEqual(self.first._reconnect_receipt[7:9], (False, True)) + self.start() + candidate = self.fixture.candidate(self.first) + candidate.enable_load_extension = None + engine = self.fresh() + engine._open_connection_handle() + self.assertIs(engine.conn, candidate) + self.assertTrue(engine.ready) + self.assertTrue(engine._runtime_initialized) + self.assertFalse(engine._has_vectors) + self.assertFalse(engine._has_hybrid) + self.assertEqual(engine._reconnect_receipt, self.first._reconnect_receipt) + self.assertEqual(self.maintenance._held_paths[self.first.db_path], 71) + self.assertEqual(self.coordinator.status[0], "running") + self.assertEqual(self.fixture.fixture.loads, []) + + def test_fresh_engine_adopts_only_validated_active_automatic_receipt(self): + self.start() + record = self.record() + self.assertIs(record[0](), self.coordinator) + self.assertEqual(record[2], self.first._reconnect_receipt) + engine = self.fresh() + candidate = self.fixture.candidate(self.first) + queries = [] + candidate.set_trace_callback(queries.append) + engine._open_connection_handle() + self.assertIs(engine.conn, candidate) + self.assertTrue(engine.ready) + self.assertTrue(engine._runtime_initialized) + self.assertFalse(engine._has_vectors) + self.assertFalse(engine._has_hybrid) + self.assertEqual(engine._reconnect_receipt, self.first._reconnect_receipt) + self.assertFalse(candidate.in_transaction) + self.assertFalse(self.release.is_set()) + self.assertEqual(self.maintenance._held_paths[self.first.db_path], 71) + self.assertFalse(any(q.lstrip().split(" ", 1)[0].upper() in {"INSERT", "UPDATE", "DELETE", "CREATE", "DROP", "ALTER"} + for q in queries)) + self.assertEqual(self.fixture.fixture.loads, []) + + def test_manual_or_receiptless_automatic_owner_remains_busy(self): + for automatic in (False, True): + with self.subTest(automatic=automatic): + if automatic: + self.start(receipt=False) + else: + self.assertTrue(self.coordinator.request(lambda conn, cancel: self.work(conn, self.coordinator))) + self.assertTrue(self.entered.wait(2)) + self.assertIsNone(self.record()) + engine = self.fresh() + self.fixture.candidate(self.first) + with self.assertRaises(self.maintenance.MaintenanceBusyError): + engine._open_connection_handle() + self.assertIsNone(engine.conn) + self.assertFalse(engine.ready) + self.fixture.connect.assert_not_called() + self.finish() + self.entered.clear() + self.release.clear() + self.worker_conn = self.fixture.memory_connection() + + def test_worker_finish_revokes_before_descriptor_release(self): + self.start() + original = self.maintenance._release_owner + observations = [] + def release(fd): + observations.append(self.record()) + self.assertIsNone(self.coordinator._active_initialization) + return original(fd) + self.maintenance._release_owner = release + self.finish() + self.assertEqual(observations, [None]) + self.assertIsNone(self.record()) + self.assertIsNone(self.coordinator._pending_initialization) + self.assertIsNone(self.coordinator._reserved_initialization) + + def test_cancel_during_validation_does_not_install_or_close_old_engine_state(self): + self.start() + engine = self.fresh() + candidate = self.fixture.candidate(self.first) + original = engine._reopen_initialized_connection + def validate(path, **kwargs): + result = original(path, **kwargs) + self.coordinator.cancel() + return result + engine._reopen_initialized_connection = validate + with self.assertRaises(self.maintenance.MaintenanceBusyError): + engine._open_connection_handle() + self.fixture.assert_closed(candidate) + self.assertIsNone(engine.conn) + self.assertFalse(engine.ready) + self.assertIsNone(engine._reconnect_receipt) + self.assertIsNone(self.record()) + self.assertEqual(self.maintenance._held_paths[self.first.db_path], 71) + + def test_same_fd_different_launch_or_coordinator_cannot_revalidate_old_record(self): + self.start() + old = self.record() + with self.coordinator._mutex: + token, receipt = self.coordinator._active_initialization + self.coordinator._active_initialization = ((token[0], token[1], "different-launch"), receipt) + with self.maintenance._active_initialization_receipt(self.first.db_path, expected=old[:2]) as value: + self.assertIsNone(value) + replacement = self.maintenance.MaintenanceCoordinator(self.first.db_path) + with self.maintenance._registry_lock: + self.maintenance._coordinators[self.first.db_path] = replacement + with self.maintenance._active_initialization_receipt(self.first.db_path, expected=old[:2]) as value: + self.assertIsNone(value) + with self.maintenance._registry_lock: + self.maintenance._coordinators[self.first.db_path] = self.coordinator + + def test_receipt_validation_does_not_hold_registry_or_coordinator_mutex(self): + self.start() + engine = self.fresh() + self.fixture.candidate(self.first) + original = engine._reopen_initialized_connection + def validate(path, **kwargs): + self.assertTrue(self.maintenance._registry_lock.acquire(blocking=False)) + self.maintenance._registry_lock.release() + self.assertTrue(self.coordinator._mutex.acquire(blocking=False)) + self.coordinator._mutex.release() + self.assertFalse(engine._write_lock.locked()) + self.assertIsNone(engine._reconnect_receipt) + self.assertEqual(kwargs["receipt"], self.first._reconnect_receipt) + return original(path, **kwargs) + engine._reopen_initialized_connection = validate + engine._open_connection_handle() + self.assertTrue(engine.ready) + + def test_exact_pending_generation_and_receiptless_request_do_not_revive_evidence(self): + self.start() + first_generation = self.coordinator._notification_generation + self.assertFalse(self.coordinator.request_layers(_initialization_receipt=self.first._reconnect_receipt)) + self.assertEqual(self.coordinator._pending_initialization[0], first_generation + 1) + self.assertFalse(self.coordinator.request_layers(threshold=37)) + self.assertIsNone(self.coordinator._pending_initialization) + self.assertEqual(self.coordinator._pending.generation, first_generation + 2) + self.assertEqual(self.coordinator._pending.threshold, 37) + self.coordinator.cancel() + self.assertIsNone(self.coordinator._active_initialization) + self.assertIsNone(self.coordinator._pending_initialization) + self.assertIsNone(self.coordinator._reserved_initialization) + + def test_busy_launch_drops_evidence_and_keeps_exact_pending_work(self): + self.maintenance._held_paths[self.first.db_path] = 91 + try: + self.assertFalse(self.coordinator.request_layers(_initialization_receipt=self.first._reconnect_receipt)) + self.assertEqual(self.coordinator._pending.generation, 1) + self.assertIsNone(self.coordinator._active_initialization) + self.assertIsNone(self.coordinator._pending_initialization) + self.assertIsNone(self.coordinator._reserved_initialization) + self.assertIsNone(self.record()) + finally: + self.maintenance._held_paths.pop(self.first.db_path) + self.coordinator.cancel() + + def test_invalid_receipts_are_not_retained_or_exposed(self): + receipt = self.first._reconnect_receipt + invalid = [object(), ("/other/database", *receipt[1:]), (*receipt[:4], ("legacy", object(), "256", "edge", "rr"), *receipt[5:]), + (*receipt[:4], ("legacy", "x" * 513, "256", "edge", "rr"), *receipt[5:]), + (*receipt[:4], ("legacy", "\ud800", "256", "edge", "rr"), *receipt[5:])] + self.start() + for value in invalid: + with self.subTest(kind=type(value).__name__): + self.assertFalse(self.coordinator.request_layers(_initialization_receipt=value)) + self.assertIsNone(self.coordinator._pending_initialization) + self.coordinator.cancel() + + def test_start_failure_revokes_before_owner_release(self): + releases = [] + original = self.maintenance._release_owner + def release(fd): + releases.append(self.record()) + return original(fd) + self.maintenance._release_owner = release + self.maintenance.threading.Thread = lambda **kwargs: types.SimpleNamespace(start=Mock(side_effect=RuntimeError("synthetic start failure"))) + with self.assertRaises(RuntimeError): + self.coordinator.request_layers(_initialization_receipt=self.first._reconnect_receipt) + self.assertEqual(releases, [None]) + self.assertIsNone(self.coordinator._active_initialization) + self.assertIsNone(self.coordinator._pending_initialization) + self.assertIsNone(self.coordinator._reserved_initialization) + self.assertFalse(self.coordinator._active) + + def test_fork_reset_clears_inherited_evidence_and_registry(self): + self.coordinator._active = True + self.coordinator._automatic_initialization = True + self.coordinator._active_initialization = ((os.getpid(), 71, "synthetic-live"), self.first._reconnect_receipt) + self.coordinator._pending_initialization = (1, self.first._reconnect_receipt) + self.coordinator._reserved_initialization = (1, self.first._reconnect_receipt) + self.maintenance._held_paths[self.first.db_path] = 71 + self.maintenance._held_fds.add(71) + self.maintenance._after_fork() + self.assertIsNone(self.coordinator._active_initialization) + self.assertIsNone(self.coordinator._pending_initialization) + self.assertIsNone(self.coordinator._reserved_initialization) + self.assertEqual(len(self.maintenance._coordinators), 0) + self.assertEqual(self.maintenance._held_paths, {}) + self.assertEqual(self.closed_fds, [71]) + self.assertIsNone(self.record()) + self.coordinator._active = False + + def test_admitted_model_or_dimension_change_refuses_ready_receipt_but_deep_override_remains_valid(self): + self.start() + engine = self.fresh() + self.fixture.candidate(self.first) + engine._open_connection_handle() + module = definitions("engine.py", {"_initialize_connection"}, self.fixture.namespace(), methods=True) + for key in (("legacy", "qwen3_256", "256", "base", "synthetic/default"), + ("legacy", "model2vec", "384", "edge", "synthetic/default")): + operation = types.SimpleNamespace(selection=None, policy=None, key=key) + with patch.object(self.api, "current_operation", return_value=operation): + with self.assertRaises(self.api.TierRuntimeError): + module._initialize_connection(engine, _suppress_maintenance=True) + for key in (self.first._reconnect_receipt[4], ("legacy", "model2vec", "256", "edge", "synthetic/deep")): + operation = types.SimpleNamespace(selection=None, policy=None, key=key) + with patch.object(self.api, "current_operation", return_value=operation), \ + patch.object(engine.conn, "execute", side_effect=AssertionError("Ready check performed SQL")), \ + patch.object(Path, "stat", side_effect=AssertionError("Ready check stat")): + module._initialize_connection(engine, _suppress_maintenance=True) + self.assertEqual(self.fixture.fixture.loads, []) + + + def test_receiptless_successor_cannot_reuse_previous_live_owner_record(self): + second_conn = self.fixture.memory_connection() + connections = iter((self.worker_conn, second_conn)) + self.maintenance.create_db = lambda path: next(connections) + second_entered, second_release = threading.Event(), threading.Event() + calls = [] + def work(conn, coordinator, **kwargs): + calls.append(conn) + if len(calls) == 1: + self.entered.set() + if not self.release.wait(3): + raise AssertionError("First synthetic worker release timed out") + else: + second_entered.set() + if not second_release.wait(3): + raise AssertionError("Successor synthetic worker release timed out") + self.maintenance.run_engine_maintenance = work + self.start() + old = self.record() + self.assertFalse(self.coordinator.request_layers()) + self.release.set() + try: + self.assertTrue(second_entered.wait(2)) + self.assertEqual(len(calls), 2) + self.assertIsNone(self.record()) + with self.maintenance._active_initialization_receipt(self.first.db_path, expected=old[:2]) as observed: + self.assertIsNone(observed) + self.assertIsNone(self.coordinator._pending_initialization) + self.assertIsNone(self.coordinator._reserved_initialization) + finally: + second_release.set() + self.assertTrue(self.coordinator.wait(3)) + + def test_actual_serving_admission_rechecks_embedding_after_candidate_validation(self): + self.start() + engine = self.fresh() + candidate = self.fixture.candidate(self.first) + original = engine._reopen_initialized_connection + vector = self.fixture.fixture.vector + def validate(path, **kwargs): + result = original(path, **kwargs) + vector.EMBEDDING_MODEL = "qwen3_256" + return result + engine._reopen_initialized_connection = validate + engine._open_connection_handle() + self.assertIs(engine.conn, candidate) + module = definitions("engine.py", {"_initialize_connection"}, self.fixture.namespace(), methods=True) + with self.api.serving_operation(candidate): + with self.assertRaises(self.api.TierRuntimeError): + module._initialize_connection(engine, _suppress_maintenance=True) + vector.EMBEDDING_MODEL = "model2vec" + vector._embedding_dim = 384 + with self.api.serving_operation(candidate): + with self.assertRaises(self.api.TierRuntimeError): + module._initialize_connection(engine, _suppress_maintenance=True) + vector._embedding_dim = 256 + self.fixture.fixture.reranker._model_name = "synthetic/deep" + with self.api.serving_operation(candidate, reranker_id="synthetic/deep"): + with patch.object(candidate, "execute", side_effect=AssertionError("Ready admission performed SQL")): + module._initialize_connection(engine, _suppress_maintenance=True) + self.assertEqual(self.fixture.fixture.loads, []) + + + + def test_failed_start_preserves_only_newer_pending_generation_receipt(self): + for donate in (True, False): + with self.subTest(donate=donate): + start_entered, allow_failure = threading.Event(), threading.Event() + attempts, errors = [], [] + receipt = tuple(list(self.first._reconnect_receipt)) + class FailedStart: + def start(inner): + start_entered.set() + if not allow_failure.wait(3): + raise AssertionError("Synthetic start interleaving timed out") + raise RuntimeError("synthetic first start failed") + def thread_factory(**kwargs): + attempts.append(kwargs) + return FailedStart() if len(attempts) == 1 else threading.Thread(**kwargs) + self.maintenance.threading.Thread = thread_factory + def request_first(): + try: + self.coordinator.request_layers(threshold=31, + _initialization_receipt=self.first._reconnect_receipt) + except BaseException as error: + errors.append(error) + requester = threading.Thread(target=request_first) + requester.start() + try: + self.assertTrue(start_entered.wait(2)) + values = {"_initialization_receipt": receipt} if donate else {} + self.assertFalse(self.coordinator.request_layers(threshold=32, **values)) + generation = self.coordinator._pending.generation + if donate: + self.assertEqual(self.coordinator._pending_initialization, (generation, receipt)) + else: + self.assertIsNone(self.coordinator._pending_initialization) + allow_failure.set() + requester.join(2) + self.assertFalse(requester.is_alive()) + self.assertEqual(len(errors), 1) + self.assertIs(type(errors[0]), RuntimeError) + self.assertTrue(self.entered.wait(2)) + offered = self.record() + if donate: + self.assertIsNotNone(offered) + self.assertIs(offered[2], receipt) + else: + self.assertIsNone(offered) + self.assertEqual(len(attempts), 2) + self.assertEqual(self.coordinator._notification_generation, generation) + self.assertIsNone(self.coordinator._pending_initialization) + self.assertIsNone(self.coordinator._reserved_initialization) + finally: + allow_failure.set() + self.release.set() + requester.join(3) + self.assertTrue(self.coordinator.wait(3)) + self.entered.clear() + self.release.clear() + self.worker_conn = self.fixture.memory_connection() + + def test_busy_acquisition_preserves_only_surviving_pending_generation_receipt(self): + for donate in (True, False): + with self.subTest(donate=donate): + acquire_entered, allow_busy = threading.Event(), threading.Event() + results, errors = [], [] + receipt = tuple(list(self.first._reconnect_receipt)) + def file_lock(path): + acquire_entered.set() + if not allow_busy.wait(3): + raise AssertionError("Synthetic owner interleaving timed out") + return None + self.maintenance.try_file_lock = file_lock + def request_first(): + try: + results.append(self.coordinator.request_layers(threshold=31, + _initialization_receipt=self.first._reconnect_receipt)) + except BaseException as error: + errors.append(error) + requester = threading.Thread(target=request_first) + requester.start() + try: + self.assertTrue(acquire_entered.wait(2)) + values = {"_initialization_receipt": receipt} if donate else {} + self.assertFalse(self.coordinator.request_layers(threshold=32, **values)) + generation = self.coordinator._pending.generation + allow_busy.set() + requester.join(2) + self.assertFalse(requester.is_alive()) + self.assertEqual(errors, []) + self.assertEqual(results, [False]) + self.assertEqual(self.coordinator._pending.generation, generation) + self.assertEqual(self.coordinator._pending.threshold, 32) + if donate: + self.assertEqual(self.coordinator._pending_initialization, (generation, receipt)) + self.assertIs(self.coordinator._pending_initialization[1], receipt) + else: + self.assertIsNone(self.coordinator._pending_initialization) + self.assertIsNone(self.coordinator._active_initialization) + self.assertIsNone(self.coordinator._reserved_initialization) + self.assertIsNone(self.record()) + finally: + allow_busy.set() + requester.join(3) + self.coordinator.cancel() + + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-tier-device-policy-732.py b/tests/test-tier-device-policy-732.py new file mode 100644 index 00000000..ed8776f7 --- /dev/null +++ b/tests/test-tier-device-policy-732.py @@ -0,0 +1,111 @@ +"""Synthetic actual-source witnesses for tier-switch device policy.""" +from __future__ import annotations + +import ast +import contextlib +import io +import logging +import os +import sys +import types +import unittest +from pathlib import Path +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] + + +def selected_functions(relative: str, names: set[str], namespace: dict[str, object]) -> None: + path = ROOT / relative + parsed = ast.parse(path.read_text(encoding="utf-8")) + selected = [node for node in parsed.body if isinstance(node, ast.FunctionDef) and node.name in names] + if {node.name for node in selected} != names: + raise AssertionError("Actual source functions were not found") + exec(compile(ast.Module(body=selected, type_ignores=[]), str(path), "exec"), namespace) + + +class TierDevicePolicyTests(unittest.TestCase): + def observed(self, override: str | None, *, cuda: bool, mps: bool) -> str: + torch = types.ModuleType("torch") + torch.cuda = types.SimpleNamespace(is_available=lambda: cuda) + torch.backends = types.SimpleNamespace(mps=types.SimpleNamespace(is_available=lambda: mps)) + shared = types.ModuleType("truememory.mps_utils") + shared.__dict__.update({ + "os": os, "logger": logging.getLogger("tier-policy-test"), + "_VALID_DEVICE_VALUES": {"cpu", "mps", "cuda", "auto"}, + }) + selected_functions("truememory/mps_utils.py", {"auto_detect_device", "resolve_device"}, shared.__dict__) + namespace: dict[str, object] = {} + selected_functions("truememory/tier_switch/manager.py", {"_detect_device"}, namespace) + package = types.ModuleType("truememory") + package.__path__ = [] + with patch.dict(sys.modules, {"torch": torch, "truememory": package, "truememory.mps_utils": shared}): + with patch.dict(os.environ): + if override is None: + os.environ.pop("TRUEMEMORY_DEVICE", None) + else: + os.environ["TRUEMEMORY_DEVICE"] = override + result = namespace["_detect_device"]() + self.assertIsInstance(result, str) + return result + + def test_cpu_override_beats_each_accelerator(self) -> None: + for cuda, mps in [(True, True), (True, False), (False, True), (False, False)]: + with self.subTest(cuda=cuda, mps=mps): + self.assertEqual(self.observed("cpu", cuda=cuda, mps=mps), "cpu") + + def test_auto_uses_shared_cuda_priority_and_identity(self) -> None: + for override in [None, "", "auto", " AUTO "]: + with self.subTest(override=override): + self.assertEqual(self.observed(override, cuda=True, mps=True), "cuda:0") + + def test_auto_mps_and_cpu_fallbacks(self) -> None: + self.assertEqual(self.observed(None, cuda=False, mps=True), "mps") + self.assertEqual(self.observed("auto", cuda=False, mps=False), "cpu") + + def test_explicit_available_devices(self) -> None: + self.assertEqual(self.observed("mps", cuda=True, mps=True), "mps") + self.assertEqual(self.observed("cuda", cuda=True, mps=True), "cuda:0") + self.assertEqual(self.observed(" CPU ", cuda=True, mps=True), "cpu") + + def test_unavailable_override_uses_shared_fallback(self) -> None: + with self.assertLogs("tier-policy-test", level="WARNING"): + self.assertEqual(self.observed("mps", cuda=True, mps=False), "cuda:0") + with self.assertLogs("tier-policy-test", level="WARNING"): + self.assertEqual(self.observed("cuda", cuda=False, mps=True), "mps") + with self.assertLogs("tier-policy-test", level="WARNING"): + self.assertEqual(self.observed("cuda", cuda=False, mps=False), "cpu") + + def test_invalid_override_retains_shared_warning_and_fallback(self) -> None: + with self.assertLogs("tier-policy-test", level="WARNING"): + self.assertEqual(self.observed("not-a-device", cuda=False, mps=True), "mps") + + def test_edge_tier_cpu_selection_is_preserved(self) -> None: + source = (ROOT / "truememory/tier_switch/manager.py").read_text(encoding="utf-8") + self.assertIn('device = "cpu" if job.target.tier_group == "edge" else _detect_device()', source) + + def test_actual_upgrade_cli_preserves_offline_environment(self) -> None: + namespace: dict[str, object] = { + "sys": sys, "_load_truememory_config": lambda: {}, + } + selected_functions("truememory/ingest/cli.py", {"_run_upgrade_tier"}, namespace) + manager = types.ModuleType("truememory.tier_switch.manager") + + class SyntheticManager: + def run_rebuild_sync(self, **kwargs: object) -> bool: + return True + + manager.RebuildManager = SyntheticManager + with patch.dict(sys.modules, {"truememory.tier_switch.manager": manager}): + for hf, transformers in [("1", "1"), ("0", "0"), ("custom", "custom")]: + with self.subTest(hf=hf, transformers=transformers), patch.dict(os.environ, { + "HF_HUB_OFFLINE": hf, "TRANSFORMERS_OFFLINE": transformers, + }), contextlib.redirect_stdout(io.StringIO()): + namespace["_run_upgrade_tier"](types.SimpleNamespace(tier="base", force=False)) + self.assertEqual(os.environ["HF_HUB_OFFLINE"], hf) + self.assertEqual(os.environ["TRANSFORMERS_OFFLINE"], transformers) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-tier-foreground-native-795.py b/tests/test-tier-foreground-native-795.py new file mode 100644 index 00000000..bf2e964e --- /dev/null +++ b/tests/test-tier-foreground-native-795.py @@ -0,0 +1,664 @@ +"""Opt-in Linux sqlite-vec publication under another thread's maintenance owner.""" + +from __future__ import annotations + +from collections.abc import Iterator +from contextlib import contextmanager +import hashlib +import os +from pathlib import Path +import struct +import sys +import threading +from types import ModuleType + +import pytest + + +pytestmark = pytest.mark.skipif( + sys.platform != "linux" or os.environ.get("TRUEMEMORY_TEST_NATIVE_VEC") != "1", + reason="Native foreground regression requires Linux and TRUEMEMORY_TEST_NATIVE_VEC=1", +) + + +class SyntheticEncoder: + def __init__(self) -> None: + self.calls: list[tuple[str, ...]] = [] + + @staticmethod + def vector(text: str) -> tuple[float, ...]: + digest = hashlib.sha256(text.encode("utf-8")).digest() + # 256 * (1 / 16) ** 2 = 1, including the deterministic sign changes. + return tuple(1 / 16 if digest[index // 8] & (1 << (index % 8)) else -1 / 16 + for index in range(256)) + + def encode(self, texts: list[str], **kwargs: object) -> object: + import numpy as np + + self.calls.append(tuple(texts)) + return np.asarray([self.vector(text) for text in texts], dtype=np.float32).reshape(len(texts), 256) + + +@pytest.fixture +def native_engine(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Iterator[tuple[object, ModuleType, SyntheticEncoder]]: + # Keep the guard before every application/native import and database open. + if sys.platform != "linux" or os.environ.get("TRUEMEMORY_TEST_NATIVE_VEC") != "1": + pytest.skip("Native foreground regression is opt-in on Linux") + monkeypatch.setenv("TRUEMEMORY_EMBED_MODEL", "edge") + monkeypatch.setenv("TRUEMEMORY_MODEL_SERVER", "off") + from truememory import reranker, vector_search + from truememory.engine import TrueMemoryEngine + from truememory.maintenance import get_coordinator + + vector_search.set_embedding_model("edge") + reranker.set_active_tier("edge") + encoder = SyntheticEncoder() + monkeypatch.setattr(vector_search, "_model", encoder) + engine = TrueMemoryEngine(tmp_path / "synthetic-foreground.sqlite") + coordinator = get_coordinator(engine.db_path) + try: + # Setup alone suppresses scheduling; all tested operations run normally. + engine._ensure_connection(_suppress_maintenance=True) + assert engine.ready and engine._has_vectors + assert encoder.calls == [] + assert engine.conn.execute("SELECT COUNT(*) FROM messages").fetchone()[0] == 0 + for table in (vector_search._active_vec_table(engine.conn), vector_search._active_sep_table(engine.conn)): + sql = engine.conn.execute("SELECT sql FROM sqlite_master WHERE name=?", (table,)).fetchone()[0] + assert "using vec0" in sql.lower() + assert engine.conn.execute(f'SELECT COUNT(*) FROM "{table}"').fetchone()[0] == 0 + yield engine, vector_search, encoder + finally: + # close() only detaches the Engine. Stop and join its shared worker first. + coordinator.cancel() + try: + assert coordinator.wait(10), "Shared maintenance teardown did not finish" + finally: + engine.close() + + +@contextmanager +def held_maintenance(path: Path) -> Iterator[threading.Thread]: + from truememory.maintenance import MaintenanceBusyError, maintenance_owner + + acquired = threading.Event() + release = threading.Event() + errors: list[BaseException] = [] + + def hold() -> None: + try: + with maintenance_owner(path): + acquired.set() + if not release.wait(30): + raise AssertionError("Foreground operation outlived the maintenance-owner deadline") + except BaseException as error: + errors.append(error) + acquired.set() + + worker = threading.Thread(target=hold, name="synthetic-foreground-maintenance-owner") + worker.start() + try: + assert acquired.wait(10), "Maintenance owner was not acquired" + assert not errors, errors + with pytest.raises(MaintenanceBusyError): + with maintenance_owner(path): + pytest.fail("A second thread acquired the existing maintenance ownership") + yield worker + assert worker.is_alive(), "Maintenance ownership ended before foreground assertions" + finally: + release.set() + worker.join(10) + assert not worker.is_alive(), "Maintenance owner did not join" + assert not errors, errors + + +def assert_publication(engine: object, vector: ModuleType, encoder: SyntheticEncoder, + message_id: int, content: str) -> tuple[bytes, bytes]: + row = engine.conn.execute( + "SELECT content,sender,recipient,timestamp FROM messages WHERE id=?", (message_id,), + ).fetchone() + assert row is not None and row[0] == content + separation = vector._build_sep_text(row[1], row[2], row[3], content) + blobs = [] + for table, text in ((vector._active_vec_table(engine.conn), content), + (vector._active_sep_table(engine.conn), separation)): + rows = engine.conn.execute(f'SELECT rowid,embedding FROM "{table}" ORDER BY rowid').fetchall() + assert len(rows) == 1 and rows[0][0] == message_id + blob = rows[0][1] + assert len(blob) == 256 * 4 + values = struct.unpack("256f", blob) + assert values == encoder.vector(text) + assert sum(value * value for value in values) == 1 + blobs.append(blob) + metadata = dict(engine.conn.execute( + "SELECT key,value FROM metadata WHERE key IN ('embed_model','embed_dim')", + ).fetchall()) + assert metadata == {"embed_model": vector.EMBEDDING_MODEL, "embed_dim": "256"} + assert not engine.conn.in_transaction + return blobs[0], blobs[1] + + +def seed_source(engine: object, content: str) -> int: + cursor = engine.conn.execute( + "INSERT INTO messages(content,sender,recipient,timestamp) VALUES(?,?,?,?)", + (content, "synthetic-sender", "synthetic-recipient", "2026-01-02"), + ) + engine.conn.commit() + return cursor.lastrowid + + +def test_first_add_publishes_vectors_and_metadata_under_maintenance(native_engine: tuple) -> None: + engine, vector, encoder = native_engine + with held_maintenance(engine.db_path): + result = engine.add("synthetic-first-add", sender="synthetic-sender", + recipient="synthetic-recipient", timestamp="2026-01-02") + assert_publication(engine, vector, encoder, result["id"], "synthetic-first-add") + assert len(encoder.calls) == 2 + + +def test_update_replaces_both_vectors_under_maintenance(native_engine: tuple) -> None: + engine, vector, encoder = native_engine + message_id = seed_source(engine, "synthetic-before-update") + vector.embed_single(engine.conn, message_id, "synthetic-before-update") + before = assert_publication(engine, vector, encoder, message_id, "synthetic-before-update") + call_count = len(encoder.calls) + with held_maintenance(engine.db_path): + result = engine.update(message_id, content="synthetic-after-update") + assert result is not None and result["id"] == message_id + after = assert_publication(engine, vector, encoder, message_id, "synthetic-after-update") + assert before[0] != after[0] and before[1] != after[1] + assert len(encoder.calls) == call_count + 2 + + +def test_direct_embed_single_publishes_known_row_under_maintenance(native_engine: tuple) -> None: + engine, vector, encoder = native_engine + message_id = seed_source(engine, "synthetic-direct-embed") + with held_maintenance(engine.db_path): + vector.embed_single(engine.conn, message_id, "synthetic-direct-embed") + assert_publication(engine, vector, encoder, message_id, "synthetic-direct-embed") + assert len(encoder.calls) == 2 + + +def test_fresh_engine_opens_during_actual_automatic_worker( + native_engine: tuple, monkeypatch: pytest.MonkeyPatch, +) -> None: + from model2vec import StaticModel + from truememory import maintenance + from truememory.engine import TrueMemoryEngine + + first, vector, encoder = native_engine + content = "synthetic-automatic-worker-open" + message_id = seed_source(first, content) + vector.embed_single(first.conn, message_id, content) + before = assert_publication(first, vector, encoder, message_id, content) + encode_count = len(encoder.calls) + coordinator = first._get_maintenance_coordinator() + second = TrueMemoryEngine(first.db_path) + entered = threading.Event() + release = threading.Event() + failures: list[str] = [] + completed: list[bool] = [] + constructor_calls: list[bool] = [] + real_run = maintenance.run_engine_maintenance + + def blocked_run(conn: object, owner: object, **kwargs: object) -> object: + try: + assert conn is not first.conn + assert owner is coordinator + assert conn.execute("SELECT COUNT(*) FROM messages").fetchone()[0] == 1 + entered.set() + if not release.wait(30): + raise AssertionError("Fresh Engine outlived the automatic-worker barrier") + result = real_run(conn, owner, **kwargs) + completed.append(True) + return result + except BaseException as error: + failures.append(type(error).__name__) + entered.set() + raise + + def forbidden_constructor(*args: object, **kwargs: object) -> object: + constructor_calls.append(True) + raise AssertionError("Opening a validated database must not construct an embedding model") + + def value_only(value: object) -> bool: + if type(value) is tuple: + return all(value_only(item) for item in value) + return type(value) in (type(None), bool, int, float, str) + + monkeypatch.setattr(maintenance, "run_engine_maintenance", blocked_run) + try: + first._maybe_startup_consolidate() + assert entered.wait(10), "Automatic worker did not reach its opened-connection barrier" + assert not failures, failures + assert coordinator.snapshot()["active"] + with pytest.raises(maintenance.MaintenanceBusyError): + with maintenance.maintenance_owner(first.db_path): + pytest.fail("Automatic worker did not retain real maintenance ownership") + + database_path = first.db_path.resolve() + with maintenance._active_initialization_receipt(database_path) as offered: + receipt = None if offered is None else offered[2] + assert type(receipt) is tuple and value_only(receipt) + assert receipt == first._reconnect_receipt + + with monkeypatch.context() as construction_guard: + construction_guard.setattr(StaticModel, "from_pretrained", forbidden_constructor) + second._ensure_connection() + assert second.ready and second._has_vectors + assert second.conn is not first.conn + assert assert_publication(second, vector, encoder, message_id, content) == before + assert len(encoder.calls) == encode_count + assert vector._model is encoder + assert constructor_calls == [] + assert coordinator.snapshot()["active"] and not release.is_set() + assert not failures, failures + finally: + release.set() + try: + assert coordinator.wait(15), "Automatic worker did not finish after barrier release" + assert not failures, failures + assert completed, "The wrapper never called the real maintenance implementation" + finally: + coordinator.cancel() + try: + assert coordinator.wait(10), "Automatic maintenance teardown did not finish" + finally: + second.close() + + +def test_fresh_public_search_waits_for_committed_style_enrollment( + native_engine: tuple, monkeypatch: pytest.MonkeyPatch, +) -> None: + import sentence_transformers + from model2vec import StaticModel + from truememory import maintenance, reranker, storage + from truememory.client import Memory + from truememory.tier_switch import runtime + + first, vector, encoder = native_engine + content = "synthetic workshop inventory record" + query = "workshop inventory" + message_id = seed_source(first, content) + vector.embed_single(first.conn, message_id, content) + before = assert_publication(first, vector, encoder, message_id, content) + encode_count = len(encoder.calls) + assert first._has_style_vec + receipt = first._reconnect_receipt + assert receipt is not None + schema_before = first.conn.execute("PRAGMA schema_version").fetchone()[0] + assert schema_before == receipt[3] + expected_triggers = { + name: " ".join(sql.split()) for name, sql in storage._STYLE_OUTPUT_TRIGGERS.items() + } + assert len(expected_triggers) == 3 + trigger_query = "SELECT name,sql FROM sqlite_master WHERE type='trigger' AND name IN (?,?,?)" + assert first.conn.execute(trigger_query, tuple(expected_triggers)).fetchall() == [] + + coordinator = first._get_maintenance_coordinator() + fresh = Memory(path=first.db_path) + enrolled = threading.Event() + release = threading.Event() + wait_entered = threading.Event() + search_done = threading.Event() + preparation_lock = threading.Lock() + preparations: list[bool] = [] + enrolled_schemas: list[int] = [] + worker_errors: list[BaseException] = [] + search_errors: list[BaseException] = [] + results: list[dict] = [] + constructor_calls: list[bool] = [] + reranker_constructor_calls: list[bool] = [] + reranker_pairs: list[tuple[tuple[str, str], ...]] = [] + reranker_predictions: list[tuple[float, ...]] = [] + expected_reranker_name = reranker.get_current_reranker_name() + real_prepare = maintenance._prepare_routed_style_maintenance + real_wait = maintenance.wait_for_automatic_owner + + class SyntheticReranker: + def predict(self, pairs: list[tuple[str, str]], **kwargs: object) -> list[float]: + scores = [1.0 if document == content else 0.25 for _, document in pairs] + reranker_pairs.append(tuple(pairs)) + reranker_predictions.append(tuple(scores)) + return scores + + synthetic_reranker = SyntheticReranker() + + def prepared_then_blocked(conn: object) -> None: + with preparation_lock: + first_preparation = not preparations + preparations.append(True) + try: + real_prepare(conn) + if not first_preparation: + return + assert conn is not first.conn + assert not conn.in_transaction + installed = { + name: " ".join(sql.strip().rstrip(";").split()) + for name, sql in conn.execute(trigger_query, tuple(expected_triggers)) + } + assert installed == expected_triggers + schema_after = conn.execute("PRAGMA schema_version").fetchone()[0] + assert schema_after == schema_before + len(expected_triggers) + enrolled_schemas.append(schema_after) + enrolled.set() + if not release.wait(30): + raise AssertionError("Public warm search outlived the style-enrollment barrier") + except BaseException as error: + worker_errors.append(error) + enrolled.set() + raise + + def observed_wait(error: object, *, deadline: float) -> object: + assert runtime.current_runtime_operation() is None + assert fresh._engine.conn is None and not fresh._engine.ready + assert len(encoder.calls) == encode_count + wait_entered.set() + return real_wait(error, deadline=deadline) + + def search_publicly() -> None: + try: + results.extend(fresh.search(query)) + except BaseException as error: + search_errors.append(error) + finally: + search_done.set() + + def forbidden_constructor(*args: object, **kwargs: object) -> object: + constructor_calls.append(True) + raise AssertionError("Warm public search must reuse its synthetic embedding model") + + def forbidden_reranker_constructor(*args: object, **kwargs: object) -> object: + reranker_constructor_calls.append(True) + raise AssertionError("Warm public search must reuse its synthetic reranker") + + search_worker = threading.Thread(target=search_publicly, name="synthetic-style-warm-search") + monkeypatch.setattr(maintenance, "_prepare_routed_style_maintenance", prepared_then_blocked) + monkeypatch.setattr(maintenance, "wait_for_automatic_owner", observed_wait) + with monkeypatch.context() as construction_guard: + construction_guard.setattr(StaticModel, "from_pretrained", forbidden_constructor) + construction_guard.setattr(sentence_transformers, "CrossEncoder", forbidden_reranker_constructor) + construction_guard.setattr(reranker, "_model", synthetic_reranker) + construction_guard.setattr(reranker, "_model_name", expected_reranker_name) + construction_guard.setattr(reranker, "_model_certified", False) + try: + first._maybe_startup_consolidate() + assert enrolled.wait(10), "Actual canonical style preparation did not complete" + assert not worker_errors, worker_errors + assert enrolled_schemas == [schema_before + 3] + assert first.conn.execute("PRAGMA schema_version").fetchone()[0] == enrolled_schemas[0] + assert coordinator.snapshot()["active"] + with pytest.raises(maintenance.MaintenanceBusyError): + with maintenance.maintenance_owner(first.db_path): + pytest.fail("Committed style enrollment lost its automatic maintenance owner") + + search_worker.start() + assert wait_entered.wait(10), "Fresh public search did not reach automatic-owner waiting" + assert not search_done.is_set() and search_worker.is_alive() + assert not search_errors, search_errors + assert fresh._engine.conn is None and not fresh._engine.ready + assert len(encoder.calls) == encode_count + assert constructor_calls == [] + assert reranker_constructor_calls == [] and reranker_pairs == [] + assert coordinator.snapshot()["active"] and not release.is_set() + assert fresh._engine._write_lock.acquire(blocking=False), "Public wait retained the Engine writer lock" + fresh._engine._write_lock.release() + + release.set() + search_worker.join(15) + assert not search_worker.is_alive() and search_done.is_set() + assert not search_errors, search_errors + assert coordinator.wait(15), "Automatic worker did not finish after style barrier release" + assert not worker_errors, worker_errors + assert fresh._engine.ready and fresh._engine._has_vectors + assert any(result["id"] == message_id and "vec" in result["source"] for result in results) + assert assert_publication(fresh._engine, vector, encoder, message_id, content) == before + search_calls = encoder.calls[encode_count:] + assert 1 <= len(search_calls) <= 2 + assert all(texts == (query,) for texts in search_calls) + assert vector._model is encoder and constructor_calls == [] + assert reranker._model is synthetic_reranker and reranker._model_name == expected_reranker_name + assert reranker_constructor_calls == [] and reranker_pairs + assert all(pair_query == query for pairs in reranker_pairs for pair_query, _ in pairs) + assert any(document == content for pairs in reranker_pairs for _, document in pairs) + assert all( + len(pairs) == len(scores) + and scores == tuple(1.0 if document == content else 0.25 for _, document in pairs) + for pairs, scores in zip(reranker_pairs, reranker_predictions) + ) + finally: + release.set() + try: + if search_worker.ident is not None: + search_worker.join(15) + assert not search_worker.is_alive(), "Public search thread did not join" + finally: + coordinator.cancel() + try: + assert coordinator.wait(10), "Style maintenance teardown did not finish" + finally: + fresh._engine.close() + + +def test_native_mcp_deep_children_keep_final_tables_and_defer_maintenance( + native_engine: tuple, monkeypatch: pytest.MonkeyPatch, +) -> None: + from model2vec import StaticModel + import sentence_transformers + from truememory import Memory, maintenance, reranker + from truememory.engine import TrueMemoryEngine + from truememory.tier_switch import runtime, serving + + monkeypatch.setenv("TRUEMEMORY_PRELOAD_MODELS", "0") + from truememory import mcp_server + + first, vector, encoder = native_engine + content = "synthetic workshop inventory record" + queries = ("workshop inventory", "synthetic inventory") + message_id = seed_source(first, content) + vector.embed_single(first.conn, message_id, content) + before = assert_publication(first, vector, encoder, message_id, content) + encode_count = len(encoder.calls) + source_row = first.conn.execute( + "SELECT sender,recipient,timestamp FROM messages WHERE id=?", (message_id,), + ).fetchone() + separation = vector._build_sep_text(*source_row, content) + coordinator = first._get_maintenance_coordinator() + parent = Memory(path=first.db_path) + expected_tables = ("vec_messages_edge", "vec_messages_sep_edge") + expected_reranker = mcp_server._DEEP_RERANKER + both_constructed = threading.Event() + both_opened = threading.Event() + query_barrier = threading.Barrier(2) + body_finished = threading.Event() + worker_entered = threading.Event() + worker_release = threading.Event() + observations_lock = threading.Lock() + children: list[Memory] = [] + opening: list[object] = [] + opened: list[object] = [] + active_queries: list[str] = [] + concurrent_queries: list[int] = [] + completed_queries: list[str] = [] + parent_keys: list[tuple] = [] + child_keys: list[tuple] = [] + requests: list[bool] = [] + worker_runs: list[bool] = [] + errors: list[BaseException] = [] + constructor_calls: list[str] = [] + reranker_pairs: list[tuple[tuple[str, str], ...]] = [] + real_open = TrueMemoryEngine._open_connection_handle + real_deep = Memory.search_deep + real_request = coordinator.request_layers + real_run = maintenance.run_engine_maintenance + + class SyntheticDeepReranker: + def predict(self, pairs: list[tuple[str, str]], **kwargs: object) -> list[float]: + reranker_pairs.append(tuple(pairs)) + return [1.0 if document == content else 0.25 for _, document in pairs] + + synthetic_reranker = SyntheticDeepReranker() + + def make_child(*args: object, **kwargs: object) -> Memory: + child = Memory(*args, **kwargs) + with observations_lock: + children.append(child) + if len(children) == 2: + both_constructed.set() + return child + + def observed_open(engine: object) -> None: + with observations_lock: + is_child = any(child._engine is engine for child in children) + first_handle = is_child and engine.conn is None + if first_handle: + opening.append(engine) + ordinal = len(opening) + if not first_handle: + return real_open(engine) + try: + assert both_constructed.wait(10), "Both real child Memory objects were not constructed" + with observations_lock: + assert len(opening) == ordinal and len(opened) == ordinal - 1 + real_open(engine) + assert engine.conn is not None and not engine.ready + with observations_lock: + opened.append(engine) + if len(opened) == 2: + both_opened.set() + except BaseException as error: + errors.append(error) + raise + + def observed_deep(memory: Memory, query: str, *args: object, **kwargs: object) -> list[dict]: + try: + assert both_opened.wait(10), "A child search retained the sibling opening lock" + assert kwargs == {"user_id": None, "limit": 10, "llm_fn": None} and args == () + operation = runtime.current_operation(memory._engine.conn) + assert operation is not None and operation._defer_maintenance + assert operation.tables == expected_tables and operation.key == parent_keys[0] + assert operation.reranker_id == expected_reranker + assert reranker.get_current_reranker_name() == expected_reranker + assert reranker.get_reranker() is synthetic_reranker + assert requests == [] and not worker_entered.is_set() + with observations_lock: + child_keys.append(operation.key) + active_queries.append(query) + concurrent_queries.append(len(active_queries)) + query_barrier.wait(10) + result = real_deep(memory, query, *args, **kwargs) + assert memory._engine.ready and memory._engine._has_vectors + assert runtime.current_operation(memory._engine.conn) is operation + assert (vector._active_vec_table(memory._engine.conn), + vector._active_sep_table(memory._engine.conn)) == expected_tables + assert assert_publication(memory._engine, vector, encoder, message_id, content) == before + assert any(item["id"] == message_id and "vec" in item["source"] for item in result) + assert requests == [] and not worker_entered.is_set() + completed_queries.append(query) + return result + except BaseException as error: + errors.append(error) + query_barrier.abort() + raise + finally: + with observations_lock: + if query in active_queries: + active_queries.remove(query) + + def observed_request(**kwargs: object) -> bool: + requests.append(True) + try: + assert body_finished.is_set() and len(requests) == 1 + assert runtime.current_runtime_operation() is None + assert not serving.current_thread_admitted() + assert len(children) == 2 and all(child._engine.conn is None for child in children) + assert sorted(completed_queries) == sorted(queries) + return real_request(**kwargs) + except BaseException as error: + errors.append(error) + raise + + def observed_run(conn: object, owner: object, **kwargs: object) -> object: + try: + assert owner is coordinator and body_finished.is_set() + worker_runs.append(True) + if len(worker_runs) == 1: + worker_entered.set() + if not worker_release.wait(30): + raise AssertionError("MCP post-parent worker outlived its observation barrier") + return real_run(conn, owner, **kwargs) + except BaseException as error: + errors.append(error) + worker_entered.set() + raise + + def forbidden_embedding_constructor(*args: object, **kwargs: object) -> object: + constructor_calls.append("embedding") + raise AssertionError("MCP child searches must retain the synthetic embedding model") + + def forbidden_reranker_constructor(*args: object, **kwargs: object) -> object: + constructor_calls.append("reranker") + raise AssertionError("MCP Deep children must retain the synthetic explicit reranker") + + with monkeypatch.context() as observed: + observed.setattr(mcp_server, "_memory", parent) + observed.setattr(mcp_server, "Memory", make_child) + observed.setattr(TrueMemoryEngine, "_open_connection_handle", observed_open) + observed.setattr(Memory, "search_deep", observed_deep) + observed.setattr(coordinator, "request_layers", observed_request) + observed.setattr(maintenance, "run_engine_maintenance", observed_run) + observed.setattr(StaticModel, "from_pretrained", forbidden_embedding_constructor) + observed.setattr(sentence_transformers, "CrossEncoder", forbidden_reranker_constructor) + observed.setattr(reranker, "_model", synthetic_reranker) + observed.setattr(reranker, "_model_name", expected_reranker) + observed.setattr(reranker, "_model_certified", False) + try: + with mcp_server._search_operation(reranker_id=expected_reranker) as (memory, operation): + assert memory is parent and parent._engine.ready + assert operation.tables == expected_tables and operation._defer_maintenance + assert operation.reranker_id == expected_reranker + parent_keys.append(operation.key) + assert assert_publication(parent._engine, vector, encoder, message_id, content) == before + assert parent._engine.conn.execute( + "SELECT name FROM sqlite_master WHERE name IN ('vec_messages','vec_messages_sep')", + ).fetchall() == [] + assert len(encoder.calls) == encode_count and constructor_calls == [] + assert requests == [] and not worker_entered.is_set() + results = mcp_server._parallel_search( + queries, None, 10, None, 10, memory=memory, operation=operation, + ) + assert not errors, errors + assert len(opening) == len(opened) == 2 and both_opened.is_set() + assert child_keys == [operation.key, operation.key] + assert max(concurrent_queries) == 2 and active_queries == [] + assert sorted(completed_queries) == sorted(queries) + assert all(child._engine.conn is None for child in children) + assert any(item["id"] == message_id and "vec" in item["source"] for item in results) + assert requests == [] and not worker_entered.is_set() + assert assert_publication(parent._engine, vector, encoder, message_id, content) == before + body_finished.set() + assert requests == [True] + assert worker_entered.wait(10), "Deferred automatic maintenance did not start after the parent" + assert not errors, errors + assert coordinator.snapshot()["active"] and not worker_release.is_set() + assert constructor_calls == [] and vector._model is encoder + assert reranker._model is synthetic_reranker and reranker._model_name == expected_reranker + assert encoder.calls[encode_count:] + assert all(text not in (content, separation) + for texts in encoder.calls[encode_count:] for text in texts) + assert all(query in queries for pairs in reranker_pairs for query, _ in pairs) + worker_release.set() + assert coordinator.wait(15), "MCP post-parent maintenance did not finish" + assert not errors, errors + assert requests == [True] and constructor_calls == [] + assert assert_publication(parent._engine, vector, encoder, message_id, content) == before + finally: + worker_release.set() + query_barrier.abort() + coordinator.cancel() + try: + assert coordinator.wait(10), "MCP shared maintenance teardown did not finish" + finally: + for child in children: + child.close() + parent.close() diff --git a/tests/test-tier-job-binding-795.py b/tests/test-tier-job-binding-795.py new file mode 100644 index 00000000..c5c77e98 --- /dev/null +++ b/tests/test-tier-job-binding-795.py @@ -0,0 +1,462 @@ +"""Durable selected-job admission using only synthetic in-memory databases.""" + +from __future__ import annotations + +import dataclasses +import runpy +import sqlite3 +import unittest +from pathlib import Path +from unittest.mock import patch + +BASE = runpy.run_path(str(Path(__file__).with_name("test-tier-job-identity-795.py"))) +CONNECT = BASE["CONNECT"] +CONNECTION = BASE["MockFileConnection"] +Cancelled = BASE["Cancelled"] + + +class FaultConnection(CONNECTION): + fault_sql: str | None = None + fault: BaseException | None = None + fail_commit = False + retain_commit = False + selection_payload_reads = 0 + + def execute(self, sql: str, parameters: tuple = ()): + if sql.startswith("SELECT job_id, tier"): + self.selection_payload_reads += 1 + if self.fault_sql is not None and self.fault_sql in sql: + raise self.fault + return super().execute(sql, parameters) + + def commit(self) -> None: + if self.fail_commit: + raise sqlite3.OperationalError("synthetic marker commit failure") + if not self.retain_commit: + super().commit() + + +class TestTierJobBinding(unittest.TestCase): + def setUp(self) -> None: + self.fixture = BASE["TestTierJobIdentity"]() + self.fixture.setUp() + self.addCleanup(self.fixture.doCleanups) + self.api = self.fixture.api + self.conn = self.fixture.conn + self.target = self.fixture.target + self.table = self.api._SELECTION_TABLE + + def select(self, **kwargs): + return self.api.select_tier_job(self.conn, self.target, **kwargs) + + def row(self, conn=None): + return (conn or self.conn).execute(f"SELECT * FROM {self.table}").fetchone() + + def clone(self, *, factory=CONNECTION): + conn = CONNECT(":memory:", factory=factory) + self.conn.backup(conn) + self.addCleanup(conn.close) + return conn + + def test_selection_commits_exact_marker_without_changing_source_identity(self) -> None: + previous = self.fixture.capture() + before = self.conn.total_changes + job = self.select() + self.assertEqual(self.conn.total_changes - before, 1) + self.assertFalse(self.conn.in_transaction) + self.assertEqual(job.target, self.target) + self.assertEqual(job.source_schema_signature, previous.source_schema_signature) + self.assertEqual(job.source_epoch, previous.source_epoch) + self.assertEqual(self.row(), (1, job.job_id, "selected", "base", "qwen3_256", 256, + "basepro", job.tracker, job.source_epoch, job.source_schema_signature)) + self.api.check_tier_job(self.conn, job) + self.api.check_selected_tier_job(self.conn, job) + self.assertFalse(self.conn.in_transaction) + self.assertEqual(self.conn.execute("SELECT COUNT(*) FROM messages").fetchone()[0], 0) + + def test_repeated_read_checks_do_not_change_source_plan_write_fence(self) -> None: + job = self.select() + changes, version = self.conn.total_changes, self.conn.execute("PRAGMA data_version").fetchone()[0] + for _ in range(3): + self.api.check_selected_tier_job(self.conn, job) + self.assertEqual(self.conn.total_changes, changes) + self.assertEqual(self.conn.execute("PRAGMA data_version").fetchone()[0], version) + self.assertFalse(self.conn.in_transaction) + + def test_live_job_cannot_be_replaced_even_with_its_id(self) -> None: + job = self.select() + before = self.row() + for kwargs in ({}, {"previous_job_id": job.job_id}, {"previous_job_id": "0" * 32}): + with self.assertRaises(self.api.TierJobSelectionError): + self.select(**kwargs) + self.assertEqual(self.row(), before) + self.assertFalse(self.conn.in_transaction) + + def test_terminal_replacement_requires_exact_previous_id(self) -> None: + old = self.select() + self.assertTrue(self.api.end_selected_tier_job(self.conn, old, outcome="cancelled")) + for kwargs in ({}, {"previous_job_id": "0" * 32}): + with self.assertRaises(self.api.TierJobSelectionError): + self.select(**kwargs) + new = self.select(previous_job_id=old.job_id) + self.assertNotEqual(old.job_id, new.job_id) + self.api.check_selected_tier_job(self.conn, new) + self.assertFalse(self.api.end_selected_tier_job(self.conn, old, outcome="failed")) + self.assertEqual(self.row()[1:3], (new.job_id, "selected")) + + def test_termination_compares_full_identity_and_is_not_completion(self) -> None: + job = self.select() + mismatched = dataclasses.replace(job, target=self.fixture.modules["embedding_target"].EmbeddingTarget( + "pro", "qwen3_256", 256, "basepro", + )) + self.assertFalse(self.api.end_selected_tier_job(self.conn, mismatched, outcome="failed")) + with self.assertRaises(self.api.TierJobSelectionError): + self.api.end_selected_tier_job(self.conn, job, outcome="completed") + self.assertEqual(self.row()[2], "selected") + self.assertTrue(self.api.end_selected_tier_job(self.conn, job, outcome="failed")) + self.assertFalse(self.api.end_selected_tier_job(self.conn, job, outcome="cancelled")) + self.assertEqual(self.row()[2], "failed") + with self.assertRaises(self.api.TierJobSelectionError): + self.api.check_selected_tier_job(self.conn, job) + + def test_preselection_clone_reopen_is_rejected_after_stale_connection_selection(self) -> None: + # The clone shares the source epoch but predates the marker write through + # the accepted connection. File observations intentionally cannot tell. + clone = self.clone() + job = self.select() + self.fixture.reopen_hooks() + with patch.object(self.api.sqlite3, "connect", return_value=clone): + with self.assertRaises(self.api.TierJobSelectionError): + with self.api.open_selected_tier_job(job): + self.fail("Pre-selection clone must not reach model admission") + self.assertTrue(clone.closed) + self.assertEqual(self.fixture.events[-1], "release") + self.api.check_selected_tier_job(self.conn, job) + + def test_clone_with_a_previously_selected_id_cannot_impersonate_new_selection(self) -> None: + old = self.select() + clone = self.clone() + self.api.end_selected_tier_job(self.conn, old, outcome="cancelled") + job = self.select(previous_job_id=old.job_id) + self.fixture.reopen_hooks() + with patch.object(self.api.sqlite3, "connect", return_value=clone): + with self.assertRaises(self.api.TierJobSelectionError): + with self.api.open_selected_tier_job(job): + self.fail("Old cloned selection must not yield") + self.assertTrue(clone.closed) + + def test_postselection_marker_copy_is_explicitly_outside_identity_guarantee(self) -> None: + job = self.select() + clone = self.clone() + self.fixture.reopen_hooks() + with patch.object(self.api.sqlite3, "connect", return_value=clone): + with self.api.open_selected_tier_job(job) as opened: + self.assertIs(opened, clone) + self.assertFalse(opened.in_transaction) + self.assertTrue(clone.closed) + + def test_owned_selected_reopen_validates_before_yield_and_releases_on_cancellation(self) -> None: + job = self.select() + reopened = self.fixture.reopen_hooks() + sentinel = Cancelled("synthetic cancellation") + with self.assertRaises(Cancelled) as caught: + with self.api.open_selected_tier_job(job) as conn: + self.assertFalse(conn.in_transaction) + self.api.check_selected_tier_job(conn, job) + raise sentinel + self.assertIs(caught.exception, sentinel) + self.assertTrue(reopened[0].closed) + self.assertEqual(self.fixture.events[-1], "release") + + def test_borrowed_transaction_never_installs_or_mutates_marker(self) -> None: + job = self.fixture.capture() + self.conn.execute("BEGIN") + before = self.conn.total_changes + for call in (lambda: self.select(), lambda: self.api.check_selected_tier_job(self.conn, job), + lambda: self.api.end_selected_tier_job(self.conn, job, outcome="failed")): + with self.assertRaises(self.api.TierJobIdentityError): + call() + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.conn.total_changes, before) + self.conn.rollback() + self.assertIsNone(self.conn.execute("SELECT 1 FROM sqlite_master WHERE name=?", (self.table,)).fetchone()) + + def test_wrong_schema_view_temp_shadow_and_side_effects_refuse(self) -> None: + for ddl in (f"CREATE TABLE {self.table}(singleton INTEGER PRIMARY KEY, job_id TEXT)", + f"CREATE VIEW {self.table} AS SELECT 1 AS singleton", + f"CREATE TEMP TABLE {self.table.upper()}(value TEXT)"): + conn = self.clone() + conn.execute(ddl) + with self.assertRaises(self.api.TierJobIdentityError): + self.api.select_tier_job(conn, self.target) + self.assertFalse(conn.in_transaction) + job = self.select() + self.conn.execute(f"CREATE TRIGGER synthetic_marker_side_effect AFTER UPDATE ON {self.table} BEGIN SELECT 1; END") + before = self.row() + for call in (lambda: self.api.check_selected_tier_job(self.conn, job), + lambda: self.api.end_selected_tier_job(self.conn, job, outcome="failed")): + with self.assertRaises(self.api.TierJobSelectionError): + call() + self.assertEqual(self.row(), before) + + def test_invalid_and_oversized_stored_values_fail_before_value_materialization(self) -> None: + job = self.select() + for field, value in (("model_id", "x" * 513), ("source_epoch", "x" * 129), + ("dimension", "not-an-integer"), ("job_id", "z" * 32), + ("state", "completed"), ("source_schema_signature", "z" * 64)): + conn = self.clone() + conn.execute("PRAGMA ignore_check_constraints=ON") + conn.execute(f"UPDATE {self.table} SET {field}=?", (value,)) + conn.commit() + with self.assertRaises(self.api.TierJobSelectionError): + self.api.check_selected_tier_job(conn, job) + self.assertFalse(conn.in_transaction) + + def test_extra_singleton_and_changed_target_refuse(self) -> None: + job = self.select() + conn = self.clone() + conn.execute("PRAGMA ignore_check_constraints=ON") + columns = ", ".join((*self.api._SELECTION_FIELDS, "state")) + conn.execute(f"INSERT INTO {self.table} SELECT 2, {columns} FROM {self.table}") + conn.commit() + with self.assertRaises(self.api.TierJobSelectionError): + self.api.check_selected_tier_job(conn, job) + changed = dataclasses.replace(job, target=self.fixture.modules["embedding_target"].EmbeddingTarget( + "custom", "synthetic/encoder", 384, "custom", + )) + with self.assertRaises(self.api.TierJobSelectionError): + self.api.check_selected_tier_job(self.conn, changed) + + def test_embedded_nul_cannot_hide_an_oversized_payload_from_admission(self) -> None: + job = self.select() + for field, bound in (("model_id", 512), ("source_epoch", 128), ("source_schema_signature", 64)): + conn = self.clone(factory=FaultConnection) + conn.execute("PRAGMA ignore_check_constraints=ON") + conn.execute(f"UPDATE {self.table} SET {field}=?", ("a\0" + "x" * (4 * bound),)) + conn.commit() + self.assertEqual(conn.execute(f"SELECT length({field}) FROM {self.table}").fetchone()[0], 1) + with self.assertRaises(self.api.TierJobSelectionError): + self.api.check_selected_tier_job(conn, job) + self.assertEqual(conn.selection_payload_reads, 0) + self.assertFalse(conn.in_transaction) + + def test_marker_byte_limit_accepts_multibyte_model_identity_at_character_bound(self) -> None: + target = self.fixture.modules["embedding_target"].EmbeddingTarget("custom", "\U00010400" * 512, 384, "custom") + job = self.api.select_tier_job(self.conn, target) + self.api.check_selected_tier_job(self.conn, job) + self.assertEqual(len(job.target.model_id.encode("utf-8")), 2048) + self.assertEqual(len(job.target.model_id), 512) + + def test_source_epoch_changed_between_capture_and_writer_refuses_without_marker(self) -> None: + original = self.api.capture_tier_job + def replace_after_capture(conn, target): + job = original(conn, target) + conn.execute("UPDATE truememory_rebuild_source_v1 SET epoch='synthetic-replacement'") + conn.commit() + return job + with patch.object(self.api, "capture_tier_job", replace_after_capture): + with self.assertRaises(self.api.TierJobSelectionError): + self.select() + self.assertIsNone(self.conn.execute("SELECT 1 FROM sqlite_master WHERE name=?", (self.table,)).fetchone()) + self.assertFalse(self.conn.in_transaction) + + def test_temp_or_path_changed_after_capture_is_rechecked_inside_writer(self) -> None: + original = self.api.capture_tier_job + for change in ("temp", "path"): + conn = self.clone() + def change_after_capture(database, target): + job = original(database, target) + if change == "temp": + database.execute("CREATE TEMP TABLE MESSAGES(id INTEGER PRIMARY KEY, content TEXT)") + else: + database.fake_path = str(BASE["SYNTHETIC_PATH"].with_name("other.db")) + return job + with patch.object(self.api, "capture_tier_job", change_after_capture): + with self.assertRaises(self.api.TierJobSelectionError): + self.api.select_tier_job(conn, self.target) + self.assertFalse(conn.in_transaction) + self.assertIsNone(conn.execute("SELECT 1 FROM sqlite_master WHERE name=?", (self.table,)).fetchone()) + + def test_invalid_previous_id_and_oversized_identity_make_no_marker_writes(self) -> None: + for value in (True, 1, "invalid", "0" * 33): + with self.assertRaises(self.api.TierJobSelectionError): + self.select(previous_job_id=value) + target = self.fixture.modules["embedding_target"].EmbeddingTarget("custom", "x" * 513, 384, "custom") + with self.assertRaises(self.api.TierJobSelectionError): + self.api.select_tier_job(self.conn, target) + self.assertIsNone(self.conn.execute("SELECT 1 FROM sqlite_master WHERE name=?", (self.table,)).fetchone()) + self.assertFalse(self.conn.in_transaction) + + def test_marker_state_change_between_admission_and_read_snapshot_refuses(self) -> None: + job = self.select() + original = self.api.check_tier_job + def cancel_after_identity(database, captured): + original(database, captured) + database.execute(f"UPDATE {self.table} SET state='cancelled'") + database.commit() + with patch.object(self.api, "check_tier_job", cancel_after_identity): + with self.assertRaises(self.api.TierJobSelectionError): + self.api.check_selected_tier_job(self.conn, job) + self.assertFalse(self.conn.in_transaction) + + def test_retained_commit_rolls_back_and_never_returns_selected_descriptor(self) -> None: + conn = self.clone(factory=FaultConnection) + conn.retain_commit = True + with self.assertRaises(self.api.TierJobSelectionError): + self.api.select_tier_job(conn, self.target) + self.assertFalse(conn.in_transaction) + self.assertIsNone(conn.execute("SELECT 1 FROM sqlite_master WHERE name=?", (self.table,)).fetchone()) + + def test_source_change_after_marker_insert_rolls_back_entire_selection(self) -> None: + original = self.api._selection_source + calls = [] + def replace_before_final_check(database, job): + calls.append(1) + if len(calls) == 2: + database.execute("UPDATE truememory_rebuild_source_v1 SET epoch='synthetic-raced-epoch'") + original(database, job) + old_epoch = self.fixture.capture().source_epoch + with patch.object(self.api, "_selection_source", replace_before_final_check): + with self.assertRaises(self.api.TierJobSelectionError): + self.select() + self.assertEqual(self.fixture.capture().source_epoch, old_epoch) + self.assertIsNone(self.conn.execute("SELECT 1 FROM sqlite_master WHERE name=?", (self.table,)).fetchone()) + + def test_selection_errors_do_not_include_source_or_target_details(self) -> None: + conn = self.clone(factory=FaultConnection) + conn.fault_sql = "INSERT INTO main.truememory_tier_selected" + conn.fault = sqlite3.OperationalError("synthetic sensitive-path target text") + with self.assertRaises(self.api.TierJobSelectionError) as caught: + self.api.select_tier_job(conn, self.target) + self.assertNotIn("sensitive", str(caught.exception)) + self.assertTrue(caught.exception.__suppress_context__) + + def test_current_source_changes_do_not_rebind_the_marker_to_a_generation(self) -> None: + job = self.select() + before = self.row() + self.conn.execute("INSERT INTO messages VALUES (1,'synthetic appended content')") + self.conn.commit() + self.api.check_selected_tier_job(self.conn, job) + self.assertEqual(self.row(), before) + fields = {row[1] for row in self.conn.execute(f"PRAGMA table_info({self.table})")} + self.assertNotIn("generation", fields) + self.assertNotIn("last_message_id", fields) + self.assertNotIn("count", fields) + + def test_schema_creation_insert_commit_and_cancellation_rollback_together(self) -> None: + for stage in ("CREATE TABLE truememory_tier_selected", "INSERT INTO main.truememory_tier_selected", "commit", "cancel"): + conn = self.clone(factory=FaultConnection) + if stage == "commit": + conn.fail_commit = True + else: + conn.fault_sql = "INSERT INTO main.truememory_tier_selected" if stage == "cancel" else stage + conn.fault = Cancelled("synthetic") if stage == "cancel" else sqlite3.OperationalError("synthetic failure") + expected = Cancelled if stage == "cancel" else self.api.TierJobSelectionError + with self.assertRaises(expected): + self.api.select_tier_job(conn, self.target) + self.assertFalse(conn.in_transaction) + self.assertIsNone(conn.execute("SELECT 1 FROM sqlite_master WHERE name=?", (self.table,)).fetchone()) + + def test_terminal_commit_failure_preserves_selected_marker(self) -> None: + job = self.select() + conn = self.clone(factory=FaultConnection) + conn.fail_commit = True + with self.assertRaises(self.api.TierJobSelectionError): + self.api.end_selected_tier_job(conn, job, outcome="failed") + self.assertFalse(conn.in_transaction) + self.assertEqual(self.row(conn)[2], "selected") + + def test_replacement_commit_failure_retains_previous_terminal_selection(self) -> None: + old = self.select() + self.api.end_selected_tier_job(self.conn, old, outcome="cancelled") + conn = self.clone(factory=FaultConnection) + conn.fail_commit = True + before = self.row(conn) + with self.assertRaises(self.api.TierJobSelectionError): + self.api.select_tier_job(conn, self.target, previous_job_id=old.job_id) + self.assertEqual(self.row(conn), before) + self.assertFalse(conn.in_transaction) + + def test_cancellation_after_insert_rolls_back_marker_and_preserves_exception(self) -> None: + original = self.api._selection_source + sentinel = Cancelled("synthetic after-insert cancellation") + checks = [] + def cancel_after_insert(database, job): + checks.append(1) + if len(checks) == 2: + raise sentinel + original(database, job) + with patch.object(self.api, "_selection_source", cancel_after_insert): + with self.assertRaises(Cancelled) as caught: + self.select() + self.assertIs(caught.exception, sentinel) + self.assertFalse(self.conn.in_transaction) + self.assertIsNone(self.conn.execute("SELECT 1 FROM sqlite_master WHERE name=?", (self.table,)).fetchone()) + + def test_selected_read_rollback_uncertainty_closes_owned_connection(self) -> None: + job = self.select() + reopened = self.fixture.reopen_hooks() + original = self.api._selection_row + def retain_read_snapshot(database): + row = original(database) + database.rollback_failure = "retained" + return row + with patch.object(self.api, "_selection_row", retain_read_snapshot): + with self.assertRaises(self.api.TierJobSelectionError): + with self.api.open_selected_tier_job(job): + self.fail("Uncertain selected read must not yield") + self.assertTrue(reopened[0].closed) + self.assertEqual(self.fixture.events[-1], "release") + + def test_uncertain_rollback_stops_without_additional_status_write(self) -> None: + job = self.select() + for failure in ("retained", sqlite3.OperationalError("synthetic rollback failure")): + conn = self.clone(factory=FaultConnection) + conn.rollback_failure = failure + # The identity read itself cannot release its snapshot. No terminal + # write may follow this uncertainty. + before = conn.total_changes + with self.assertRaises(self.api.TierJobIdentityError): + self.api.end_selected_tier_job(conn, job, outcome="failed") + self.assertTrue(conn.in_transaction) + self.assertEqual(conn.total_changes, before) + conn.rollback_failure = None + conn.rollback() + + def test_writer_rollback_failure_does_not_retry_or_publish_another_status(self) -> None: + conn = self.clone(factory=FaultConnection) + original = self.api.capture_tier_job + def fail_writer_after_capture(database, target): + job = original(database, target) + database.rollback_failure = "retained" + database.fault_sql = "INSERT INTO main.truememory_tier_selected" + database.fault = sqlite3.OperationalError("synthetic write failure") + return job + with patch.object(self.api, "capture_tier_job", fail_writer_after_capture): + with self.assertRaises(self.api.TierJobSelectionError): + self.api.select_tier_job(conn, self.target) + self.assertTrue(conn.in_transaction) + self.assertEqual(conn.total_changes, 0) + conn.rollback_failure = None + conn.rollback() + self.assertIsNone(conn.execute("SELECT 1 FROM sqlite_master WHERE name=?", (self.table,)).fetchone()) + + def test_selections_on_shared_memory_connection_serialize_without_stealing(self) -> None: + uri = "file:synthetic-tier-binding?mode=memory&cache=shared" + first = CONNECT(uri, uri=True, factory=CONNECTION) + second = CONNECT(uri, uri=True, factory=CONNECTION) + self.addCleanup(first.close) + self.addCleanup(second.close) + self.conn.backup(first) + job = self.api.select_tier_job(first, self.target) + with self.assertRaises(self.api.TierJobSelectionError): + self.api.select_tier_job(second, self.target) + self.api.check_selected_tier_job(second, job) + self.assertTrue(self.api.end_selected_tier_job(second, job, outcome="cancelled")) + next_job = self.api.select_tier_job(first, self.target, previous_job_id=job.job_id) + self.assertFalse(self.api.end_selected_tier_job(second, job, outcome="failed")) + self.api.check_selected_tier_job(first, next_job) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-tier-job-identity-795.py b/tests/test-tier-job-identity-795.py new file mode 100644 index 00000000..58a73e4c --- /dev/null +++ b/tests/test-tier-job-identity-795.py @@ -0,0 +1,528 @@ +"""Synthetic job admission; local tests use mocked files and in-memory SQLite.""" + +import builtins +import dataclasses +import os +import sqlite3 +import stat +import sys +import threading +import types +import unittest +from contextlib import contextmanager +from pathlib import Path +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] +CONNECT = sqlite3.connect +SYNTHETIC_PATH = Path(Path.cwd().anchor) / "synthetic" / "tier database?#.db" + + +def load_modules() -> dict[str, types.ModuleType]: + modules = {} + + def safe_import(name, globals=None, locals=None, fromlist=(), level=0): + if name in {"numpy", "torch", "sentence_transformers", "model2vec", "sqlite_vec"}: + raise AssertionError("Native/model imports are excluded from job admission") + if name.startswith("truememory."): + short = name.removeprefix("truememory.") + if short in modules: + return modules[short] + raise AssertionError("Unexpected application import: " + name) + return builtins.__import__(name, globals, locals, fromlist, level) + + for name in ("storage", "_platform", "maintenance", "rebuild_source", "embedding_target", "tier_switch.job"): + module = types.ModuleType("synthetic_tier_job_" + name.replace(".", "_")) + sys.modules[module.__name__] = module + module.__dict__["__builtins__"] = dict(vars(builtins), __import__=safe_import) + path = ROOT / "truememory" / (name.replace(".", "/") + ".py") + exec(compile(path.read_text(encoding="utf-8"), str(path), "exec"), module.__dict__) + modules[name] = module + return modules + + +class MockFileConnection(sqlite3.Connection): + closed = False + fake_path = str(SYNTHETIC_PATH) + extra_database = False + rollback_failure = None + close_failure = None + + def execute(self, sql: str, parameters: tuple = ()): + if sql == "PRAGMA database_list": + rows = [(0, "main", self.fake_path)] + if self.extra_database: + rows.append((2, "extra", "/synthetic/attached.db")) + return rows + return super().execute(sql, parameters) + + def close(self) -> None: + self.closed = True + super().close() + if self.close_failure is not None: + raise self.close_failure + + def rollback(self) -> None: + if self.rollback_failure == "retained": + return + if self.rollback_failure is not None: + raise self.rollback_failure + super().rollback() + + +class Cancelled(BaseException): + pass + + +class TestTierJobIdentity(unittest.TestCase): + def setUp(self) -> None: + self.modules = load_modules() + self.api = self.modules["tier_switch.job"] + self.source = self.modules["rebuild_source"] + self.target = self.modules["embedding_target"].EmbeddingTarget("base", "qwen3_256", 256, "basepro") + self.conn = CONNECT(":memory:", factory=MockFileConnection) + self.addCleanup(self.conn.close) + self.conn.execute("CREATE TABLE messages(id INTEGER PRIMARY KEY, content TEXT)") + self.source.ensure_rebuild_tracking(self.conn) + self.identity = (7, 19) + self.observations = 0 + self.events = [] + + @contextmanager + def observe(path): + self.assertEqual(path, SYNTHETIC_PATH) + self.observations += 1 + self.events.append("file") + yield self.identity + + self.observe = observe + self.addCleanup(patch.stopall) + patch.object(self.api, "_observe_file", observe).start() + patch.object(self.api.Path, "resolve", lambda path, strict=False: path).start() + + def capture(self): + return self.api.capture_tier_job(self.conn, self.target) + + def reopen_hooks(self, *, before_open=None, opened=None): + @contextmanager + def owner(path): + self.assertEqual(path, SYNTHETIC_PATH) + self.events.append(("owner", threading.get_ident())) + try: + yield object() + finally: + self.events.append("release") + + reopened = [] + + def connect(database, **kwargs): + self.events.append("connect") + self.assertEqual(database, SYNTHETIC_PATH.as_uri() + "?mode=rw") + self.assertEqual(kwargs, {"uri": True}) + if before_open is not None: + before_open() + conn = CONNECT(":memory:", factory=MockFileConnection) + self.conn.backup(conn) + reopened.append(conn) + if opened is not None: + opened(conn) + return conn + + patch.object(self.api, "maintenance_owner", owner).start() + patch.object(self.api.sqlite3, "connect", connect).start() + return reopened + + def test_capture_is_frozen_read_only_and_retains_no_connection(self) -> None: + changes = self.conn.total_changes + job = self.capture() + self.assertEqual(self.conn.total_changes, changes) + self.assertFalse(self.conn.in_transaction) + self.assertEqual(job.target, self.target) + self.assertEqual(job.database_path, SYNTHETIC_PATH) + self.assertEqual(job.file_identity, (7, 19)) + self.assertEqual(job.tracker, "rebuild-bridge-v1") + self.assertNotEqual(job.job_id, self.capture().job_id) + self.assertNotIn(str(SYNTHETIC_PATH), repr(job)) + self.assertEqual({field.name for field in dataclasses.fields(job)}, { + "job_id", "target", "database_path", "file_identity", "tracker", "source_epoch", "source_schema_signature", + }) + with self.assertRaises(dataclasses.FrozenInstanceError): + job.job_id = "changed" + + def test_missing_tracking_never_installs_it(self) -> None: + conn = CONNECT(":memory:", factory=MockFileConnection) + self.addCleanup(conn.close) + conn.execute("CREATE TABLE messages(id INTEGER PRIMARY KEY, content TEXT)") + before = conn.total_changes + with self.assertRaises(self.api.TierJobIdentityError): + self.api.capture_tier_job(conn, self.target) + self.assertEqual(conn.total_changes, before) + self.assertFalse(conn.in_transaction) + self.assertEqual(conn.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall(), [("messages",)]) + + def test_borrowed_transaction_is_untouched(self) -> None: + job = self.capture() + self.conn.execute("BEGIN") + for call in (lambda: self.capture(), lambda: self.api.check_tier_job(self.conn, job)): + with self.assertRaises(self.api.TierJobIdentityError): + call() + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + + def test_memory_and_attachments_fail_before_file_observation(self) -> None: + for path, attached in (("", False), (str(SYNTHETIC_PATH), True)): + with self.subTest(path=path, attached=attached): + self.conn.fake_path = path + self.conn.extra_database = attached + with self.assertRaises(self.api.TierJobIdentityError): + self.capture() + self.assertEqual(self.observations, 0) + with CONNECT(":memory:") as real_memory: + with self.assertRaises(self.api.TierJobIdentityError): + self.api.capture_tier_job(real_memory, self.target) + real_memory.close() + + def test_temp_shadow_and_unrelated_temp_objects_fail_closed(self) -> None: + for ddl in ("CREATE TEMP TABLE MESSAGES(id)", "CREATE TEMP VIEW unrelated AS SELECT 1"): + with self.subTest(ddl=ddl): + conn = CONNECT(":memory:", factory=MockFileConnection) + try: + self.conn.backup(conn) + conn.execute(ddl) + with self.assertRaises(self.api.TierJobIdentityError): + self.api.capture_tier_job(conn, self.target) + finally: + conn.close() + self.assertEqual(self.observations, 0) + + def test_source_mutations_with_same_epoch_are_adapter_responsibility(self) -> None: + job = self.capture() + self.conn.execute("INSERT INTO messages VALUES (1,'synthetic source')") + self.conn.commit() + self.api.check_tier_job(self.conn, job) + self.conn.execute("UPDATE messages SET content='synthetic correction'") + self.conn.commit() + self.api.check_tier_job(self.conn, job) + self.assertFalse(self.conn.in_transaction) + + def test_epoch_schema_tracker_and_trigger_changes_refuse(self) -> None: + job = self.capture() + actions = ( + "UPDATE truememory_rebuild_source_v1 SET epoch='replacement'", + "ALTER TABLE messages ADD COLUMN synthetic TEXT", + "DROP TRIGGER truememory_rebuild_messages_v1_ai", + ) + for sql in actions: + with self.subTest(sql=sql): + conn = CONNECT(":memory:", factory=MockFileConnection) + try: + self.conn.backup(conn) + conn.execute(sql) + conn.commit() + with self.assertRaises(self.api.TierJobIdentityError): + self.api.check_tier_job(conn, job) + self.assertFalse(conn.in_transaction) + finally: + conn.close() + altered = dataclasses.replace(job, tracker="canonical-v1") + with self.assertRaises(self.api.TierJobIdentityError): + self.api.check_tier_job(self.conn, altered) + + def test_replaced_file_refuses_before_connect(self) -> None: + job = self.capture() + reopened = self.reopen_hooks() + self.identity = (7, 20) + with self.assertRaises(self.api.TierJobIdentityError): + with self.api.open_tier_job(job): + self.fail("Replacement must not yield") + self.assertEqual(reopened, []) + self.assertNotIn("connect", self.events) + self.assertEqual(self.events[-1], "release") + + def test_change_during_open_closes_before_yield(self) -> None: + job = self.capture() + reopened = self.reopen_hooks(before_open=lambda: setattr(self, "identity", (7, 20))) + with self.assertRaises(self.api.TierJobIdentityError): + with self.api.open_tier_job(job): + self.fail("Raced open must not yield") + self.assertEqual(len(reopened), 1) + self.assertTrue(reopened[0].closed) + self.assertEqual(self.events[-1], "release") + + def test_reopened_epoch_and_pragma_path_checked_before_yield(self) -> None: + job = self.capture() + for change in ("epoch", "path"): + def opened(conn): + if change == "epoch": + conn.execute("UPDATE truememory_rebuild_source_v1 SET epoch='replacement'") + conn.commit() + else: + conn.fake_path = "/synthetic/other.db" + reopened = self.reopen_hooks(opened=opened) + with self.assertRaises(self.api.TierJobIdentityError): + with self.api.open_tier_job(job): + self.fail("Unmatched reopened identity must not yield") + self.assertTrue(reopened[0].closed) + + def test_reopen_is_rw_only_owner_is_in_calling_worker_thread_and_cleanup_is_exact(self) -> None: + job = self.capture() + self.events.clear() + reopened = self.reopen_hooks() + sentinel = Cancelled("synthetic cancellation") + with self.assertRaises(Cancelled) as raised: + with self.api.open_tier_job(job) as conn: + self.assertFalse(conn.in_transaction) + self.api.check_tier_job(conn, job) + conn.execute("BEGIN") + conn.execute("INSERT INTO messages VALUES (1,'uncommitted synthetic row')") + raise sentinel + self.assertIs(raised.exception, sentinel) + self.assertEqual(self.events[0], ("owner", threading.get_ident())) + self.assertTrue(reopened[0].closed) + self.assertEqual(self.events[-1], "release") + self.assertEqual(self.conn.execute("SELECT COUNT(*) FROM messages").fetchone()[0], 0) + + def test_body_oserror_is_not_relabelled_as_identity_failure(self) -> None: + job = self.capture() + self.reopen_hooks() + sentinel = OSError("synthetic caller operation") + with self.assertRaises(OSError) as raised: + with self.api.open_tier_job(job): + raise sentinel + self.assertIs(raised.exception, sentinel) + + def test_open_and_source_failure_close_and_release(self) -> None: + job = self.capture() + self.reopen_hooks() + for error in (sqlite3.OperationalError("/private/path synthetic"), Cancelled("synthetic cancellation")): + with patch.object(self.api.sqlite3, "connect", side_effect=error): + expected = Cancelled if isinstance(error, Cancelled) else self.api.TierJobIdentityError + with self.assertRaises(expected) as caught: + with self.api.open_tier_job(job): + self.fail("Failed connect must not yield") + self.assertEqual(self.events[-1], "release") + if expected is self.api.TierJobIdentityError: + self.assertNotIn("/private", str(caught.exception)) + self.assertTrue(caught.exception.__suppress_context__) + reopened = self.reopen_hooks() + with patch.object(self.api, "read_rebuild_revision", side_effect=Cancelled("synthetic snapshot cancellation")): + with self.assertRaises(Cancelled): + with self.api.open_tier_job(job): + self.fail("Cancelled source read must not yield") + self.assertTrue(reopened[0].closed) + self.assertEqual(self.events[-1], "release") + + def test_snapshot_failure_rolls_back_and_does_not_log_source_error(self) -> None: + sentinel = sqlite3.OperationalError("synthetic identifying source detail") + with patch.object(self.api, "read_rebuild_revision", side_effect=sentinel): + with self.assertRaises(self.api.TierJobIdentityError) as caught: + self.capture() + self.assertFalse(self.conn.in_transaction) + self.assertNotIn("identifying", str(caught.exception)) + self.assertTrue(caught.exception.__suppress_context__) + + def test_uncertain_snapshot_rollback_propagates_and_owned_reopen_closes(self) -> None: + job = self.capture() + for failure in ("retained", sqlite3.OperationalError("synthetic rollback failure")): + self.conn.rollback_failure = failure + with self.assertRaises(self.api.TierJobIdentityError): + self.capture() + self.assertTrue(self.conn.in_transaction) + self.conn.rollback_failure = None + self.conn.rollback() + reopened = self.reopen_hooks(opened=lambda conn: setattr(conn, "rollback_failure", failure)) + with self.assertRaises(self.api.TierJobIdentityError): + with self.api.open_tier_job(job): + self.fail("Uncertain snapshot cleanup must not yield") + self.assertTrue(reopened[0].closed) + self.assertEqual(self.events[-1], "release") + + def test_connection_close_failure_still_releases_owner_and_is_sanitized(self) -> None: + job = self.capture() + reopened = self.reopen_hooks(opened=lambda conn: setattr( + conn, "close_failure", sqlite3.OperationalError("/private/synthetic.db"), + )) + with self.assertRaises(self.api.TierJobIdentityError) as caught: + with self.api.open_tier_job(job): + pass + self.assertTrue(reopened[0].closed) + self.assertEqual(self.events[-1], "release") + self.assertNotIn("/private", str(caught.exception)) + + def test_descriptor_handoff_acquires_owner_only_inside_worker(self) -> None: + job = self.capture() + dump = "\n".join(self.conn.iterdump()) + caller_thread = threading.get_ident() + owners = [] + results = [] + opened = [] + @contextmanager + def owner(path): + owners.append(threading.get_ident()) + try: + yield object() + finally: + owners.append("released") + def connect(database, **kwargs): + conn = CONNECT(":memory:", factory=MockFileConnection) + conn.executescript(dump) + opened.append(conn) + return conn + def worker(): + try: + with self.api.open_tier_job(job) as conn: + results.append((threading.get_ident(), conn.in_transaction)) + except BaseException as error: + results.append(type(error).__name__) + with patch.object(self.api, "maintenance_owner", owner), patch.object(self.api.sqlite3, "connect", connect): + thread = threading.Thread(target=worker) + thread.start() + thread.join(timeout=3) + self.assertFalse(thread.is_alive()) + self.assertEqual(len(results), 1) + self.assertEqual(results[0], (thread.ident, False)) + self.assertEqual(owners, [thread.ident, "released"]) + self.assertNotEqual(thread.ident, caller_thread) + self.assertTrue(opened[0].closed) + + def test_busy_owner_prevents_all_file_and_database_opening(self) -> None: + job = self.capture() + self.events.clear() + reopened = self.reopen_hooks() + with patch.object(self.api, "maintenance_owner", side_effect=self.modules["maintenance"].MaintenanceBusyError("synthetic busy")): + with self.assertRaises(self.modules["maintenance"].MaintenanceBusyError): + with self.api.open_tier_job(job): + self.fail("Busy owner must not yield") + self.assertEqual(reopened, []) + self.assertEqual(self.events, []) + + def test_missing_path_is_sanitized_and_no_reopen_occurs(self) -> None: + job = self.capture() + reopened = self.reopen_hooks() + with patch.object(self.api, "_observe_file", side_effect=FileNotFoundError("/private/synthetic.db")): + with self.assertRaises(self.api.TierJobIdentityError) as caught: + with self.api.open_tier_job(job): + self.fail("Missing database must not yield") + self.assertEqual(reopened, []) + self.assertNotIn("/private", str(caught.exception)) + + def test_capture_and_final_check_detect_second_observation_change(self) -> None: + original = self.api.read_rebuild_revision + def replace_after_read(conn): + result = original(conn) + self.identity = (7, 20) + return result + job = self.capture() + for operation in (lambda: self.capture(), lambda: self.api.check_tier_job(self.conn, job)): + self.identity = (7, 19) + with patch.object(self.api, "read_rebuild_revision", replace_after_read): + with self.assertRaises(self.api.TierJobIdentityError): + operation() + self.assertFalse(self.conn.in_transaction) + + def test_same_epoch_stale_connection_cannot_be_disproved_by_path_observations(self) -> None: + # Explicit limitation: capture assumes this connection still names the + # current file. A copied epoch plus a stale handle violates that contract. + self.identity = (7, 99) + job = self.capture() + reopened = self.reopen_hooks() + with self.api.open_tier_job(job) as conn: + self.api.check_tier_job(conn, job) + self.assertEqual(job.file_identity, (7, 99)) + self.assertTrue(reopened[0].closed) + + +class TestFileObservation(unittest.TestCase): + def test_same_handle_api_ignores_mutable_and_cross_api_ctime(self) -> None: + api = load_modules()["tier_switch.job"] + value = types.SimpleNamespace(st_mode=stat.S_IFREG, st_dev=5, st_ino=19, st_ctime_ns=10) + with patch.object(api.os, "open", return_value=81) as opened, patch.object(api.os, "fstat", return_value=value) as fstat, patch.object(api.os, "close") as closed: + with api._observe_file(SYNTHETIC_PATH) as first: + value.st_ctime_ns = 999 + with api._observe_file(SYNTHETIC_PATH) as second: + self.assertEqual(first, second) + self.assertEqual(first, (5, 19)) + self.assertEqual(fstat.call_count, 2) + self.assertEqual(opened.call_args.args, (SYNTHETIC_PATH, os.O_RDONLY | getattr(os, "O_NONBLOCK", 0) | getattr(os, "O_BINARY", 0))) + self.assertEqual(closed.call_count, 2) + self.assertEqual(closed.call_args.args, (81,)) + + def test_unsupported_identity_and_open_errors_fail_closed_without_path(self) -> None: + api = load_modules()["tier_switch.job"] + for mode, device, inode in ((stat.S_IFDIR, 5, 19), (stat.S_IFIFO, 5, 19), (stat.S_IFREG, 5, 0), (stat.S_IFREG, -1, 19), (stat.S_IFREG, True, 19)): + value = types.SimpleNamespace(st_mode=mode, st_dev=device, st_ino=inode) + with patch.object(api.os, "open", return_value=81), patch.object(api.os, "fstat", return_value=value), patch.object(api.os, "close") as closed: + with self.assertRaises(api.TierJobIdentityError): + with api._observe_file(SYNTHETIC_PATH): + self.fail("Unsupported identity must not yield") + self.assertEqual(closed.call_count, 1) + with patch.object(api.os, "open", side_effect=OSError("/private/synthetic.db")): + with self.assertRaises(api.TierJobIdentityError) as caught: + with api._observe_file(SYNTHETIC_PATH): + self.fail("Failed file observation must not yield") + self.assertNotIn("/private", str(caught.exception)) + + def test_observer_preserves_caller_baseexception_and_oserror(self) -> None: + api = load_modules()["tier_switch.job"] + value = types.SimpleNamespace(st_mode=stat.S_IFREG, st_dev=5, st_ino=19) + for sentinel in (Cancelled("synthetic"), OSError("synthetic"), ValueError("synthetic")): + with patch.object(api.os, "open", return_value=81), patch.object(api.os, "fstat", return_value=value), patch.object(api.os, "close") as closed: + with self.assertRaises(type(sentinel)) as raised: + with api._observe_file(SYNTHETIC_PATH): + raise sentinel + self.assertIs(raised.exception, sentinel) + self.assertEqual(closed.call_count, 1) + + def test_fstat_cancellation_and_failure_close_the_readonly_descriptor(self) -> None: + api = load_modules()["tier_switch.job"] + for sentinel in (Cancelled("synthetic"), OSError("synthetic")): + with patch.object(api.os, "open", return_value=81), patch.object(api.os, "fstat", side_effect=sentinel), patch.object(api.os, "close") as closed: + expected = Cancelled if isinstance(sentinel, Cancelled) else api.TierJobIdentityError + with self.assertRaises(expected): + with api._observe_file(SYNTHETIC_PATH): + self.fail("Failed observation must not yield") + self.assertEqual(closed.call_args.args, (81,)) + + +@unittest.skipUnless(os.environ.get("TRUEMEMORY_TIER_JOB_FILE_TEST") == "1", "opt-in synthetic file DB gate only") +class TestTierJobFileDatabase(unittest.TestCase): + def test_existing_database_reopen_replacement_missing_and_worker_ownership(self) -> None: + import tempfile + + modules = load_modules() + api = modules["tier_switch.job"] + target = modules["embedding_target"].EmbeddingTarget("base", "qwen3_256", 256, "basepro") + with tempfile.TemporaryDirectory(prefix="truememory-synthetic-job-") as directory: + path = Path(directory) / "synthetic.db" + conn = CONNECT(path) + try: + conn.execute("CREATE TABLE messages(id INTEGER PRIMARY KEY, content TEXT)") + modules["rebuild_source"].ensure_rebuild_tracking(conn) + job = api.capture_tier_job(conn, target) + finally: + conn.close() + outcomes = [] + def worker(): + with api.open_tier_job(job) as opened: + api.check_tier_job(opened, job) + outcomes.append(not opened.in_transaction) + thread = threading.Thread(target=worker) + thread.start() + thread.join(timeout=5) + self.assertFalse(thread.is_alive()) + self.assertEqual(outcomes, [True]) + archived = path.with_suffix(".old") + path.rename(archived) + with self.assertRaises(api.TierJobIdentityError): + with api.open_tier_job(job): + self.fail("Missing file must not yield") + self.assertFalse(path.exists()) + replacement = CONNECT(path) + replacement.close() + with self.assertRaises(api.TierJobIdentityError): + with api.open_tier_job(job): + self.fail("Replaced file must not yield") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-tier-mcp-serving-795.py b/tests/test-tier-mcp-serving-795.py new file mode 100644 index 00000000..9187726f --- /dev/null +++ b/tests/test-tier-mcp-serving-795.py @@ -0,0 +1,306 @@ +"""MCP policy capture and child cleanup with actual gates and synthetic peers.""" +from __future__ import annotations + +import ast +import builtins +import json +import logging +import runpy +import sqlite3 +import threading +import time +import types +import unittest +from collections.abc import Iterator +from concurrent.futures import ThreadPoolExecutor +from contextlib import contextmanager +from pathlib import Path +from unittest.mock import Mock, patch + +ROOT = Path(__file__).resolve().parents[1] +GATE = runpy.run_path(str(ROOT / "truememory/tier_switch/serving.py")) + + +class PolicyRejected(RuntimeError): + pass + + +class TestMCPServing(unittest.TestCase): + def setUp(self) -> None: + self.events = [] + self.gate = GATE["ServingGate"]() + self.tier = "pro" + self.llm = object() + self.deep_llm = None + self.query_failure = None + self.construction_failure = False + self.maintenance_requests = [] + owner = self + + class Memory: + def __init__(self, path="synthetic"): + if owner.construction_failure: + raise OSError("synthetic open failure") + self._engine = types.SimpleNamespace(db_path=path, conn=object(), _write_lock=threading.Lock()) + self._engine._open_connection_handle = lambda: owner.events.append(("open", self._engine.conn)) + self._engine._runtime_initialized = True + self._engine.ready = True + def request_maintenance(): + owner.assert_drained() + owner.maintenance_requests.append(self._engine.conn) + self._engine._maybe_auto_consolidate = request_maintenance + self._engine._maybe_startup_consolidate = request_maintenance + + def __enter__(self): + return self + + def __exit__(self, *exc): + owner.events.append(("close", self._engine.conn)) + + def search_deep(self, query, **kwargs): + owner.events.append(("query", query, kwargs)) + if owner.query_failure is not None: + raise owner.query_failure + return [{"id": "shared", "score": 2}, {"id": query, "score": 1}] + + self.memory = Memory() + + @contextmanager + def serving_operation(conn, *, connection_lock=None, reranker_id=None, _defer_maintenance=False): + owner.events.append(("admit", conn, reranker_id)) + with owner.gate.operation_lease(("synthetic-generation",)) as lease: + def fork_child(): + child = lease.fork_child() + owner.events.append(("reserve", child)) + + @contextmanager + def join(*, conn): + owner.events.append(("join", conn)) + with child.join(): + yield + + return types.SimpleNamespace(join=join, close=child.close) + + yield types.SimpleNamespace(tier=owner.tier, reranker_id=reranker_id or "synthetic/default", + fork_child=fork_child, _defer_maintenance=_defer_maintenance) + owner.events.append(("release", conn)) + + def reject(error): + if isinstance(error, PolicyRejected): + raise error + + runtime = types.SimpleNamespace(serving_operation=serving_operation, raise_if_serving_rejection=reject) + + maintenance_tree = ast.parse((ROOT / "truememory/maintenance.py").read_text(encoding="utf-8")) + maintenance_error = next(node for node in maintenance_tree.body + if isinstance(node, ast.ClassDef) and node.name == "MaintenanceBusyError") + maintenance_namespace = {} + exec(compile(ast.fix_missing_locations(ast.Module(body=[maintenance_error], type_ignores=[])), + "actual-maintenance-error", "exec"), maintenance_namespace) + storage_tree = ast.parse((ROOT / "truememory/storage.py").read_text(encoding="utf-8")) + budget = next(ast.literal_eval(node.value) for node in storage_tree.body if isinstance(node, ast.Assign) + and any(isinstance(target, ast.Name) and target.id == "DEFAULT_BUSY_TIMEOUT_MS" + for target in node.targets)) + modules = { + "truememory.tier_switch.runtime": runtime, + "truememory.maintenance": types.SimpleNamespace( + MaintenanceBusyError=maintenance_namespace["MaintenanceBusyError"], + wait_for_automatic_owner=Mock(side_effect=AssertionError("Unexpected automatic owner wait in healthy MCP fixture"))), + "truememory.storage": types.SimpleNamespace(DEFAULT_BUSY_TIMEOUT_MS=budget), + } + + def safe_import(name, globals=None, locals=None, fromlist=(), level=0): + if name in modules: + return modules[name] + if name.startswith("truememory") or name.split(".")[0] in { + "numpy", "torch", "sqlite_vec", "model2vec", "sentence_transformers"}: + raise AssertionError("Unexpected native/application import in MCP fixture: " + name) + return builtins.__import__(name, globals, locals, fromlist, level) + + runtime_tree = ast.parse((ROOT / "truememory/tier_switch/runtime.py").read_text(encoding="utf-8")) + context_names = {"open_engine_for_operation", "engine_serving_operation"} + context_definitions = [node for node in runtime_tree.body + if isinstance(node, ast.FunctionDef) and node.name in context_names] + self.assertEqual({node.name for node in context_definitions}, context_names) + writer_class = next(node for node in runtime_tree.body + if isinstance(node, ast.ClassDef) and node.name == "ConnectionWriteLock") + context_namespace = dict(__builtins__=dict(vars(builtins), __import__=safe_import), + contextmanager=contextmanager, serving_operation=serving_operation, + current_operation=lambda conn: None, time=time, threading=threading, sqlite3=sqlite3) + future = ast.ImportFrom(module="__future__", names=[ast.alias(name="annotations")], level=0) + exec(compile(ast.fix_missing_locations(ast.Module(body=[future, writer_class, *context_definitions], type_ignores=[])), + "actual-public-context", "exec"), context_namespace) + runtime.engine_serving_operation = context_namespace["engine_serving_operation"] + + names = {"_search_operation", "_parallel_search", "_resolve_deepsearch_llm", + "truememory_search", "truememory_search_deep"} + tree = ast.parse((ROOT / "truememory/mcp_server.py").read_text(encoding="utf-8")) + nodes = [ast.ImportFrom(module="__future__", names=[ast.alias(name="annotations")], level=0)] + for node in tree.body: + if isinstance(node, ast.FunctionDef) and node.name in names: + node.decorator_list = [d for d in node.decorator_list + if isinstance(d, ast.Name) and d.id == "contextmanager"] + nodes.append(node) + self.ns = dict(__builtins__=dict(vars(builtins), __import__=safe_import), contextmanager=contextmanager, + Iterator=Iterator, Memory=Memory, ThreadPoolExecutor=ThreadPoolExecutor, json=json, threading=threading, + log=logging.getLogger(__name__), _get_memory=lambda: self.memory, + _get_llm_fn=lambda: self.llm, _get_deepsearch_llm_fn=lambda: self.deep_llm, + _load_config=lambda: {"tier": "edge"}, _touch_search_time=lambda: None, + _set_reranker=lambda name: self.events.append(("reranker", name)), + _SEARCH_INTERNAL_LIMIT=100, _DEEP_INTERNAL_LIMIT=500, + _DEEP_RERANKER="BAAI/bge-reranker-v2-m3") + exec(compile(ast.fix_missing_locations(ast.Module(body=nodes, type_ignores=[])), + "actual-mcp-serving", "exec"), self.ns) + + def assert_drained(self) -> None: + with self.gate.exclusive_activation(timeout=0.1): + pass + + def test_outer_request_schedules_once_after_all_children_release(self) -> None: + self.ns["truememory_search"]("first | second") + self.assertEqual(self.maintenance_requests, [self.memory._engine.conn]) + self.assertEqual(self.events[-1][0], "release") + self.assert_drained() + + def test_failed_child_schedules_once_after_request_cleanup(self) -> None: + self.query_failure = PolicyRejected("synthetic rejection") + with self.assertRaises(PolicyRejected): + self.ns["truememory_search"]("first | second") + self.assertEqual(self.maintenance_requests, [self.memory._engine.conn]) + self.assert_drained() + + def test_parallel_handles_open_serially_and_release_before_search(self) -> None: + first_opened = threading.Event() + both_constructed = threading.Event() + second_opened = threading.Event() + release = threading.Event() + mutex = threading.Lock() + constructed = [] + opened = [] + errors = [] + outputs = [] + owner = self + original = self.ns["Memory"] + + class ConcurrentMemory(original): + def __init__(self, path="synthetic"): + super().__init__(path) + with mutex: + constructed.append(self) + if len(constructed) == 2: + both_constructed.set() + def open_handle(): + with mutex: + opened.append(self) + number = len(opened) + if number == 1: + first_opened.set() + if not release.wait(2): + raise AssertionError("First open was not released") + else: + second_opened.set() + self._engine._open_connection_handle = open_handle + + def search_deep(self, query, **kwargs): + owner.assertTrue(second_opened.wait(2), "Opening lock was retained during search") + return super().search_deep(query, **kwargs) + + self.ns["Memory"] = ConcurrentMemory + def run_request(): + try: + outputs.extend(json.loads(self.ns["truememory_search"]("first | second"))) + except BaseException as error: + errors.append(error) + worker = threading.Thread(target=run_request) + worker.start() + try: + self.assertTrue(first_opened.wait(2)) + self.assertTrue(both_constructed.wait(2)) + self.assertFalse(second_opened.wait(0.05), "Sibling opened during the first maintenance owner") + self.assertEqual(len(opened), 1) + finally: + release.set() + worker.join(5) + self.assertFalse(worker.is_alive()) + self.assertEqual(errors, []) + self.assertEqual(len(opened), 2) + self.assertEqual([row["id"] for row in outputs], ["shared", "first", "second"]) + self.assertEqual(self.maintenance_requests, [self.memory._engine.conn]) + self.assert_drained() + + def test_standard_uses_admitted_policy_despite_old_config(self) -> None: + result = json.loads(self.ns["truememory_search"]("synthetic", limit=1)) + query = next(event for event in self.events if event[0] == "query") + self.assertEqual(len(result), 1) + self.assertEqual(query[2]["limit"], 100) + self.assertIs(query[2]["llm_fn"], self.llm) + self.assertEqual(self.events[0][0], "open") + self.assertEqual(self.events[1][0], "admit") + self.assertEqual(self.events[-1][0], "release") + self.assert_drained() + + def test_deep_override_and_edge_explicit_llm_survive(self) -> None: + self.tier = "edge" + self.deep_llm = object() + self.ns["truememory_search_deep"]("synthetic") + query = next(event for event in self.events if event[0] == "query") + self.assertEqual(self.events[1][2], "BAAI/bge-reranker-v2-m3") + self.assertEqual(query[2]["limit"], 500) + self.assertIs(query[2]["llm_fn"], self.deep_llm) + self.assertIn(("reranker", "BAAI/bge-reranker-v2-m3"), self.events) + self.assert_drained() + + def test_deep_default_llm_is_pro_only(self) -> None: + self.assertIsNone(self.ns["_resolve_deepsearch_llm"]("edge")) + self.assertIs(self.ns["_resolve_deepsearch_llm"]("pro"), self.llm) + + def test_parallel_children_join_and_release_before_parent(self) -> None: + result = json.loads(self.ns["truememory_search"]("first | second")) + self.assertEqual([row["id"] for row in result], ["shared", "first", "second"]) + self.assertEqual(sum(e[0] == "reserve" for e in self.events), 2) + self.assertEqual(sum(e[0] == "join" for e in self.events), 2) + self.assertEqual(sum(e[0] == "close" for e in self.events), 2) + self.assertEqual(self.events[-1][0], "release") + self.assert_drained() + + def test_identity_rejection_cannot_return_partial_success(self) -> None: + self.query_failure = PolicyRejected("synthetic stale selection") + with self.assertRaises(PolicyRejected): + self.ns["truememory_search"]("first | second") + self.assert_drained() + + def test_failed_child_constructor_releases_unused_reservation(self) -> None: + self.construction_failure = True + self.assertEqual(json.loads(self.ns["truememory_search"]("first | second")), []) + self.assertEqual(sum(e[0] == "reserve" for e in self.events), 2) + self.assert_drained() + + def test_failed_submission_releases_every_reserved_child(self) -> None: + original = ThreadPoolExecutor.submit + calls = 0 + + def submit(pool, *args, **kwargs): + nonlocal calls + calls += 1 + if calls == 2: + raise RuntimeError("synthetic submission failure") + return original(pool, *args, **kwargs) + + with patch.object(ThreadPoolExecutor, "submit", submit): + with self.assertRaisesRegex(RuntimeError, "submission"): + self.ns["truememory_search"]("first | second") + self.assert_drained() + + def test_input_caps_and_empty_input_are_preserved(self) -> None: + self.assertEqual(self.ns["truememory_search"](" "), "[]") + self.assertEqual(self.events, []) + self.ns["truememory_search"]("", queries=[str(n) + "x" * 2100 for n in range(12)], limit=999) + queries = [e for e in self.events if e[0] == "query"] + self.assertEqual(len(queries), 10) + self.assertTrue(all(len(e[1]) == 2000 for e in queries)) + self.assertTrue(all(e[2]["limit"] == 100 for e in queries)) + self.assert_drained() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-tier-metadata-publication-795.py b/tests/test-tier-metadata-publication-795.py new file mode 100644 index 00000000..5f3488cd --- /dev/null +++ b/tests/test-tier-metadata-publication-795.py @@ -0,0 +1,176 @@ +"""Foreground metadata publication under actual serving and writer guards.""" +from __future__ import annotations + +import datetime +from pathlib import Path +import runpy +import sqlite3 +import unittest +from unittest.mock import Mock + +BOUNDARIES = runpy.run_path(str(Path(__file__).with_name("test-tier-public-boundaries-795.py"))) + + +class TestForegroundMetadata(unittest.TestCase): + def setUp(self) -> None: + self.fixture = BOUNDARIES["TestPublicBoundaries"]() + self.addCleanup(self.fixture.doCleanups) + self.fixture.setUp() + self.conn, self.runtime = self.fixture.conn, self.fixture.api + self.writer = self.fixture.load_application("tier_switch.writer") + self.vector = self.fixture.fixture.vector + self.source = BOUNDARIES["definitions"]( + "vector_search.py", + {"VectorPublicationChanged", "_write_foreground_embedder_metadata_no_commit", + "_write_embedder_metadata_no_commit", "_write_embedder_metadata"}, + self.fixture.namespace(datetime=datetime, _model_generation=7, + EMBEDDING_MODEL=self.vector.EMBEDDING_MODEL, + _embedding_dim=self.vector._embedding_dim), + ) + self.identity = (7, self.source.EMBEDDING_MODEL, self.source._embedding_dim) + self.conn.execute("DELETE FROM main.metadata WHERE key IN ('embed_model','embed_dim')") + self.conn.commit() + self.owner = Mock(side_effect=RuntimeError("Synthetic maintenance owner busy")) + self.fixture.modules["maintenance"].maintenance_owner = self.owner + + def publish(self, selection: object) -> None: + self.source._write_foreground_embedder_metadata_no_commit( + self.conn, self.identity, selection=selection) + + def rows(self) -> list: + return self.conn.execute( + "SELECT key,value FROM main.metadata WHERE key IN ('embed_model','embed_dim') ORDER BY key" + ).fetchall() + + def test_initial_and_matching_publication_do_not_acquire_maintenance(self) -> None: + with self.runtime.serving_operation(self.conn): + selection = self.writer.capture_writer_selection(self.conn) + for _ in range(2): + with self.writer.writer_transaction(self.conn, selection): + self.publish(selection) + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.rows(), [("embed_dim", "256"), ("embed_model", "model2vec")]) + self.owner.assert_not_called() + + def test_unbound_and_clean_calls_refuse_without_writes(self) -> None: + selection = self.writer.capture_writer_selection(self.conn) + with self.assertRaises(self.source.VectorPublicationChanged): + self.publish(selection) + with self.runtime.serving_operation(self.conn): + with self.assertRaises(self.writer.WriterSelectionChanged): + self.publish(selection) + self.assertEqual(self.rows(), []) + self.assertFalse(self.conn.in_transaction) + + def test_changed_model_generation_or_identity_refuses_without_writes(self) -> None: + with self.runtime.serving_operation(self.conn): + selection = self.writer.capture_writer_selection(self.conn) + for name, value in (("_model_generation", 8), ("EMBEDDING_MODEL", "synthetic/changed"), + ("_embedding_dim", 512)): + with self.subTest(field=name): + old = getattr(self.source, name) + setattr(self.source, name, value) + with self.writer.writer_transaction(self.conn, selection): + with self.assertRaises(self.source.VectorPublicationChanged): + self.publish(selection) + self.assertEqual(self.rows(), []) + setattr(self.source, name, old) + + def test_partial_mismatched_and_nontext_metadata_refuse_atomically(self) -> None: + for rows in ( + [("embed_model", "model2vec")], [("embed_dim", "256")], + [("embed_model", "synthetic/wrong"), ("embed_dim", "256")], + [("embed_model", "model2vec"), ("embed_dim", "512")], + [("embed_model", b"model2vec"), ("embed_dim", "256")], + ): + with self.subTest(rows=rows): + self.conn.execute("DELETE FROM main.metadata WHERE key IN ('embed_model','embed_dim')") + self.conn.executemany("INSERT INTO main.metadata(key,value) VALUES(?,?)", rows) + self.conn.commit() + before = self.rows() + with self.runtime.serving_operation(self.conn): + selection = self.writer.capture_writer_selection(self.conn) + with self.writer.writer_transaction(self.conn, selection): + changes = self.conn.total_changes + with self.assertRaises(self.source.VectorPublicationChanged): + self.publish(selection) + self.assertEqual(self.conn.total_changes, changes) + self.assertEqual(self.rows(), before) + + def test_temp_metadata_neither_satisfies_nor_receives_main_publication(self) -> None: + self.conn.execute("CREATE TEMP TABLE metadata(key TEXT PRIMARY KEY,value TEXT,updated_at TEXT)") + self.conn.execute("INSERT INTO temp.metadata VALUES('embed_model','synthetic/temp',NULL)") + self.conn.commit() + with self.runtime.serving_operation(self.conn): + selection = self.writer.capture_writer_selection(self.conn) + with self.writer.writer_transaction(self.conn, selection): + self.publish(selection) + self.assertEqual(self.rows(), [("embed_dim", "256"), ("embed_model", "model2vec")]) + self.assertEqual(self.conn.execute("SELECT value FROM temp.metadata").fetchall(), [("synthetic/temp",)]) + + def test_borrowed_failure_rolls_back_our_metadata_and_retains_outer_work(self) -> None: + self.conn.execute("INSERT INTO messages(id,content) VALUES(701,'synthetic outer work')") + with self.runtime.serving_operation(self.conn): + selection = self.writer.capture_writer_selection(self.conn) + before = self.conn.commits, self.conn.rollbacks + with self.assertRaisesRegex(RuntimeError, "synthetic downstream"): + with self.writer.writer_transaction(self.conn, selection): + self.publish(selection) + raise RuntimeError("synthetic downstream failure") + self.assertTrue(self.conn.in_transaction) + self.assertEqual((self.conn.commits, self.conn.rollbacks), before) + self.assertEqual(self.rows(), []) + self.assertEqual(self.conn.execute("SELECT content FROM messages WHERE id=701").fetchone(), + ("synthetic outer work",)) + + def test_owned_failure_rolls_back_metadata(self) -> None: + with self.runtime.serving_operation(self.conn): + selection = self.writer.capture_writer_selection(self.conn) + with self.assertRaisesRegex(RuntimeError, "synthetic downstream"): + with self.writer.writer_transaction(self.conn, selection): + self.publish(selection) + raise RuntimeError("synthetic downstream failure") + self.assertEqual(self.rows(), []) + self.assertFalse(self.conn.in_transaction) + + def test_selected_publication_validates_but_does_not_rewrite_metadata(self) -> None: + selected = self.fixture.fixture.select() + self.source.EMBEDDING_MODEL = selected.target.model_id + self.source._embedding_dim = selected.target.dimension + self.identity = (7, self.source.EMBEDDING_MODEL, self.source._embedding_dim) + with self.runtime.serving_operation(self.conn): + selection = self.writer.capture_writer_selection(self.conn) + with self.writer.writer_transaction(self.conn, selection): + changes = self.conn.total_changes + self.publish(selection) + self.assertEqual(self.conn.total_changes, changes) + self.owner.assert_not_called() + + def test_general_metadata_mutators_still_require_maintenance_ownership(self) -> None: + for name in ("_write_embedder_metadata_no_commit", "_write_embedder_metadata"): + with self.subTest(name=name), self.runtime.serving_operation(self.conn): + selection = self.writer.capture_writer_selection(self.conn) + with self.writer.writer_transaction(self.conn, selection): + with self.assertRaisesRegex(RuntimeError, "Synthetic maintenance owner busy"): + getattr(self.source, name)(self.conn) + self.assertEqual(self.owner.call_count, 2) + self.assertEqual(self.rows(), []) + + def test_matching_metadata_supports_sqlite_row_factory(self) -> None: + self.conn.execute("INSERT INTO main.metadata(key,value) VALUES('embed_model','model2vec')") + self.conn.execute("INSERT INTO main.metadata(key,value) VALUES('embed_dim','256')") + self.conn.commit() + self.conn.row_factory = sqlite3.Row + try: + with self.runtime.serving_operation(self.conn): + selection = self.writer.capture_writer_selection(self.conn) + with self.writer.writer_transaction(self.conn, selection): + self.publish(selection) + self.assertEqual([tuple(row) for row in self.rows()], + [("embed_dim", "256"), ("embed_model", "model2vec")]) + finally: + self.conn.row_factory = None + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-tier-public-adoption-795.py b/tests/test-tier-public-adoption-795.py new file mode 100644 index 00000000..128ccc93 --- /dev/null +++ b/tests/test-tier-public-adoption-795.py @@ -0,0 +1,1687 @@ +"""Public adoption orchestration with production AST, stubs and in-memory SQL.""" + +from __future__ import annotations + +import argparse +import ast +import builtins +import contextlib +import dataclasses +import io +import json +import logging +import re +import sqlite3 +import sys +import threading +import types +import unittest +from pathlib import Path +from unittest.mock import Mock, patch + +ROOT = Path(__file__).resolve().parents[1] + + +@dataclasses.dataclass(frozen=True) +class Target: + tier: str + model_id: str + dimension: int + tier_group: str + + @classmethod + def capture(cls, tier: str) -> Target: + if tier not in {"edge", "base", "pro"}: + raise ValueError("unsupported synthetic target") + return cls( + tier, + "model2vec" if tier == "edge" else "qwen3_256", + 256, + "edge" if tier == "edge" else "basepro", + ) + + @property + def identity(self) -> tuple: + return self.model_id, self.dimension, self.tier_group + + @property + def tables(self) -> tuple[str, str]: + return f"vec_messages_{self.tier_group}", f"vec_messages_sep_{self.tier_group}" + + +class ActivationIntent(types.SimpleNamespace): + pass + + +class PolicyIntent(types.SimpleNamespace): + pass + + +class LegacyTierPolicy(types.SimpleNamespace): + pass + + +class FakeConnection: + def __init__(self) -> None: + self.closed = False + self.in_transaction = False + self.executed: list[str] = [] + + def close(self) -> None: + self.closed = True + + def execute(self, sql: str, parameters: tuple = ()) -> object: + self.executed.append(sql) + raise AssertionError("Unexpected SQL on stub: " + sql) + + +@contextlib.contextmanager +def transaction(conn: sqlite3.Connection, *, write: bool): + if conn.in_transaction: + raise RuntimeError("borrowed transaction") + conn.execute("BEGIN IMMEDIATE" if write else "BEGIN") + try: + yield + conn.commit() if write else conn.rollback() + except BaseException: + if conn.in_transaction: + conn.rollback() + raise + + +class Harness: + def __init__(self) -> None: + self.events: list[object] = [] + self.config = {"tier": "edge", "independent": "keep"} + self.guard = types.SimpleNamespace(tier="edge", generation="old") + self.state = types.SimpleNamespace( + selection=None, legacy_policy=None, intent=None + ) + self.job = types.SimpleNamespace( + job_id="a" * 32, + target=Target.capture("base"), + database_path=Path("/synthetic/store.db").absolute(), + tracker="canonical-v1", + source_epoch="epoch", + source_schema_signature="b" * 64, + ) + self.intent = ActivationIntent( + job_id=self.job.job_id, + target=self.job.target, + tracker=self.job.tracker, + source_epoch=self.job.source_epoch, + source_schema_signature=self.job.source_schema_signature, + expected_config=self.guard, + status_id=17, + state="staged", + ) + self.marker = None + self.result = LegacyTierPolicy( + target=Target.capture("pro"), generation="new", config_acknowledged=False + ) + self.modules = { + "truememory.embedding_target": types.SimpleNamespace( + EmbeddingTarget=Target + ), + "truememory.tier_config": types.SimpleNamespace( + get_tier_config=lambda tier: {"reranker": "synthetic/reranker"} + ), + "truememory.maintenance": types.SimpleNamespace( + maintenance_owner=self.owner + ), + "truememory.rebuild_source": types.SimpleNamespace( + ensure_rebuild_tracking=lambda conn: self.events.append("tracking") + ), + "truememory.tier_switch.cache": types.SimpleNamespace( + preflight_ram_check=lambda group: (True, ""), + get_transition_action=lambda old, new: ( + "config_only" if {old, new} == {"base", "pro"} else "delta_or_full" + ), + ), + "truememory.tier_switch.throttler": types.SimpleNamespace( + DynamicThrottler=lambda **kwargs: object() + ), + "truememory.tier_switch.worker": types.SimpleNamespace( + RebuildWorker=Mock() + ), + "truememory.tier_switch.serving": types.SimpleNamespace( + exclusive_activation=self.exclusive + ), + "truememory.tier_switch.activation": types.SimpleNamespace( + ActivationIntent=ActivationIntent, + LegacyTierPolicy=LegacyTierPolicy, + read_activation_state=lambda conn: self.state, + _state=lambda conn: self.state, + commit_config_only_transition=self.commit_policy, + stage_activation_intent=self.stage, + acknowledge_config=lambda conn, **kwargs: self.events.append("ack"), + acknowledge_policy_config=lambda conn, **kwargs: self.events.append( + "ack" + ), + commit_certified_selection=lambda *args: self.result, + _transaction=transaction, + _normalized=lambda sql: re.sub( + r"\s*([(),])\s*", r"\1", " ".join(sql.strip().rstrip(";").split()) + ).lower(), + _schema_sql=lambda conn, name: conn.execute( + "SELECT sql FROM sqlite_master WHERE name=?", (name,) + ).fetchone()[0], + _guard_pair=lambda conn, target, **kwargs: self.events.append( + ("pair", kwargs) + ), + ), + "truememory.tier_switch.job": types.SimpleNamespace( + read_selected_job_marker=lambda conn: self.marker, + select_tier_job=self.select, + check_selected_tier_job=lambda *args: self.events.append("check"), + end_selected_tier_job=lambda *args, **kwargs: self.events.append( + ("end", kwargs) + ), + _cancel_policy_job_in_writer=lambda *args, **kwargs: self.events.append( + ("cancel", kwargs) + ), + ), + "truememory.tier_switch.projection": types.SimpleNamespace( + capture_config_guard=lambda **kwargs: self.guard, + mirror_activation=self.mirror, + patch_config_fields=self.patch_config, + CONFIG_WRITE_LOCK=threading.RLock(), + ConfigFileLock=lambda *args, **kwargs: contextlib.nullcontext(), + _read_config=lambda path: self.config.copy(), + _write_config=self.write_config, + ), + "truememory.tier_switch.runtime": types.SimpleNamespace( + apply_frozen_selection=lambda result: self.events.append( + "apply_frozen" + ), + apply_runtime_policy=lambda result: self.events.append("apply_policy"), + serving_operation=Mock( + side_effect=AssertionError("native readiness forbidden") + ), + ), + "truememory.tier_switch.source": types.SimpleNamespace( + read_tier_progress=lambda *args, **kwargs: None + ), + "truememory.vector_search": types.SimpleNamespace( + init_prepared_target_tables=lambda *args: self.events.append("tables") + ), + "truememory.storage": types.SimpleNamespace( + DEFAULT_BUSY_TIMEOUT_MS=1, + _validate_db_path=lambda p: p, + create_db=Mock(side_effect=AssertionError("initialization forbidden")), + ), + } + self.modules["truememory"] = types.SimpleNamespace( + vector_search=self.modules["truememory.vector_search"] + ) + vector = self.modules["truememory.vector_search"] + vector.resolve_tier = lambda: "edge" + vector.EMBEDDING_MODEL = "model2vec" + vector._embedding_dim = 256 + self.modules["truememory"].reranker = types.SimpleNamespace( + get_current_reranker_name=lambda: "synthetic/reranker" + ) + vector._model = None + self.modules["truememory"].reranker._model = None + + def apply_embedding_policy(target, **kwargs): + self.events.append("lazy_embedding") + vector.EMBEDDING_MODEL = target.model_id + vector._embedding_dim = target.dimension + vector.resolve_tier = lambda: target.tier + + vector.apply_embedding_policy = apply_embedding_policy + self.modules["truememory"].reranker.apply_reranker_policy = lambda *args: ( + self.events.append("lazy_reranker") + ) + + def set_embedding_model(tier): + self.events.append("lazy_embedding") + applied = Target.capture(tier) + vector.EMBEDDING_MODEL = applied.model_id + vector._embedding_dim = applied.dimension + vector.resolve_tier = lambda: tier + + vector.set_embedding_model = set_embedding_model + self.modules["truememory"].reranker.set_active_tier = lambda tier: self.events.append("lazy_reranker") + self.manager_module = self.load("truememory/tier_switch/manager.py") + self.manager_module.os = types.SimpleNamespace(environ={}, name="posix") + self.modules["truememory.tier_switch.manager"] = self.manager_module + self.manager = self.manager_module.RebuildManager() + self.conn = FakeConnection() + self.manager_module._open_db = lambda *args, **kwargs: self.conn + self.manager_module._load_tier_extension = lambda conn: self.events.append( + "extension" + ) + self.manager_module._detect_device = lambda: "cpu" + + def importer(self, name, globals=None, locals=None, fromlist=(), level=0): + if name in self.modules: + return self.modules[name] + if name.startswith("truememory") or name in { + "torch", + "numpy", + "sqlite_vec", + "sentence_transformers", + "model2vec", + }: + raise AssertionError("Unstubbed/native import: " + name) + return builtins.__import__(name, globals, locals, fromlist, level) + + def load( + self, relative: str, names: set[str] | None = None, extra: dict | None = None + ): + path = ROOT / relative + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + if names is not None: + tree.body = [ + node + for node in tree.body + if isinstance(node, (ast.FunctionDef, ast.ClassDef)) + and node.name in names + ] + for node in tree.body: + node.decorator_list = [] + module = types.ModuleType("synthetic_public_adoption") + module.__dict__.update( + { + "__builtins__": dict(vars(builtins), __import__=self.importer), + "Path": Path, + "sqlite3": sqlite3, + "sys": sys, + "threading": threading, + } + ) + if extra: + module.__dict__.update(extra) + exec(compile(tree, str(path), "exec"), module.__dict__) + return module + + @contextlib.contextmanager + def owner(self, path): + self.events.append("owner_enter") + try: + yield + finally: + self.events.append("owner_exit") + + @contextlib.contextmanager + def exclusive(self, **kwargs): + self.events.append("exclusive_enter") + try: + yield + finally: + self.events.append("exclusive_exit") + + def write_config(self, path, config): + self.config = config.copy() + self.events.append("config") + + def patch_config(self, fields, **kwargs): + self.config.update(fields) + return self.config.copy() + + def mirror(self, result, **kwargs): + self.events.append("mirror") + + def commit_policy(self, conn, **kwargs): + self.events.append("policy_commit") + return self.result + + def select(self, conn, target, **kwargs): + self.events.append("select") + return self.job + + def stage(self, conn, job, **kwargs): + self.events.append("stage") + return self.intent + + def prepare(self, old="edge", requested="base"): + self.guard.tier = old + self.manager._serving_identity = lambda *args: ( + Target.capture(old), + Target.capture(old).tables, + ) + self.manager._check_inactive_target = lambda *args: self.events.append( + "inactive_check" + ) + self.manager._guard_status = lambda *args: self.events.append("status_guard") + self.manager._create_status_row = lambda *args, **kwargs: ( + self.events.append(("status", kwargs)) or 17 + ) + return self.manager._prepare_job(self.conn, requested, False) + + +class MemoryPath: + def __truediv__(self, name: str) -> MemoryPath: + return self + + @property + def parent(self) -> MemoryPath: + return self + + def mkdir(self, **kwargs) -> None: + pass + + def write_text(self, content: str, **kwargs) -> None: + pass + + +def configure_harness(h: Harness, manager: object): + h.modules["truememory.tier_switch.manager"].RebuildManager = types.SimpleNamespace( + get_instance=lambda: manager + ) + return h.load( + "truememory/mcp_server.py", + {"truememory_configure"}, + { + "json": json, + "os": types.SimpleNamespace(environ={}), + "log": logging.getLogger("synthetic_configure"), + "_memory": object(), + "_memory_lock": threading.Lock(), + "_config_cache": None, + "_load_config": lambda: h.config.copy(), + "_CONFIG_PATH": Path("/synthetic/config.json"), + "_CONFIG_LOCK_PATH": Path("/synthetic/config.json.lock"), + "_DB_PATH": "/synthetic/store.db", + "_VALID_INTENSITIES": ("standard", "enhanced", "max"), + "_config_file_lock": lambda **kwargs: contextlib.nullcontext(), + "_config_write_lock": threading.RLock(), + }, + ) + + +def cancel_cli_harness(h: Harness, manager: object): + factory = Mock(return_value=manager) + h.modules["truememory.tier_switch.manager"].RebuildManager = factory + h.modules["truememory"].__version__ = "synthetic" + module = h.load( + "truememory/ingest/cli.py", + { + "main", + "_positive_rebuild_status_id", + "_rebuild_job_id", + "_run_cancel_rebuild", + }, + {"argparse": argparse, "re": re, "os": types.SimpleNamespace(environ={})}, + ) + return module, factory + + +def status_connection() -> sqlite3.Connection: + tree = ast.parse((ROOT / "truememory/tier_switch/manager.py").read_text(encoding="utf-8")) + method = next( + n + for n in ast.walk(tree) + if isinstance(n, ast.FunctionDef) and n.name == "_guard_status" + ) + sql = next( + n.value + for n in ast.walk(method) + if isinstance(n, ast.Constant) + and isinstance(n.value, str) + and n.value.startswith("CREATE TABLE rebuild_status") + ) + conn = sqlite3.connect(":memory:") + conn.execute(sql) + return conn + + +class PublicAdoptionTests(unittest.TestCase): + def setUp(self): + self.h = Harness() + + def test_force_refuses_before_config_or_sql(self): + h = self.h + with self.assertRaises(h.manager_module.TierSwitchUnsupportedError): + h.manager._prepare_job(h.conn, "base", True) + self.assertEqual(h.events, []) + self.assertEqual(h.conn.executed, []) + + def test_shared_prepare_orders_status_job_intent_before_source(self): + h = self.h + self.assertEqual(h.prepare(), (h.job, h.intent, 17)) + self.assertLess(h.events.index("inactive_check"), h.events.index("tracking")) + self.assertLess(h.events.index("tables"), h.events.index("select")) + self.assertLess(h.events.index("select"), h.events.index("stage")) + self.assertEqual(h.conn.executed, []) + + def test_live_marker_refuses_without_writes(self): + h = self.h + h.marker = types.SimpleNamespace(state="selected", job_id="old") + with self.assertRaises(h.manager_module.TierSwitchUnsupportedError): + h.prepare() + self.assertNotIn("tracking", h.events) + self.assertNotIn("select", h.events) + + def test_same_space_policy_projects_without_source_or_native(self): + h = self.h + h.state.intent = PolicyIntent(expected_config=h.guard, intent_id="policy") + self.assertIsNone(h.prepare("base", "pro")) + self.assertEqual( + [ + x + for x in h.events + if isinstance(x, str) and x not in {"exclusive_enter", "exclusive_exit"} + ], + ["policy_commit", "mirror", "apply_policy", "ack"], + ) + self.assertEqual(h.conn.executed, []) + self.assertEqual(h.manager._last_action, "config_only") + + def test_pending_same_tier_retries_projection(self): + h = self.h + h.state.legacy_policy = h.result + h.state.intent = PolicyIntent(expected_config=h.guard, intent_id="policy") + self.assertIsNone(h.prepare("pro", "pro")) + self.assertIn("apply_policy", h.events) + self.assertEqual(h.manager._last_outcome, "complete") + + def test_pending_other_tier_refuses(self): + h = self.h + h.state.legacy_policy = h.result + h.state.intent = PolicyIntent(expected_config=h.guard, intent_id="policy") + with self.assertRaises(h.manager_module.TierSwitchUnsupportedError): + h.prepare("pro", "edge") + self.assertNotIn("mirror", h.events) + + def test_acknowledged_same_tier_still_projects_runtime(self): + h = self.h + h.result.config_acknowledged = True + h.state.legacy_policy = h.result + self.assertIsNone(h.prepare("pro", "pro")) + self.assertIn("apply_policy", h.events) + + def test_runtime_failure_after_mirror_remains_pending(self): + h = self.h + h.state.legacy_policy = h.result + h.state.intent = PolicyIntent(expected_config=h.guard, intent_id="policy") + h.modules["truememory.tier_switch.runtime"].apply_runtime_policy = Mock( + side_effect=RuntimeError("runtime fault") + ) + with self.assertRaisesRegex(RuntimeError, "runtime fault"): + h.prepare("pro", "pro") + self.assertEqual(h.manager._last_outcome, "activation_pending") + self.assertNotIn("ack", h.events) + + def test_config_failure_never_applies_or_acknowledges(self): + h = self.h + h.state.intent = PolicyIntent(expected_config=h.guard, intent_id="policy") + h.modules["truememory.tier_switch.projection"].mirror_activation = Mock( + side_effect=RuntimeError("config fault") + ) + with self.assertRaisesRegex(RuntimeError, "config fault"): + h.prepare("base", "pro") + self.assertEqual(h.manager._last_outcome, "activation_pending") + self.assertNotIn("apply_policy", h.events) + self.assertNotIn("ack", h.events) + + def test_commit_ambiguity_is_pending_and_issues_no_followup_sql(self): + h = self.h + h.modules[ + "truememory.tier_switch.activation" + ].commit_config_only_transition = Mock( + side_effect=sqlite3.OperationalError("commit") + ) + with self.assertRaises(sqlite3.Error): + h.prepare("base", "pro") + self.assertEqual(h.manager._last_outcome, "activation_pending") + self.assertNotIn("mirror", h.events) + self.assertEqual(h.conn.executed, []) + + def test_thread_failure_discards_connection_without_terminal_retry(self): + h = self.h + h.manager._bootstrap_empty_configuration = lambda *args: False + h.manager._prepare_job = lambda *args, **kwargs: (h.job, h.intent, 17) + thread = Mock() + thread.start.side_effect = RuntimeError("thread start") + thread.is_alive.return_value = False + with patch.object(h.manager_module.threading, "Thread", return_value=thread): + with self.assertRaisesRegex(RuntimeError, "thread start"): + h.manager.start_rebuild( + "base", + backup_path=Path("/synthetic/backup"), + db_path=h.job.database_path, + ) + self.assertTrue(h.conn.closed) + self.assertFalse(h.manager._claimed) + self.assertIsNone(h.manager._active_thread) + self.assertEqual(h.manager._last_outcome, "interrupted") + self.assertEqual(h.conn.executed, []) + + def test_background_handoff_contains_only_immutable_identity(self): + h = self.h + h.manager._bootstrap_empty_configuration = lambda *args: False + h.manager._prepare_job = lambda *args, **kwargs: (h.job, h.intent, 17) + thread = Mock() + thread.is_alive.return_value = False + with patch.object( + h.manager_module.threading, "Thread", return_value=thread + ) as factory: + self.assertEqual( + h.manager.start_rebuild("base", db_path=h.job.database_path), 17 + ) + self.assertTrue(h.conn.closed) + self.assertEqual(factory.call_args.kwargs["args"], (h.job, h.intent, 17)) + self.assertNotIn(h.conn, factory.call_args.kwargs["args"]) + + def test_status_insert_fault_discards_connection(self): + h = self.h + h.manager._bootstrap_empty_configuration = lambda *args: False + h.manager._prepare_job = Mock( + side_effect=sqlite3.OperationalError("status insert") + ) + with self.assertRaises(sqlite3.Error): + h.manager.start_rebuild("base", db_path=h.job.database_path) + self.assertTrue(h.conn.closed) + self.assertFalse(h.manager._claimed) + self.assertEqual(h.conn.executed, []) + + def test_new_store_only_persists_onboarding_config(self): + h = self.h + with patch.object(Path, "stat", side_effect=FileNotFoundError): + self.assertTrue( + h.manager._bootstrap_empty_configuration( + h.job.database_path, "pro", False + ) + ) + self.assertEqual(h.config, {"tier": "pro", "independent": "keep"}) + self.assertNotIn("policy_commit", h.events) + self.assertNotIn("apply_policy", h.events) + self.assertNotIn("tier_activation_generation", h.config) + + def test_tagged_config_cannot_bootstrap_missing_store(self): + h = self.h + h.config["tier_activation_generation"] = "old" + with ( + patch.object(Path, "stat", side_effect=FileNotFoundError), + self.assertRaises(h.manager_module.TierSwitchUnsupportedError), + ): + h.manager._bootstrap_empty_configuration(h.job.database_path, "pro", False) + self.assertEqual(h.config["tier"], "edge") + + def test_nonempty_schema_is_not_bootstrapped(self): + h = self.h + conn = sqlite3.connect(":memory:") + conn.execute("CREATE VIEW present AS SELECT 1") + with ( + patch.object(Path, "stat", return_value=object()), + patch.object(h.manager_module.sqlite3, "connect", return_value=conn), + ): + self.assertFalse( + h.manager._bootstrap_empty_configuration( + h.job.database_path, "pro", False + ) + ) + self.assertEqual(h.config["tier"], "edge") + + def test_uninitialized_empty_schema_bootstraps_without_database_writes(self): + h = self.h + conn = sqlite3.connect(":memory:") + queries = [] + conn.set_trace_callback(queries.append) + with ( + patch.object(Path, "stat", return_value=object()), + patch.object(h.manager_module.sqlite3, "connect", return_value=conn), + ): + self.assertTrue( + h.manager._bootstrap_empty_configuration( + h.job.database_path, "pro", False + ) + ) + self.assertEqual(len(queries), 1) + self.assertTrue(queries[0].startswith("SELECT")) + + def test_existing_unsupported_database_refuses_before_pragmas(self): + h = self.h + conn = sqlite3.connect(":memory:") + conn.execute("CREATE TABLE metadata(wrong BLOB)") + queries = [] + conn.set_trace_callback(queries.append) + h.modules["truememory.tier_switch.activation"].read_activation_state = Mock( + side_effect=RuntimeError("unsupported metadata") + ) + opener = h.load( + "truememory/tier_switch/manager.py", + {"_open_db"}, + {"TierSwitchUnsupportedError": RuntimeError}, + ) + with ( + patch.object(opener.sqlite3, "connect", return_value=conn), + self.assertRaisesRegex(RuntimeError, "unsupported metadata"), + ): + opener._open_db(h.job.database_path, initialize=True) + self.assertEqual(len(queries), 1) + self.assertTrue(queries[0].startswith("SELECT")) + h.modules["truememory.storage"].create_db.assert_not_called() + + def test_existing_opener_does_not_create_missing_database(self): + h = self.h + opener = h.load( + "truememory/tier_switch/manager.py", + {"_open_db"}, + {"TierSwitchUnsupportedError": RuntimeError}, + ) + with patch.object( + opener.sqlite3, "connect", side_effect=sqlite3.OperationalError("missing") + ) as connect: + with self.assertRaises(sqlite3.Error): + opener._open_db(h.job.database_path) + self.assertIn("?mode=rw", connect.call_args.args[0]) + + def test_cli_config_only_never_probes(self): + h = self.h + h.modules["truememory.tier_switch.manager"].RebuildManager = lambda: ( + types.SimpleNamespace( + run_rebuild_sync=lambda tier: True, _last_action="config_only" + ) + ) + cli = h.load("truememory/ingest/cli.py", {"_setup_model_readiness"}) + self.assertEqual(cli._setup_model_readiness("pro", "base"), []) + h.modules[ + "truememory.tier_switch.runtime" + ].serving_operation.assert_not_called() + + def test_cli_same_tier_invokes_authoritative_manager(self): + h = self.h + run = Mock(side_effect=RuntimeError("pending")) + h.modules["truememory.tier_switch.manager"].RebuildManager = lambda: ( + types.SimpleNamespace(run_rebuild_sync=run) + ) + cli = h.load("truememory/ingest/cli.py", {"_setup_model_readiness"}) + self.assertEqual(cli._setup_model_readiness("pro", "pro"), ["tier activation"]) + run.assert_called_once_with("pro") + + def test_both_completion_flags_required(self): + h = self.h + h.state.intent = h.intent + plan = types.SimpleNamespace( + action="resume", manifest=types.SimpleNamespace(consumed=1, total=1) + ) + h.modules["truememory.tier_switch.source"].plan_tier_source = ( + lambda *args, **kwargs: plan + ) + h.modules["truememory.tier_switch.source"].initialize_tier_source = Mock( + side_effect=AssertionError("no init") + ) + worker = Mock() + worker.run_source.return_value = types.SimpleNamespace( + status="complete", + captured_range_complete=False, + current_source_complete=True, + plan=plan, + ) + h.manager_module.RebuildWorker = lambda *args: worker + h.manager._terminal_status = lambda *args: h.events.append("terminal") + h.manager._finalize_rebuild = Mock( + side_effect=AssertionError("cannot finalize") + ) + self.assertFalse(h.manager._run_job(h.conn, h.job, h.intent, 17)) + h.manager._finalize_rebuild.assert_not_called() + self.assertEqual(h.manager._last_outcome, "failed") + + def test_source_conflict_does_not_finalize_or_retry(self): + h = self.h + h.state.intent = h.intent + planner = Mock(side_effect=RuntimeError("source changed")) + h.modules["truememory.tier_switch.source"].plan_tier_source = planner + h.modules["truememory.tier_switch.source"].initialize_tier_source = Mock() + h.manager._finalize_rebuild = Mock() + with self.assertRaisesRegex(RuntimeError, "source changed"): + h.manager._run_job(h.conn, h.job, h.intent, 17) + self.assertEqual(planner.call_count, 1) + h.manager._finalize_rebuild.assert_not_called() + self.assertEqual(h.conn.executed, []) + + def test_local_cancel_rejects_wrong_status(self): + h = self.h + h.manager._active_worker = Mock() + h.manager._active_status_id = 17 + with self.assertRaises(h.manager_module.TierSwitchUnsupportedError): + h.manager.cancel(99) + h.manager._active_worker.cancel.assert_not_called() + self.assertFalse(h.manager._cancel_requested.is_set()) + + def test_interrupted_cancel_requires_exact_job(self): + h = self.h + h.marker = types.SimpleNamespace(**vars(h.job), state="selected") + h.state.intent = h.intent + with self.assertRaises(h.manager_module.TierSwitchUnsupportedError): + h.manager.cancel(17, expected_job_id="wrong") + self.assertTrue(h.conn.closed) + self.assertFalse( + any(isinstance(x, tuple) and x[0] == "cancel" for x in h.events) + ) + + def test_interrupted_cancel_cas_uses_exact_marker(self): + h = self.h + h.marker = types.SimpleNamespace(**vars(h.job), state="selected") + h.state.intent = h.intent + conn = sqlite3.connect(":memory:") + h.manager_module._open_db = lambda *args: conn + result = h.manager.cancel(17, expected_job_id=h.job.job_id) + self.assertEqual(result, {"status": "cancelled", "job_id": h.job.job_id}) + writes = [x for x in h.events if isinstance(x, tuple) and x[0] == "cancel"] + self.assertEqual(len(writes), 1) + self.assertEqual(writes[0][1]["job_id"], h.job.job_id) + + def test_status_backup_path_round_trip_real_sql(self): + h = self.h + conn = sqlite3.connect(":memory:") + self.addCleanup(conn.close) + tree = ast.parse((ROOT / "truememory/tier_switch/manager.py").read_text(encoding="utf-8")) + method = next( + n + for n in ast.walk(tree) + if isinstance(n, ast.FunctionDef) and n.name == "_guard_status" + ) + sql = next( + n.value + for n in ast.walk(method) + if isinstance(n, ast.Constant) + and isinstance(n.value, str) + and n.value.startswith("CREATE TABLE rebuild_status") + ) + conn.execute(sql) + sid = h.manager._create_status_row( + conn, + "basepro", + "base", + "streamed", + 0, + backup_path=Path("/synthetic/backup"), + ) + self.assertEqual( + conn.execute( + "SELECT backup_path FROM rebuild_status WHERE id=?", (sid,) + ).fetchone(), + (str(Path("/synthetic/backup")),), + ) + self.assertFalse(conn.in_transaction) + + def test_status_trigger_refused_before_insert(self): + h = self.h + conn = sqlite3.connect(":memory:") + self.addCleanup(conn.close) + tree = ast.parse((ROOT / "truememory/tier_switch/manager.py").read_text(encoding="utf-8")) + method = next( + n + for n in ast.walk(tree) + if isinstance(n, ast.FunctionDef) and n.name == "_guard_status" + ) + sql = next( + n.value + for n in ast.walk(method) + if isinstance(n, ast.Constant) + and isinstance(n.value, str) + and n.value.startswith("CREATE TABLE rebuild_status") + ) + conn.execute(sql) + conn.execute( + "CREATE TRIGGER side_effect AFTER INSERT ON rebuild_status BEGIN SELECT 1; END" + ) + with self.assertRaises(h.manager_module.TierSwitchUnsupportedError): + h.manager._create_status_row(conn, "basepro", "base", "streamed", 0) + self.assertEqual( + conn.execute("SELECT count(*) FROM rebuild_status").fetchone(), (0,) + ) + + def test_nonempty_inactive_pair_requires_receipt(self): + h = self.h + conn = sqlite3.connect(":memory:") + self.addCleanup(conn.close) + for name in h.job.target.tables: + conn.execute(f'CREATE TABLE "{name}"(embedding BLOB)') + conn.execute("INSERT INTO vec_messages_basepro VALUES (x'01')") + conn.commit() + before = conn.total_changes + with self.assertRaises(h.manager_module.TierSwitchUnsupportedError): + h.manager._check_inactive_target(conn, h.job.target) + self.assertEqual(conn.total_changes, before) + + def test_configure_queued_clears_memory_and_reports_served_tier(self): + h = self.h + manager = types.SimpleNamespace( + start_rebuild=Mock(return_value=42), _last_outcome="preparing" + ) + module = configure_harness(h, manager) + with patch.object(Path, "home", return_value=MemoryPath()): + result = json.loads(module.truememory_configure("base")) + self.assertIsNone(module._memory) + self.assertEqual(result["tier"], "base") + self.assertEqual(result["served_tier"], "edge") + self.assertEqual(result["status"], "building") + self.assertEqual(result["status_id"], 42) + self.assertEqual(h.config["tier"], "edge") + + def test_configure_failure_clears_memory_and_keeps_served_config(self): + h = self.h + manager = types.SimpleNamespace( + start_rebuild=Mock(side_effect=RuntimeError("synthetic fault")), + _last_outcome="interrupted", + ) + module = configure_harness(h, manager) + with ( + patch.object(Path, "home", return_value=MemoryPath()), + self.assertLogs("synthetic_configure", "ERROR"), + ): + result = json.loads(module.truememory_configure("base")) + self.assertIsNone(module._memory) + self.assertEqual(result["served_tier"], "edge") + self.assertEqual(result["status"], "switch_failed") + self.assertIn("synthetic fault", result["rebuild_error"]) + self.assertEqual(h.config["tier"], "edge") + + def test_configure_same_tier_does_not_bypass_pending(self): + h = self.h + h.config["tier"] = "pro" + manager = types.SimpleNamespace( + start_rebuild=Mock(side_effect=RuntimeError("runtime pending")), + _last_outcome="activation_pending", + ) + module = configure_harness(h, manager) + with ( + patch.object(Path, "home", return_value=MemoryPath()), + self.assertLogs("synthetic_configure", "ERROR"), + ): + result = json.loads( + module.truememory_configure("pro", search_intensity="enhanced") + ) + self.assertEqual(result["status"], "activation_pending") + self.assertEqual(manager.start_rebuild.call_count, 1) + self.assertEqual(h.config["search_intensity"], "enhanced") + self.assertIsNone(module._memory) + + def test_superseded_policy_status_is_not_running(self): + h = self.h + conn = status_connection() + sid = h.manager._create_status_row(conn, "basepro", "base", "streamed", 0) + h.manager_module._open_db = lambda *args: conn + h.state.intent = PolicyIntent() + result = h.manager.get_status(sid) + self.assertEqual(result["status"], "superseded") + self.assertFalse(result["activated"]) + + def test_status_reader_bounds_text_before_python_materialization(self): + h = self.h + conn = status_connection() + sid = h.manager._create_status_row(conn, "basepro", "base", "streamed", 0) + conn.execute( + "UPDATE rebuild_status SET error=?,backup_path=?", + ("e" * 100000, "b" * 100000), + ) + conn.commit() + queries = [] + conn.set_trace_callback(queries.append) + h.manager_module._open_db = lambda *args: conn + result = h.manager.get_status(sid) + self.assertIsNone(result["error"]) + self.assertIsNone(result["backup_path"]) + self.assertFalse(any("SELECT *" in query for query in queries)) + self.assertTrue( + any("length(CAST(error AS BLOB))<=4096" in query for query in queries) + ) + + def test_orphan_marker_exposed_without_claiming_row_ownership(self): + h = self.h + conn = status_connection() + sid = h.manager._create_status_row(conn, "basepro", "base", "streamed", 0) + h.manager_module._open_db = lambda *args: conn + h.marker = types.SimpleNamespace(job_id=h.job.job_id, state="selected") + result = h.manager.get_status(sid) + self.assertEqual(result["status"], "interrupted") + self.assertEqual(result["selected_job_id"], h.job.job_id) + self.assertNotIn("job_id", result) + + def test_status_sql_fault_returns_unknown_and_discards_connection(self): + h = self.h + h.manager._guard_status = Mock( + side_effect=sqlite3.OperationalError("status fault") + ) + conn = sqlite3.connect(":memory:") + h.manager_module._open_db = lambda *args: conn + result = h.manager.get_status(17) + self.assertEqual(result, {"status": "unknown", "error": "OperationalError"}) + with self.assertRaises(sqlite3.ProgrammingError): + conn.execute("SELECT 1") + + def test_local_cancel_rejects_wrong_explicit_job(self): + h = self.h + h.manager._claimed = True + h.manager._active_status_id = 17 + h.manager._active_job_id = h.job.job_id + with self.assertRaises(h.manager_module.TierSwitchUnsupportedError): + h.manager.cancel(17, expected_job_id="wrong") + self.assertFalse(h.manager._cancel_requested.is_set()) + + def test_public_force_refuses_before_database_open(self): + h = self.h + h.manager_module._open_db = Mock( + side_effect=AssertionError("must not initialize") + ) + with self.assertRaises(h.manager_module.TierSwitchUnsupportedError): + h.manager.start_rebuild("base", force=True, db_path=h.job.database_path) + h.manager_module._open_db.assert_not_called() + self.assertFalse(h.manager._claimed) + + def test_bootstrap_unloaded_runtime_projects_lazy_identity(self): + h = self.h + with patch.object(Path, "stat", side_effect=FileNotFoundError): + self.assertTrue( + h.manager._bootstrap_empty_configuration( + h.job.database_path, "pro", False + ) + ) + self.assertEqual(h.manager._last_outcome, "complete") + self.assertEqual(h.manager._served_tier, "pro") + self.assertTrue( + h.manager._bootstrap_empty_configuration( + h.job.database_path, "pro", False + ) + ) + self.assertEqual(h.manager._last_outcome, "complete") + self.assertEqual(h.manager._served_tier, "pro") + self.assertEqual(h.config["tier"], "pro") + self.assertEqual( + h.modules["truememory.vector_search"].EMBEDDING_MODEL, "qwen3_256" + ) + self.assertEqual(h.events.count("lazy_embedding"), 1) + self.assertEqual(h.events.count("lazy_reranker"), 1) + self.assertIsNone(h.modules["truememory.vector_search"]._model) + + def test_configure_bootstrap_pending_reports_runtime_served_tier(self): + h = self.h + h.config["tier"] = "pro" + manager = types.SimpleNamespace( + start_rebuild=Mock(return_value=0), + _last_outcome="activation_pending", + _last_action="onboarding", + _served_tier="edge", + ) + module = configure_harness(h, manager) + with patch.object(Path, "home", return_value=MemoryPath()): + result = json.loads(module.truememory_configure("pro")) + self.assertEqual(result["status"], "activation_pending") + self.assertEqual(result["tier"], "pro") + self.assertEqual(result["served_tier"], "edge") + self.assertNotIn("note", result) + + def test_committed_tier_reported_even_when_config_mirror_fails(self): + h = self.h + h.modules["truememory.tier_switch.projection"].mirror_activation = Mock( + side_effect=RuntimeError("mirror") + ) + with self.assertRaisesRegex(RuntimeError, "mirror"): + h.manager._project_result(h.conn, h.result, h.intent, policy=True) + self.assertEqual(h.manager._served_tier, "pro") + self.assertEqual(h.manager._last_outcome, "activation_pending") + + def test_ambiguous_public_request_discards_before_fresh_readback(self): + h = self.h + h.manager._bootstrap_empty_configuration = lambda *args: False + fresh = FakeConnection() + calls = [] + + def opener(*args, **kwargs): + calls.append(kwargs) + if len(calls) == 1: + return h.conn + self.assertTrue(h.conn.closed) + return fresh + + def failing_prepare(*args, **kwargs): + h.manager._last_outcome = "activation_pending" + raise sqlite3.OperationalError("commit ambiguity") + + h.manager_module._open_db = opener + h.manager._prepare_job = failing_prepare + h.state.legacy_policy = h.result + with self.assertRaises(sqlite3.Error): + h.manager.start_rebuild("pro", db_path=h.job.database_path) + self.assertEqual(len(calls), 2) + self.assertTrue(fresh.closed) + self.assertEqual(h.manager._served_tier, "pro") + self.assertEqual(h.conn.executed, []) + + def test_unknown_readback_does_not_claim_old_config_served(self): + h = self.h + h.config["tier"] = "base" + manager = types.SimpleNamespace( + start_rebuild=Mock(return_value=0), + _last_outcome="activation_pending", + _served_tier=None, + ) + module = configure_harness(h, manager) + with patch.object(Path, "home", return_value=MemoryPath()): + result = json.loads(module.truememory_configure("pro")) + self.assertIsNone(result["served_tier"]) + self.assertEqual(result["status"], "activation_pending") + + def test_bootstrap_loaded_mismatch_refuses_before_config(self): + h = self.h + h.modules["truememory.vector_search"]._model = object() + with ( + patch.object(Path, "stat", side_effect=FileNotFoundError), + self.assertRaises(h.manager_module.TierSwitchUnsupportedError), + ): + h.manager._bootstrap_empty_configuration(h.job.database_path, "pro", False) + self.assertEqual(h.manager._served_tier, "edge") + self.assertEqual(h.config["tier"], "edge") + self.assertNotIn("config", h.events) + self.assertNotIn("lazy_embedding", h.events) + + def test_bootstrap_loaded_reranker_refuses_before_config(self): + h = self.h + h.modules["truememory"].reranker._model = object() + with ( + patch.object(Path, "stat", side_effect=FileNotFoundError), + self.assertRaises(h.manager_module.TierSwitchUnsupportedError), + ): + h.manager._bootstrap_empty_configuration(h.job.database_path, "pro", False) + self.assertEqual(h.config["tier"], "edge") + self.assertNotIn("lazy_embedding", h.events) + + def test_bootstrap_lazy_projection_error_never_writes_config(self): + h = self.h + h.modules["truememory.vector_search"].set_embedding_model = Mock( + side_effect=RuntimeError("projection fault") + ) + with ( + patch.object(Path, "stat", side_effect=FileNotFoundError), + self.assertRaisesRegex(RuntimeError, "projection fault"), + ): + h.manager._bootstrap_empty_configuration(h.job.database_path, "pro", False) + self.assertEqual(h.manager._last_outcome, "activation_pending") + self.assertEqual(h.config["tier"], "edge") + self.assertNotIn("config", h.events) + + def test_bootstrap_config_failure_restores_proven_original_unloaded_identity(self): + h = self.h + h.modules["truememory.tier_switch.projection"]._write_config = Mock( + side_effect=OSError("before replace") + ) + with ( + patch.object(Path, "stat", side_effect=FileNotFoundError), + self.assertRaisesRegex(OSError, "before replace"), + ): + h.manager._bootstrap_empty_configuration(h.job.database_path, "pro", False) + self.assertEqual(h.config["tier"], "edge") + self.assertEqual( + h.modules["truememory.vector_search"].EMBEDDING_MODEL, "model2vec" + ) + self.assertEqual(h.modules["truememory.vector_search"].resolve_tier(), "edge") + self.assertEqual(h.manager._served_tier, "edge") + self.assertIsNone(h.modules["truememory.vector_search"]._model) + + def test_bootstrap_post_replace_fault_preserves_matching_new_identity(self): + h = self.h + + def replaced_then_failed(path, config): + h.config = config.copy() + raise OSError("after replace") + + h.modules[ + "truememory.tier_switch.projection" + ]._write_config = replaced_then_failed + with ( + patch.object(Path, "stat", side_effect=FileNotFoundError), + self.assertRaisesRegex(OSError, "after replace"), + ): + h.manager._bootstrap_empty_configuration(h.job.database_path, "pro", False) + self.assertEqual(h.config["tier"], "pro") + self.assertEqual( + h.modules["truememory.vector_search"].EMBEDDING_MODEL, "qwen3_256" + ) + self.assertEqual(h.manager._served_tier, "pro") + self.assertEqual(h.manager._last_outcome, "activation_pending") + + def test_unreadable_store_cannot_be_treated_as_absent(self): + h = self.h + with ( + patch.object(Path, "stat", side_effect=PermissionError), + self.assertRaises(PermissionError), + ): + h.manager._bootstrap_empty_configuration(h.job.database_path, "pro", False) + self.assertEqual(h.config["tier"], "edge") + self.assertNotIn("lazy_embedding", h.events) + + def test_cancel_cli_dispatch_uses_only_exact_supplied_identity(self): + h = self.h + supplied_db = "/synthetic/store.db" + manager = types.SimpleNamespace( + cancel=Mock(return_value={"status": "cancelled", "job_id": h.job.job_id}) + ) + module, factory = cancel_cli_harness(h, manager) + output = io.StringIO() + with ( + patch.object( + sys, + "argv", + [ + "truememory-ingest", + "cancel-rebuild", + "--status-id", + "17", + "--job-id", + h.job.job_id, + "--db", + supplied_db, + ], + ), + contextlib.redirect_stdout(output), + ): + module.main() + factory.assert_called_once_with() + manager.cancel.assert_called_once_with( + 17, expected_job_id=h.job.job_id, db_path=Path(supplied_db) + ) + self.assertIn("Rebuild 17 cancelled", output.getvalue()) + self.assertIn("explicitly", output.getvalue()) + self.assertEqual(h.events, []) + + def test_cancel_cli_parser_requires_both_exact_ids_before_manager(self): + h = self.h + manager = types.SimpleNamespace(cancel=Mock()) + module, factory = cancel_cli_harness(h, manager) + invalid = [[], ["--status-id", "17"], ["--job-id", h.job.job_id]] + invalid += [ + ["--status-id", value, "--job-id", h.job.job_id] + for value in ( + "0", + "-1", + "+17", + " 17", + "017", + "1_7", + "1.0", + "9223372036854775808", + "9" * 200, + ) + ] + invalid += [ + ["--status-id", "17", "--job-id", value] + for value in ("a" * 31, "a" * 33, "A" * 32, "g" * 32, " " + h.job.job_id) + ] + for arguments in invalid: + with ( + self.subTest(arguments=arguments), + patch.object( + sys, "argv", ["truememory-ingest", "cancel-rebuild", *arguments] + ), + contextlib.redirect_stderr(io.StringIO()), + ): + with self.assertRaises(SystemExit) as error: + module.main() + self.assertEqual(error.exception.code, 2) + factory.assert_not_called() + manager.cancel.assert_not_called() + + def test_cancel_cli_direct_handler_rejects_bool_and_invalid_job(self): + h = self.h + manager = types.SimpleNamespace(cancel=Mock()) + module, factory = cancel_cli_harness(h, manager) + for status_id, job_id in ( + (True, h.job.job_id), + (0, h.job.job_id), + (17, "A" * 32), + ): + with ( + self.subTest(status_id=status_id, job_id=job_id), + contextlib.redirect_stderr(io.StringIO()), + ): + with self.assertRaises(SystemExit) as error: + module._run_cancel_rebuild( + argparse.Namespace(status_id=status_id, job_id=job_id, db=None) + ) + self.assertEqual(error.exception.code, 2) + factory.assert_not_called() + + def test_cancel_cli_accepts_maximum_positive_sqlite_status_without_substitution( + self, + ): + h = self.h + manager = types.SimpleNamespace( + cancel=Mock(return_value={"status": "cancelled", "job_id": h.job.job_id}) + ) + module, _ = cancel_cli_harness(h, manager) + with ( + patch.object( + sys, + "argv", + [ + "truememory-ingest", + "cancel-rebuild", + "--status-id", + "9223372036854775807", + "--job-id", + h.job.job_id, + ], + ), + contextlib.redirect_stdout(io.StringIO()), + ): + module.main() + manager.cancel.assert_called_once_with( + 9223372036854775807, expected_job_id=h.job.job_id, db_path=None + ) + + def test_cancel_cli_busy_failure_is_nonzero_and_never_retries(self): + h = self.h + manager = types.SimpleNamespace( + cancel=Mock(side_effect=RuntimeError("maintenance owner busy")) + ) + module, _ = cancel_cli_harness(h, manager) + output = io.StringIO() + with contextlib.redirect_stderr(output), self.assertRaises(SystemExit) as error: + module._run_cancel_rebuild( + argparse.Namespace(status_id=17, job_id=h.job.job_id, db=None) + ) + self.assertEqual(error.exception.code, 1) + manager.cancel.assert_called_once_with( + 17, expected_job_id=h.job.job_id, db_path=None + ) + self.assertIn("maintenance owner busy", output.getvalue()) + self.assertIn("truememory_status(status_id)", output.getvalue()) + + def test_cancel_cli_nonterminal_or_mismatched_result_never_claims_cancelled(self): + h = self.h + for result in ( + {"status": "no_selected_job"}, + {"status": "cancellation_requested", "status_id": 17}, + {"status": "cancelled", "job_id": "b" * 32}, + ): + manager = types.SimpleNamespace(cancel=Mock(return_value=result)) + module, _ = cancel_cli_harness(h, manager) + output = io.StringIO() + with ( + self.subTest(result=result), + contextlib.redirect_stdout(output), + contextlib.redirect_stderr(io.StringIO()), + ): + with self.assertRaises(SystemExit) as error: + module._run_cancel_rebuild( + argparse.Namespace(status_id=17, job_id=h.job.job_id, db=None) + ) + self.assertEqual(error.exception.code, 1) + self.assertEqual(output.getvalue(), "") + self.assertEqual(manager.cancel.call_count, 1) + + def test_cancel_cli_help_names_existing_status_and_explicit_retry(self): + h = self.h + manager = types.SimpleNamespace(cancel=Mock()) + module, factory = cancel_cli_harness(h, manager) + output = io.StringIO() + with ( + patch.object( + sys, "argv", ["truememory-ingest", "cancel-rebuild", "--help"] + ), + contextlib.redirect_stdout(output), + ): + with self.assertRaises(SystemExit) as error: + module.main() + self.assertEqual(error.exception.code, 0) + self.assertIn("truememory_status(status_id)", output.getvalue()) + self.assertIn("--status-id", output.getvalue()) + self.assertIn("--job-id", output.getvalue()) + self.assertIn("Retry upgrade-tier explicitly", " ".join(output.getvalue().split())) + factory.assert_not_called() + + def test_public_status_recovery_command_is_bound_to_matching_marker(self): + h = self.h + conn = status_connection() + sid = h.manager._create_status_row(conn, "basepro", "base", "streamed", 0) + h.intent.status_id = sid + h.state.intent = h.intent + h.marker = types.SimpleNamespace(**vars(h.job), state="selected") + h.manager_module._open_db = lambda *args: conn + result = h.manager.get_status(sid) + self.assertEqual(result["job_id"], h.job.job_id) + self.assertIn( + f"cancel-rebuild --status-id {sid} --job-id {h.job.job_id}", + result["recovery"], + ) + self.assertIn("same database path", result["recovery"]) + + def test_orphan_status_does_not_offer_bound_cli_cancellation(self): + h = self.h + conn = status_connection() + sid = h.manager._create_status_row(conn, "basepro", "base", "streamed", 0) + h.manager_module._open_db = lambda *args: conn + h.marker = types.SimpleNamespace(job_id=h.job.job_id, state="selected") + result = h.manager.get_status(sid) + self.assertNotIn("job_id", result) + self.assertNotIn("--status-id", result["recovery"]) + self.assertIn("does not prove", result["recovery"]) + + def test_exact_job_with_wrong_positive_status_cannot_cancel_marker(self): + h = self.h + h.marker = types.SimpleNamespace(**vars(h.job), state="selected") + h.state.intent = h.intent + with self.assertRaises(h.manager_module.TierSwitchUnsupportedError): + h.manager.cancel(18, expected_job_id=h.job.job_id) + self.assertTrue(h.conn.closed) + self.assertFalse( + any(isinstance(item, tuple) and item[0] == "cancel" for item in h.events) + ) + + +class ActualLegacyConfigurationComposition(unittest.TestCase): + """Compose actual lazy setters, manager and runtime without native imports.""" + + def setUp(self): + import os + import runpy + import uuid + self.os = os + self.ns = runpy.run_path(str(ROOT / "tests/test-tier-public-boundaries-795.py")) + self.boundary = self.ns["TestPublicBoundaries"]() + self.addCleanup(self.boundary.doCleanups) + self.boundary.setUp() + self.conn, self.api = self.boundary.conn, self.boundary.api + self.selected_marker_row = self.conn.execute("SELECT * FROM truememory_tier_selected_job_v1").fetchone() + self.conn.execute("DELETE FROM truememory_tier_selected_job_v1") + self.conn.executemany("INSERT OR REPLACE INTO metadata(key,value) VALUES (?,?)", + (("embed_model", "model2vec"), ("embed_dim", "256"))) + self.conn.commit() + self.cfg = self.boundary.load_application("tier_config") + self.ns["enter_context"](self, patch.dict(os.environ, {"TRUEMEMORY_EMBED_MODEL": "edge"})) + definitions, namespace = self.ns["definitions"], self.boundary.namespace + self.vector = definitions("vector_search.py", { + "_resolve_model_name", "set_embedding_model", "get_embedding_dim", "resolve_tier", + "_active_tier_group", "_active_vec_table", "_active_sep_table", + }, namespace(os=os, sqlite3=sqlite3, logger=logging.getLogger("synthetic"), + _model=None, _lock=threading.Lock(), _model_generation=0, + _frozen_embedding_target=None, _runtime_policy_tier=None, + EMBEDDING_MODEL="model2vec", _embedding_dim=256, _REMOVED_MODELS={"qwen3"}, + _TIER_ALIASES={tier: config["embed_model"] for tier, config in self.cfg.TIERS.items()}, + _MODEL_DIMS=self.cfg.MODEL_DIMS, _cfg_get_embed_model=self.cfg.get_embed_model, + _cfg_get_embed_dim_for_model=self.cfg.get_embed_dim_for_model, + _cfg_get_model_group=self.cfg.get_model_group)) + self.reranker = definitions("reranker.py", { + "set_active_tier", "get_current_reranker_name", "get_reranker_name_for_tier", + }, namespace(_model=None, _active_tier="edge", _frozen_reranker_id=None, + _TIER_RERANKERS={tier: config["reranker"] for tier, config in self.cfg.TIERS.items()}, + _cfg_get_reranker=self.cfg.get_reranker, log=logging.getLogger("synthetic"))) + self.boundary.fixture.vector, self.boundary.fixture.reranker = self.vector, self.reranker + self.boundary.modules["vector_search"] = self.vector + self.boundary.modules["reranker"] = self.reranker + self.h = Harness() + for name, module in self.boundary.modules.items(): + self.h.modules["truememory." + name] = module + self.h.modules["truememory"] = types.SimpleNamespace(vector_search=self.vector, reranker=self.reranker) + self.h.modules["truememory.tier_config"] = self.cfg + self.h.manager_module.os = os + self.projection = self.h.modules["truememory.tier_switch.projection"] + guard = definitions("tier_switch/projection.py", {"_guard", "ConfigProjectionConflict"}, + namespace(_GENERATION_KEY="tier_activation_generation")) + self.projection._guard = guard._guard + self.h.manager._config_path = Path("/synthetic/config.json") + self.h.manager._config_lock_path = Path("/synthetic/config.lock") + self.owner = definitions("maintenance.py", { + "MaintenanceBusyError", "MaintenanceOwnership", "_acquire_owner", "_release_owner", + "_bind_owner", "maintenance_owner", + }, namespace(NamedTuple=__import__("typing").NamedTuple, + contextmanager=contextlib.contextmanager, Path=Path, + os=types.SimpleNamespace(getpid=os.getpid, close=lambda fd: None), uuid=uuid, + canonical_database_path=lambda value: self.h.job.database_path, + _registry_lock=threading.Lock(), _held_paths={}, _held_fds=set(), + _owner_waiters={}, _coordinators={}, _thread_owners=threading.local(), + try_file_lock=lambda path: 987)) + self.h.modules["truememory.maintenance"] = self.owner + self.h.manager_module._open_db = Mock(side_effect=AssertionError("Unexpected writable/native open")) + self.sql = [] + self.conn.set_trace_callback(self.sql.append) + + def connect_read_snapshot(self): + connection = self.conn + class OwnedHandle: + closed = False + def __getattr__(self, name): + return getattr(connection, name) + def close(self): + self.closed = True + handle = OwnedHandle() + opener = Mock(return_value=handle) + self.h.manager_module.sqlite3 = types.SimpleNamespace(connect=opener) + self.ns["enter_context"](self, patch.object(Path, "stat", return_value=types.SimpleNamespace())) + return handle, opener + + def assert_no_persistent_mutation(self): + self.assertFalse(any(sql.lstrip().split(" ", 1)[0].upper() in { + "INSERT", "UPDATE", "DELETE", "CREATE", "DROP", "ALTER", "REPLACE", + } for sql in self.sql)) + self.assertNotIn("config", self.h.events) + self.assertFalse(self.conn.in_transaction) + self.h.manager_module._open_db.assert_not_called() + self.assertEqual(self.boundary.fixture.loads, []) + + def test_actual_bootstrap_then_fresh_legacy_admission_stays_lazy(self): + for tier in ("base", "pro", "edge"): + with self.subTest(tier=tier), patch.object(Path, "stat", side_effect=FileNotFoundError): + self.assertTrue(self.h.manager._bootstrap_empty_configuration(self.h.job.database_path, tier, False)) + with self.api.serving_operation(self.conn) as operation: + self.assertEqual(operation.tier, tier) + self.assertIsNone(operation.selection) + self.assertIsNone(operation.policy) + self.assertEqual(operation.reranker_id, self.cfg.get_tier_config(tier)["reranker"]) + self.assertEqual(self.vector.resolve_tier(), tier) + self.assertIsNone(self.vector._runtime_policy_tier) + self.assertIsNone(self.vector._frozen_embedding_target) + self.assertIsNone(self.reranker._frozen_reranker_id) + self.assertIsNone(self.vector._model) + self.assertIsNone(self.reranker._model) + self.assertEqual(self.boundary.fixture.loads, []) + self.assertEqual(self.h.config["tier"], tier) + + def test_actual_bootstrap_refuses_controlled_markers_before_ready_or_config(self): + target = self.boundary.modules["embedding_target"].EmbeddingTarget.capture("base") + with self.boundary.fixture.gate.exclusive_activation(): + self.h.manager._apply_bootstrap_identity(target, self.cfg.get_tier_config("base")["reranker"]) + for marker, value in (("_runtime_policy_tier", "base"), ("_frozen_embedding_target", target), + ("_frozen_reranker_id", self.cfg.get_tier_config("base")["reranker"])): + slot = self.reranker if marker == "_frozen_reranker_id" else self.vector + for loaded in (False, True): + with self.subTest(marker=marker, loaded=loaded): + self.vector._model = object() if loaded else None + self.reranker._model = object() if loaded else None + before = self.vector._model, self.reranker._model + setattr(slot, marker, value) + with patch.object(Path, "stat", side_effect=FileNotFoundError): + with self.assertRaisesRegex(self.h.manager_module.TierSwitchUnsupportedError, "controlled runtime"): + self.h.manager._bootstrap_empty_configuration(self.h.job.database_path, "base", False) + self.assertEqual((self.vector._model, self.reranker._model), before) + self.assertEqual(getattr(slot, marker), value) + self.assertEqual(self.h.config["tier"], "edge") + self.assertNotIn("config", self.h.events) + setattr(slot, marker, None) + self.assertEqual(self.boundary.fixture.loads, []) + + def test_actual_bootstrap_preserves_loaded_ordinary_same_tier_certified_reranker(self): + self.vector._model, self.reranker._model = object(), object() + self.reranker._model_certified = True + before = self.vector._model, self.reranker._model + with patch.object(Path, "stat", side_effect=FileNotFoundError): + self.assertTrue(self.h.manager._bootstrap_empty_configuration(self.h.job.database_path, "edge", False)) + self.assertEqual((self.vector._model, self.reranker._model), before) + self.assertTrue(self.reranker._model_certified) + self.assertIsNone(self.reranker._frozen_reranker_id) + with self.api.serving_operation(self.conn) as operation: + self.assertEqual(operation.tier, "edge") + self.assertEqual(self.boundary.fixture.loads, []) + + def test_actual_bootstrap_config_fault_restores_old_legacy_admission(self): + self.projection._write_config = Mock(side_effect=OSError("before replace")) + with patch.object(Path, "stat", side_effect=FileNotFoundError), self.assertRaisesRegex(OSError, "before replace"): + self.h.manager._bootstrap_empty_configuration(self.h.job.database_path, "pro", False) + with self.api.serving_operation(self.conn) as operation: + self.assertEqual(operation.tier, "edge") + self.assertEqual(self.h.config["tier"], "edge") + self.assertEqual(self.vector.EMBEDDING_MODEL, "model2vec") + self.assertIsNone(self.vector._runtime_policy_tier) + self.assertIsNone(self.reranker._frozen_reranker_id) + self.assertEqual(self.boundary.fixture.loads, []) + + def test_actual_bootstrap_post_replace_fault_keeps_new_legacy_admission(self): + def replace_then_fail(path, config): + self.h.config = config.copy() + raise OSError("after replace") + self.projection._write_config = replace_then_fail + with patch.object(Path, "stat", side_effect=FileNotFoundError), self.assertRaisesRegex(OSError, "after replace"): + self.h.manager._bootstrap_empty_configuration(self.h.job.database_path, "pro", False) + with self.api.serving_operation(self.conn) as operation: + self.assertEqual(operation.tier, "pro") + self.assertEqual(self.h.config["tier"], "pro") + self.assertEqual(self.h.manager._last_outcome, "activation_pending") + self.assertIsNone(self.vector._runtime_policy_tier) + self.assertEqual(self.boundary.fixture.loads, []) + + def test_actual_noop_while_other_maintenance_owner_and_reader_are_held(self): + held, release = threading.Event(), threading.Event() + errors = [] + def maintenance(): + try: + with self.owner.maintenance_owner(self.h.job.database_path), self.boundary.fixture.gate.operation_lease(self.api._legacy_key()): + held.set() + if not release.wait(3): + raise AssertionError("Synthetic owner release timed out") + except BaseException as error: + errors.append(error) + thread = threading.Thread(target=maintenance) + thread.start() + try: + self.assertTrue(held.wait(1)) + with self.assertRaises(self.owner.MaintenanceBusyError): + with self.owner.maintenance_owner(self.h.job.database_path): + self.fail("Negative control unexpectedly acquired busy owner") + self.vector._model, self.reranker._model = object(), object() + before_models = self.vector._model, self.reranker._model + writer = self.boundary.load_application("tier_switch.writer") + captured = writer.capture_writer_selection(self.conn) + handle, opener = self.connect_read_snapshot() + self.assertEqual(self.h.manager.start_rebuild("edge", db_path=self.h.job.database_path), 0) + self.assertEqual(self.h.manager._last_action, "noop") + self.assertEqual(self.h.manager._last_outcome, "complete") + self.assertTrue(handle.closed) + self.assertEqual(opener.call_count, 1) + self.assertTrue(opener.call_args.args[0].endswith("?mode=ro")) + self.assertEqual((self.vector._model, self.reranker._model), before_models) + self.assertEqual(writer.capture_writer_selection(self.conn), captured) + self.conn.execute("BEGIN IMMEDIATE") + try: + writer.require_writer_selection(self.conn, captured) + finally: + self.conn.rollback() + self.assert_no_persistent_mutation() + finally: + release.set() + thread.join(3) + self.assertFalse(thread.is_alive()) + self.assertEqual(errors, []) + + def test_actual_noop_sync_path_avoids_owner(self): + self.connect_read_snapshot() + self.owner.maintenance_owner = Mock(side_effect=AssertionError("Unexpected owner")) + self.assertTrue(self.h.manager._run_rebuild_sync_inner("edge", db_path=self.h.job.database_path)) + self.owner.maintenance_owner.assert_not_called() + self.assert_no_persistent_mutation() + + def test_actual_noop_generation_tagged_config_cannot_bypass_recovery(self): + self.h.config["tier_activation_generation"] = "a" * 32 + handle, _ = self.connect_read_snapshot() + self.assertFalse(self.h.manager._try_legacy_noop(self.h.job.database_path, "edge")) + self.assertTrue(handle.closed) + self.assert_no_persistent_mutation() + + def test_actual_noop_staged_intent_and_orphan_job_cannot_bypass_recovery(self): + self.conn.execute("INSERT INTO truememory_tier_selected_job_v1 VALUES (" + ",".join("?" for _ in self.selected_marker_row) + ")", + self.selected_marker_row) + self.conn.commit() + self.boundary.fixture.fixture.build() + self.connect_read_snapshot() + self.sql.clear() + self.assertFalse(self.h.manager._try_legacy_noop(self.h.job.database_path, "edge")) + self.assert_no_persistent_mutation() + self.conn.execute("DELETE FROM metadata WHERE key='tier_activation_v1'") + self.conn.commit() + self.sql.clear() + self.assertFalse(self.h.manager._try_legacy_noop(self.h.job.database_path, "edge")) + self.assert_no_persistent_mutation() + + def test_actual_noop_policy_and_selected_authority_cannot_bypass_recovery(self): + self.boundary.seed_policy() + self.connect_read_snapshot() + self.sql.clear() + self.assertFalse(self.h.manager._try_legacy_noop(self.h.job.database_path, "pro")) + self.assert_no_persistent_mutation() + + def test_actual_noop_rejects_incoherent_embedding_without_writes(self): + self.vector.EMBEDDING_MODEL = "qwen3_256" + self.connect_read_snapshot() + with self.assertRaisesRegex(self.h.manager_module.TierSwitchUnsupportedError, "Legacy model configuration"): + self.h.manager._try_legacy_noop(self.h.job.database_path, "edge") + self.assert_no_persistent_mutation() + + def test_actual_noop_rejects_incoherent_reranker_without_writes(self): + self.reranker._active_tier = "base" + self.connect_read_snapshot() + with self.assertRaisesRegex(self.h.manager_module.TierSwitchUnsupportedError, "Legacy runtime identity"): + self.h.manager._try_legacy_noop(self.h.job.database_path, "edge") + self.assert_no_persistent_mutation() + + def test_concurrent_actual_configure_reports_preparing_until_status_allocated(self): + h = Harness() + entered, release = threading.Event(), threading.Event() + failures = [] + def bootstrap(*args): + entered.set() + if not release.wait(3): + raise AssertionError("Preparation release timed out") + h.manager._last_outcome = "complete" + return True + h.manager._bootstrap_empty_configuration = bootstrap + def first_call(): + try: + h.manager.start_rebuild("pro", db_path=h.job.database_path) + except BaseException as error: + failures.append(error) + thread = threading.Thread(target=first_call) + thread.start() + try: + self.assertTrue(entered.wait(1)) + module = configure_harness(h, h.manager) + with patch.object(Path, "home", return_value=MemoryPath()): + result = json.loads(module.truememory_configure("pro")) + self.assertTrue(h.manager._claimed) + self.assertEqual(h.manager._active_status_id, 0) + self.assertEqual(h.manager._last_outcome, "preparing") + self.assertEqual(result["status"], "preparing") + self.assertIsNone(result["served_tier"]) + self.assertNotIn("status_id", result) + self.assertNotIn("rebuild_error", result) + self.assertIn("preparation", result["note"]) + self.assertNotIn("TrueMemory is ready", result["next_steps"]) + self.assertIsNone(module._memory) + finally: + release.set() + thread.join(3) + self.assertFalse(thread.is_alive()) + self.assertEqual(failures, []) + self.assertEqual(h.events.count("owner_enter"), 1) + + def test_actual_noop_sql_fault_closes_only_owned_connection(self): + handle, _ = self.connect_read_snapshot() + original = self.conn.execute + def execute(sql, *args): + if "temp.sqlite_master" in sql: + raise sqlite3.OperationalError("synthetic unreadable authority") + return original(sql, *args) + with patch.object(self.conn, "execute", side_effect=execute): + with self.assertRaisesRegex(sqlite3.OperationalError, "synthetic unreadable authority"): + self.h.manager._try_legacy_noop(self.h.job.database_path, "edge") + self.assertTrue(handle.closed) + self.assertIsNone(self.h.manager._last_action) + self.assert_no_persistent_mutation() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-tier-public-boundaries-795.py b/tests/test-tier-public-boundaries-795.py new file mode 100644 index 00000000..b2b12d91 --- /dev/null +++ b/tests/test-tier-public-boundaries-795.py @@ -0,0 +1,2452 @@ +"""Public tier boundaries with production AST, stdlib stubs and memory SQLite.""" +from __future__ import annotations + +import ast +import builtins +import contextlib +import dataclasses +import datetime +import enum +import gc +import logging +import os +from pathlib import Path +import runpy +import sqlite3 +import sys +import threading +import time +import types +import unittest +import uuid +import weakref +from unittest.mock import Mock, patch + +ROOT = Path(__file__).resolve().parents[1] +BRIDGE = runpy.run_path(str(Path(__file__).with_name("test-tier-runtime-bridge-795.py"))) +_REAL_PATH_RESOLVE = Path.resolve + + +def enter_context(test_case: unittest.TestCase, context: contextlib.AbstractContextManager[object]) -> object: + result = type(context).__enter__(context) + test_case.addCleanup(type(context).__exit__, context, None, None, None) + return result + + +def definitions(path: str, names: set[str], namespace: dict, *, methods: bool = False) -> types.ModuleType: + if path == "maintenance.py" and "_acquire_owner" in names: + names = names | {"_AutomaticAdmission", "_automatic_admission_locked"} + namespace = {"threading": threading, "weakref": weakref, **namespace} + tree = ast.parse((ROOT / "truememory" / path).read_text(encoding="utf-8")) + if methods: + cls = next(node for node in tree.body if isinstance(node, ast.ClassDef)) + nodes = [node for node in cls.body if isinstance(node, ast.FunctionDef) and node.name in names] + else: + nodes = [node for node in tree.body + if isinstance(node, (ast.FunctionDef, ast.ClassDef)) and node.name in names + or isinstance(node, ast.Assign) and len(node.targets) == 1 + and isinstance(node.targets[0], ast.Name) and node.targets[0].id in names] + future = ast.ImportFrom(module="__future__", names=[ast.alias(name="annotations")], level=0) + module = types.ModuleType("synthetic_public_" + path.replace("/", "_").replace(".", "_")) + module.__dict__.update(namespace) + with patch.dict(sys.modules, {module.__name__: module}): + exec(compile(ast.fix_missing_locations(ast.Module(body=[future, *nodes], type_ignores=[])), path, "exec"), module.__dict__) + return module + + +class TestFixtureCompatibility(unittest.TestCase): + def test_context_cleanup_works_without_testcase_enter_context(self): + case = unittest.TestCase() + entered, exited = [], [] + @contextlib.contextmanager + def context(): + entered.append(True) + try: + yield "synthetic context" + finally: + exited.append(True) + result = enter_context(case, context()) + self.assertEqual(result, "synthetic context") + self.assertEqual(entered, [True]) + self.assertEqual(exited, []) + case.doCleanups() + self.assertEqual(exited, [True]) + + def test_nested_setup_failure_restores_global_path_stat(self): + warm = runpy.run_path(str(Path(__file__).with_name("test-tier-warm-admission-795.py"))) + active = warm["ACTIVE"]["TestActiveInitialization"] + original = Path.stat + def fail_setup(case): + enter_context(case, patch.object(Path, "stat", return_value=types.SimpleNamespace())) + raise RuntimeError("synthetic partial setup") + case = warm["TestWarmAdmission"]() + with patch.object(active, "setUp", fail_setup): + try: + with self.assertRaisesRegex(RuntimeError, "synthetic partial setup"): + case.setUp() + finally: + case.doCleanups() + self.assertIs(Path.stat, original) + + +class TestPublicBoundaries(unittest.TestCase): + def setUp(self) -> None: + self.fixture = BRIDGE["TestRuntimeBridge"]() + self.addCleanup(self.fixture.doCleanups) + self.fixture.setUp() + self.api, self.conn = self.fixture.api, self.fixture.conn + self.modules = self.fixture.modules + maintenance = definitions("maintenance.py", {"MaintenanceBusyError"}, {}) + self.modules["maintenance"] = types.SimpleNamespace( + MaintenanceBusyError=maintenance.MaintenanceBusyError, + wait_for_automatic_owner=Mock(side_effect=AssertionError("Unexpected automatic owner wait in healthy fixture")), + maintenance_owner=lambda path: contextlib.nullcontext(), connection_database_path=lambda conn: None) + storage = ast.parse((ROOT / "truememory/storage.py").read_text(encoding="utf-8")) + self.modules["storage"].DEFAULT_BUSY_TIMEOUT_MS = next( + ast.literal_eval(node.value) for node in storage.body if isinstance(node, ast.Assign) + and any(isinstance(target, ast.Name) and target.id == "DEFAULT_BUSY_TIMEOUT_MS" for target in node.targets)) + enter_context(self, patch.dict(sys.modules, {"truememory.tier_switch.runtime": self.api})) + + def imports(self, name, globals=None, locals=None, fromlist=(), level=0): + if name == "truememory.tier_switch.runtime": + return self.api + return self.fixture.safe_import(name, globals, locals, fromlist, level) + + def namespace(self, **values): + return dict(__builtins__=dict(vars(builtins), __import__=self.imports), **values) + + def test_direct_selected_legacy_vector_mutators_refuse_before_writes(self) -> None: + self.fixture.select() + names = {"_write_embedder_metadata", "_write_embedder_metadata_no_commit", "_check_embedder_compatibility", + "init_vec_table", "_rebuild_table_as_cosine", "_finish_cosine_swap", + "migrate_to_cosine_metric", "migrate_legacy_vec_tables"} + module = definitions("vector_search.py", names, self.namespace()) + queries = [] + self.conn.set_trace_callback(queries.append) + for name in sorted(names): + args = (self.conn, "vec_messages") if name == "_rebuild_table_as_cosine" else ( + (self.conn, "vec_messages", 256) if name == "_finish_cosine_swap" else (self.conn,)) + with self.subTest(name=name), self.assertRaises(self.api.TierRuntimeError): + getattr(module, name)(*args) + self.assertFalse(any(sql.lstrip().split(" ", 1)[0].upper() in {"INSERT", "UPDATE", "DELETE", "CREATE", "DROP", "ALTER"} + for sql in queries)) + self.assertFalse(self.conn.in_transaction) + + def test_legacy_guard_preserves_borrowed_transaction_and_source(self) -> None: + self.conn.execute("INSERT INTO messages(id,content) VALUES(77,'synthetic pending')") + before = (self.conn.commits, self.conn.rollbacks) + self.api.require_legacy_vector_mutation(self.conn) + self.assertEqual((self.conn.commits, self.conn.rollbacks), before) + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.conn.execute("SELECT content FROM messages WHERE id=77").fetchone()[0], "synthetic pending") + + def test_destructive_ingest_refuses_selected_before_create_or_unlink(self) -> None: + self.fixture.select() + create = Mock(side_effect=AssertionError("Unexpected create")) + module = definitions("engine.py", {"ingest"}, self.namespace(Path=Path, create_db=create), methods=True) + engine = types.SimpleNamespace(conn=self.conn, db_path=Path(":memory:")) + maintenance = types.SimpleNamespace(maintenance_owner=lambda path: contextlib.nullcontext()) + self.modules["maintenance"] = maintenance + with patch.object(Path, "unlink", side_effect=AssertionError("Unexpected unlink")) as unlink: + with self.assertRaises(self.api.TierRuntimeError): + module.ingest(engine, "synthetic-input") + create.assert_not_called() + unlink.assert_not_called() + self.assertFalse(self.conn.in_transaction) + + def test_destructive_ingest_rejects_borrowed_before_any_cleanup(self) -> None: + self.conn.execute("INSERT INTO messages(id,content) VALUES(88,'synthetic pending')") + self.modules["maintenance"] = types.SimpleNamespace(maintenance_owner=lambda path: contextlib.nullcontext()) + before = (self.conn.commits, self.conn.rollbacks) + with self.assertRaises(self.api.TierRuntimeError): + with self.api.destructive_ingest_operation(types.SimpleNamespace(conn=self.conn, db_path=Path(":memory:"))): + self.fail("Unexpected destructive body") + self.assertTrue(self.conn.in_transaction) + self.assertEqual((self.conn.commits, self.conn.rollbacks), before) + + def test_selected_maintenance_declines_without_native_reconciliation(self) -> None: + self.fixture.select() + with self.api.maintenance_serving_operation(self.conn) as allowed: + self.assertFalse(allowed) + self.assertEqual(self.fixture.loads, []) + + def test_cluster_only_runner_defers_before_dependency_or_builder(self) -> None: + self.fixture.select() + events = [] + @contextlib.contextmanager + def owner(path): + events.append("owner") + yield types.SimpleNamespace(generation="synthetic") + events.append("released") + real = self.api.maintenance_serving_operation + @contextlib.contextmanager + def serving(conn, **kwargs): + self.assertEqual(events, ["owner"]) + with real(conn, **kwargs) as value: + events.append("serving-check") + yield value + source = definitions("maintenance.py", {"LayerResult", "_maintenance_embedding_scope", "_selected_cluster_deferred", "run_layers"}, + self.namespace(NamedTuple=__import__("typing").NamedTuple, contextmanager=contextlib.contextmanager, + maintenance_owner=owner, connection_database_path=lambda conn: None)) + spec = types.SimpleNamespace(layer="clusters", connection=self.conn, + resolve_dependency=Mock(side_effect=AssertionError("Unexpected dependency")), + build=Mock(side_effect=AssertionError("Unexpected compute"))) + with patch.object(self.api, "maintenance_serving_operation", side_effect=serving): + result = source.run_layers(self.conn, (spec,), force=True) + self.assertEqual(result[0].outcome, "deferred") + self.assertEqual(result[0].error_category, "SelectedVectorLayerUnsupported") + self.assertFalse(result[0].attempted) + self.assertEqual(events, ["owner", "serving-check", "released"]) + spec.resolve_dependency.assert_not_called() + spec.build.assert_not_called() + + def test_generic_nonvector_scope_has_no_serving_import_or_model_work(self) -> None: + module = definitions("maintenance.py", {"_maintenance_embedding_scope"}, self.namespace(contextmanager=contextlib.contextmanager)) + with patch.object(self.api, "maintenance_serving_operation", side_effect=AssertionError("Unexpected model scope")): + with module._maintenance_embedding_scope(self.conn, False, None) as allowed: + self.assertTrue(allowed) + + def gate(self, names): + tree = ast.parse((ROOT / "truememory/ingest/encoding_gate.py").read_text(encoding="utf-8")) + cls = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == "EncodingGate") + nodes = [node for node in cls.body if isinstance(node, ast.FunctionDef) and node.name in names] + namespace = self.namespace(log=logging.getLogger("synthetic-public")) + exec(compile(ast.fix_missing_locations(ast.Module(body=[ast.ImportFrom(module="__future__", names=[ast.alias(name="annotations")], level=0), *nodes], type_ignores=[])), "actual-gate-methods", "exec"), namespace) + return namespace + + def test_gate_generation_drops_old_native_reference_and_cached_search(self) -> None: + refresh = self.gate({"_refresh_serving_generation"})["_refresh_serving_generation"] + class Encoder: + pass + model = Encoder() + previous = weakref.ref(model) + gate = types.SimpleNamespace(_embed_model=model, _serving_key=("old",), _last_search_results=[{"content": "synthetic stale"}], _pe_available=False) + del model + with self.api.serving_operation(self.conn) as operation: + refresh(gate) + gc.collect() + self.assertIsNone(previous()) + self.assertEqual(gate._serving_key, operation.key) + self.assertEqual(gate._last_search_results, []) + self.assertTrue(gate._pe_available) + retained = object() + gate._embed_model = retained + refresh(gate) + self.assertIs(gate._embed_model, retained) + + def test_typed_rejections_escape_gate_search_and_normal_availability_degrades(self) -> None: + search = self.gate({"_search"})["_search"] + for error in (self.api.TierRuntimeError("synthetic"), self.fixture.gate.ServingLeaseTimeout("synthetic")): + gate = types.SimpleNamespace(user_id="", memory=types.SimpleNamespace(search=Mock(side_effect=error))) + with self.assertRaises(type(error)) as raised: + search(gate, "synthetic") + self.assertIs(raised.exception, error) + gate = types.SimpleNamespace(user_id="", memory=types.SimpleNamespace(search=Mock(side_effect=RuntimeError("synthetic unavailable")))) + self.assertEqual(search(gate, "synthetic"), []) + + def test_receipt_and_writer_errors_are_typed_not_generic_runtime_or_value(self) -> None: + class ReceiptError(ConnectionError): + pass + class WriterError(RuntimeError): + pass + with patch.dict(sys.modules, {"truememory.model_client": types.SimpleNamespace(ProtocolMismatchError=ReceiptError), + "truememory.tier_switch.writer": types.SimpleNamespace(WriterSelectionChanged=WriterError)}): + for error in (ReceiptError("synthetic"), WriterError("synthetic")): + with self.assertRaises(type(error)) as raised: + self.api.raise_if_serving_rejection(error) + self.assertIs(raised.exception, error) + self.api.raise_if_serving_rejection(RuntimeError("ordinary unavailable")) + self.api.raise_if_serving_rejection(ValueError("ordinary unavailable")) + + def pipeline(self): + names = {"IngestionResult", "IngestionPipeline"} + module = definitions("ingest/pipeline.py", names, self.namespace( + dataclass=dataclasses.dataclass, field=dataclasses.field, contextlib=contextlib, + sqlite3=sqlite3, time=time, datetime=datetime, Path=Path, os=os, log=logging.getLogger("synthetic-public"), + _invalidate_recall_after_batch=lambda result: contextlib.nullcontext(), + _dedup_store_lock=contextlib.nullcontext, _safe_log=lambda value: value)) + return module + + def test_store_and_update_typed_errors_never_fall_back_to_add(self) -> None: + module = self.pipeline() + pipeline = module.IngestionPipeline.__new__(module.IngestionPipeline) + pipeline.user_id = "" + error = self.api.TierRuntimeError("synthetic stale") + fallback = Mock(side_effect=AssertionError("Unexpected fallback")) + pipeline.memory = types.SimpleNamespace(_engine=types.SimpleNamespace(add=Mock(side_effect=error)), add=fallback, + update=Mock(side_effect=error)) + fact = types.SimpleNamespace(category="general") + for action in (lambda: pipeline._store_fact("synthetic", fact, ""), + lambda: pipeline._update_fact(1, "synthetic", fact, "")): + with self.assertRaises(self.api.TierRuntimeError) as raised: + action() + self.assertIs(raised.exception, error) + fallback.assert_not_called() + + def test_dedup_typed_search_refusal_does_not_become_add(self) -> None: + module = definitions("ingest/dedup.py", {"DedupAction", "DedupDecision", "check_duplicate"}, self.namespace( + Enum=enum.Enum, dataclass=dataclasses.dataclass, log=logging.getLogger("synthetic-public"))) + error = self.api.TierRuntimeError("synthetic stale") + memory = types.SimpleNamespace(search_vectors=Mock(side_effect=error)) + with self.assertRaises(self.api.TierRuntimeError) as raised: + module.check_duplicate("synthetic", memory) + self.assertIs(raised.exception, error) + memory.search_vectors.side_effect = RuntimeError("synthetic availability") + self.assertEqual(module.check_duplicate("synthetic", memory).action, module.DedupAction.ADD) + + def test_both_pipeline_routes_pin_one_fact_and_release_before_next_extraction(self) -> None: + module = self.pipeline() + pipeline = module.IngestionPipeline.__new__(module.IngestionPipeline) + engine = types.SimpleNamespace(conn=self.conn, _write_lock=threading.Lock(), _open_connection_handle=lambda: None) + scheduling = definitions("engine.py", {"_maybe_auto_consolidate"}, self.namespace(), methods=True) + engine._has_consolidation = False + engine._maybe_auto_consolidate = types.MethodType(scheduling._maybe_auto_consolidate, engine) + pipeline.memory = types.SimpleNamespace(_engine=engine) + pipeline.llm_config, pipeline.use_llm_dedup, pipeline.user_id, pipeline.gate_enabled = None, False, "", True + fact = types.SimpleNamespace(content="synthetic fact", category="general", confidence=1.0) + observed = [] + def check(label): + op = self.api.current_operation(self.conn) + self.assertIsNotNone(op) + observed.append((label, op)) + def evaluate(*args): + check("gate") + return types.SimpleNamespace(should_encode=True, encoding_score=1.0, novelty=1.0, salience=1.0, + prediction_error=1.0, reason="synthetic") + pipeline.gate = types.SimpleNamespace(reset_batch=lambda: None, evaluate=evaluate) + class Action(enum.Enum): + ADD = "add" + UPDATE = "update" + SKIP = "skip" + def dedup(*args, **kwargs): + check("dedup") + return types.SimpleNamespace(action=Action.ADD, fact=fact.content, reason="synthetic", existing_id=None) + pipeline._store_fact = lambda *args: check("store") + def extract(*args): + self.assertIsNone(self.api.current_operation(self.conn)) + return [fact, fact] + module.extract_facts_simple = extract + module.DedupAction, module.check_duplicate = Action, dedup + module.parse_transcript = lambda path: ["synthetic"] + module.format_for_extraction = lambda messages: "synthetic " * 20 + for action in (lambda: pipeline.ingest_text("synthetic " * 20), lambda: pipeline.ingest_transcript("synthetic-input")): + observed.clear() + result = action() + self.assertEqual(result.facts_stored, 2) + self.assertEqual([label for label, op in observed], ["gate", "dedup", "store"] * 2) + self.assertEqual(len({id(op) for label, op in observed[:3]}), 1) + self.assertIsNot(observed[0][1], observed[3][1]) + self.assertIsNone(self.api.current_operation(self.conn)) + + + def load_application(self, name: str) -> types.ModuleType: + module = types.ModuleType("synthetic_public_" + name.replace(".", "_")) + module.__dict__["__builtins__"] = dict(vars(builtins), __import__=self.imports) + enter_context(self, patch.dict(sys.modules, {module.__name__: module})) + path = ROOT / "truememory" / (name.replace(".", "/") + ".py") + exec(compile(path.read_text(encoding="utf-8"), str(path), "exec"), module.__dict__) + self.modules[name] = module + return module + + def seed_policy(self, *, uppercase: bool = False): + activation = self.modules["tier_switch.activation"] + target = self.modules["embedding_target"].EmbeddingTarget("pro", "qwen3_256", 256, "basepro") + pair = ("vec_messages", "vec_messages_sep") + intent = activation.PolicyIntent("a" * 32, None, None, target, + "Alibaba-NLP/gte-reranker-modernbert-base", activation.ConfigGuard(True, "base", None), "b" * 32, pair) + for key, value in (("embed_model", target.model_id), ("embed_dim", "256"), + ("tier_activation_v1", activation._dump(intent))): + activation._put(self.conn, key, value) + self.conn.commit() + if uppercase: + ddl = self.conn.execute("SELECT sql FROM main.sqlite_master WHERE name='metadata'").fetchone()[0] + self.conn.execute("ALTER TABLE metadata RENAME TO temporary_metadata") + self.conn.execute(ddl.replace("metadata", "METADATA", 1)) + self.conn.execute("INSERT INTO METADATA SELECT * FROM temporary_metadata") + self.conn.commit() + return activation.read_activation_state(self.conn).legacy_policy + + def test_policy_reader_does_not_invent_selection_or_allow_legacy_mutation(self) -> None: + policy = self.seed_policy(uppercase=True) + self.assertIsNone(self.api._read_selection(self.conn)) + self.assertEqual(self.api._read_policy(self.conn), policy) + with self.assertRaises(self.api.TierRuntimeError): + self.api.require_legacy_vector_mutation(self.conn) + with self.assertRaises(self.api.TierRuntimeError): + with self.api.destructive_ingest_operation(types.SimpleNamespace(conn=self.conn, db_path=Path(":memory:"))): + self.fail("Policy database reached destructive body") + self.assertFalse(self.conn.in_transaction) + + def test_policy_writer_fences_absence_generation_and_exact_connection(self) -> None: + writer = self.load_application("tier_switch.writer") + old = writer.capture_writer_selection(self.conn) + policy = self.seed_policy() + fresh = writer.capture_writer_selection(self.conn) + self.assertEqual(fresh.policy, policy) + self.assertIsNone(fresh.selection) + for captured in (old, writer.WriterCapture(self.conn, None), None): + with self.subTest(captured=captured), self.assertRaises(writer.WriterSelectionChanged): + with writer.writer_transaction(self.conn, captured): + self.fail("Uncaptured policy was accepted") + with writer.writer_transaction(self.conn, fresh, model_id=policy.target.model_id, + dimension=256, tables=policy.tables): + self.conn.execute("INSERT INTO messages(id,content) VALUES(901,'synthetic policy fact')") + self.assertEqual(self.conn.execute("SELECT content FROM messages WHERE id=901").fetchone()[0], "synthetic policy fact") + self.conn.execute("BEGIN IMMEDIATE") + for captured in (writer.WriterCapture(self.conn, None, dataclasses.replace(policy, generation="c" * 32)), + writer.WriterCapture(object(), None, policy)): + with self.assertRaises(writer.WriterSelectionChanged): + writer.require_writer_selection(self.conn, captured) + writer.require_writer_selection(self.conn, writer.WriterCapture(self.conn, None, + dataclasses.replace(policy, config_acknowledged=True))) + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + + def install_policy_projectors(self): + client = types.SimpleNamespace() + class EmbeddingProxy: + pass + class CertifiedEmbeddingProxy(EmbeddingProxy): + def __init__(self, target): + self.target = target + class RerankerProxy: + pass + class CertifiedRerankerProxy(RerankerProxy): + def __init__(self, name): + self.name = name + client.EmbeddingProxy, client.CertifiedEmbeddingProxy = EmbeddingProxy, CertifiedEmbeddingProxy + client.RerankerProxy, client.CertifiedRerankerProxy = RerankerProxy, CertifiedRerankerProxy + self.modules["model_client"] = client + vector = definitions("vector_search.py", {"apply_embedding_policy"}, self.namespace( + _model=object(), EMBEDDING_MODEL="qwen3_256", _embedding_dim=256, _model_generation=0, + _target_state_lock=lambda deadline: contextlib.nullcontext(), _target_remaining=lambda deadline: None, + _load_frozen_embedding_target=Mock(side_effect=AssertionError("Unexpected native load")))) + reranker = definitions("reranker.py", {"apply_reranker_policy"}, self.namespace( + _model=object(), _model_name="Alibaba-NLP/gte-reranker-modernbert-base", _model_certified=True, + _reranker_load_lock=lambda control: contextlib.nullcontext())) + self.fixture.vector, self.fixture.reranker = vector, reranker + self.modules["vector_search"], self.modules["reranker"] = vector, reranker + return vector, reranker, client + + def test_same_space_policy_projection_retains_local_models_and_has_zero_loads(self) -> None: + policy = self.seed_policy() + vector, reranker, _ = self.install_policy_projectors() + before = vector._model, reranker._model + with self.fixture.gate.exclusive_activation(): + self.api.apply_runtime_policy(policy) + self.assertIs(vector._model, before[0]) + self.assertIs(reranker._model, before[1]) + self.assertEqual(self.api.runtime_acknowledgement(), self.api.policy_key(policy)) + with self.api.serving_operation(self.conn) as operation: + self.assertEqual(operation.policy, policy) + self.assertIsNone(operation.selection) + self.assertEqual((operation.tables, operation.tier), (policy.tables, "pro")) + with self.api.serving_operation(self.conn) as nested: + self.assertIs(nested, operation) + writer = self.load_application("tier_switch.writer") + self.assertEqual(writer.capture_writer_selection(self.conn).policy, policy) + vector._load_frozen_embedding_target.assert_not_called() + + def test_policy_projection_rebinds_proxies_and_keeps_absent_slots_lazy(self) -> None: + policy = self.seed_policy() + vector, reranker, client = self.install_policy_projectors() + vector._model, reranker._model = client.EmbeddingProxy(), client.RerankerProxy() + self.api.apply_runtime_policy(policy) + self.assertIsInstance(vector._model, client.CertifiedEmbeddingProxy) + self.assertEqual(vector._model.target, policy.target) + self.assertIsInstance(reranker._model, client.CertifiedRerankerProxy) + vector._model = reranker._model = None + self.api.apply_runtime_policy(policy) + self.assertIsNone(vector._model) + self.assertIsNone(reranker._model) + self.assertIsNone(self.api.runtime_acknowledgement()) + vector._load_frozen_embedding_target.assert_not_called() + self.conn.execute("INSERT INTO messages(id,content) VALUES(902,'synthetic borrowed')") + with self.assertRaises(self.api.TierRuntimeError): + with self.api.serving_operation(self.conn): + self.fail("Borrowed policy admission attempted native preparation") + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + vector._load_frozen_embedding_target.side_effect = None + vector._load_frozen_embedding_target.return_value = object() + def prepare(tier, name, **kwargs): + self.assertFalse(self.conn.in_transaction) + reranker._model, reranker._model_certified = object(), True + reranker.apply_frozen_reranker = prepare + with self.api.serving_operation(self.conn) as operation: + self.assertEqual(operation.policy, policy) + vector._load_frozen_embedding_target.assert_called_once() + + def test_policy_projection_failure_never_acknowledges_partial_slots(self) -> None: + policy = self.seed_policy() + _, reranker, _ = self.install_policy_projectors() + self.api.apply_runtime_policy(policy) + error = MemoryError("synthetic projection failure") + reranker.apply_reranker_policy = Mock(side_effect=error) + with self.assertRaises(MemoryError) as raised: + self.api.apply_runtime_policy(policy) + self.assertIs(raised.exception, error) + self.assertIsNone(self.api.runtime_acknowledgement()) + + def test_gate_prediction_error_releases_reference_on_all_outcomes(self) -> None: + namespace = self.gate({"_compute_prediction_error"}) + namespace["EncodingGate"] = types.SimpleNamespace(_refresh_serving_generation=lambda gate: None) + namespace["_PE_NOISE"] = set() + namespace["np"] = types.SimpleNamespace(linalg=types.SimpleNamespace(norm=lambda value: 1.0), dot=lambda a, b: 1.0) + class Model: + pass + model = Model() + gate = types.SimpleNamespace(_embed_model=model, _last_search_results=[{"content": "synthetic nearest"}], + _pe_available=True, _pe_degradation_count=0) + encode = Mock(return_value=[[1.0]] * 4) + self.modules["mps_utils"] = types.SimpleNamespace(encode_with_model_ownership=encode) + for error in (None, self.api.TierRuntimeError("synthetic refusal"), KeyboardInterrupt("synthetic stop")): + gate._embed_model = model + encode.side_effect = error + if error is None: + self.assertEqual(namespace["_compute_prediction_error"](gate, "synthetic fact"), 0.0) + else: + with self.assertRaises(type(error)) as raised: + namespace["_compute_prediction_error"](gate, "synthetic fact") + self.assertIs(raised.exception, error) + self.assertIsNone(gate._embed_model) + + def test_legacy_mutator_owner_refuses_before_sql_and_does_not_end_borrowed_work(self) -> None: + error = RuntimeError("synthetic owner busy") + self.modules["maintenance"].maintenance_owner = Mock(side_effect=error) + self.conn.execute("INSERT INTO messages(id,content) VALUES(903,'synthetic borrowed')") + before = self.conn.commits, self.conn.rollbacks + with self.api.serving_operation(self.conn): + with self.assertRaises(RuntimeError) as raised: + with self.api.legacy_vector_mutation(self.conn): + self.fail("Contended owner admitted mutation") + self.assertIs(raised.exception, error) + self.assertEqual((self.conn.commits, self.conn.rollbacks), before) + self.assertTrue(self.conn.in_transaction) + + def test_legacy_mutator_rechecks_policy_after_owner_acquisition(self) -> None: + @contextlib.contextmanager + def owner(path): + self.seed_policy() + yield + self.modules["maintenance"].maintenance_owner = owner + with self.assertRaises(self.api.TierRuntimeError): + with self.api.legacy_vector_mutation(self.conn): + self.fail("Policy appearing before owner admission reached writes") + + def test_existing_controlled_open_never_calls_schema_factory_and_closes_on_rejection(self) -> None: + policy = self.seed_policy() + factory = Mock(side_effect=AssertionError("Unexpected legacy initialization")) + self.modules["storage"] = types.SimpleNamespace(DEFAULT_BUSY_TIMEOUT_MS=10000) + path = Path(Path.cwd().anchor) / "synthetic" / "database.db" + statements = [] + self.conn.set_trace_callback(statements.append) + with patch.object(Path, "exists", return_value=True), patch.object(self.api.sqlite3, "connect", return_value=self.conn) as connect: + opened = self.api.open_serving_connection(path, factory) + self.assertIs(opened, self.conn) + self.assertEqual(self.api._read_policy(opened), policy) + self.assertIn("?mode=rw", connect.call_args.args[0]) + self.assertFalse(any(sql.lstrip().split(" ", 1)[0].upper() in {"INSERT", "UPDATE", "DELETE", "CREATE", "DROP", "ALTER"} + for sql in statements)) + factory.assert_not_called() + error = self.api.TierRuntimeError("synthetic invalid journal") + close = Mock() + with patch.object(Path, "exists", return_value=True), patch.object(self.api.sqlite3, "connect", return_value=self.conn), \ + patch.object(self.api, "read_activation_state", side_effect=error), patch.object(self.conn, "close", close): + with self.assertRaises(self.api.TierRuntimeError) as raised: + self.api.open_serving_connection(path, factory) + self.assertIs(raised.exception, error) + close.assert_called_once() + + + def test_nonblocking_owner_breaks_reader_activation_cycle(self) -> None: + path = Path(Path.cwd().anchor) / "synthetic" / "database.db" + owner = definitions("maintenance.py", {"MaintenanceBusyError", "MaintenanceOwnership", "_acquire_owner", + "_release_owner", "_bind_owner", "maintenance_owner"}, self.namespace( + NamedTuple=__import__("typing").NamedTuple, contextmanager=contextlib.contextmanager, + Path=Path, os=types.SimpleNamespace(getpid=os.getpid, close=lambda fd: None), uuid=uuid, + canonical_database_path=lambda value: path, _registry_lock=threading.Lock(), + _held_paths={}, _held_fds=set(), _owner_waiters={}, _coordinators={}, + _thread_owners=threading.local(), try_file_lock=lambda value: 987)) + owner.connection_database_path = lambda conn: path + self.modules["maintenance"] = owner + held, finished = threading.Event(), threading.Event() + entered, errors = [], [] + def activate(): + try: + with owner.maintenance_owner(path): + held.set() + with self.fixture.gate.exclusive_activation(): + entered.append(True) + except BaseException as error: + errors.append(error) + finally: + finished.set() + self.conn.execute("INSERT INTO messages(id,content) VALUES(904,'synthetic borrowed')") + before = self.conn.commits, self.conn.rollbacks + with self.api.serving_operation(self.conn): + worker = threading.Thread(target=activate) + worker.start() + self.assertTrue(held.wait(2)) + with self.assertRaises(owner.MaintenanceBusyError): + with self.api.legacy_vector_mutation(self.conn): + self.fail("Busy activation owner allowed legacy writes") + self.assertEqual(entered, []) + self.assertTrue(self.conn.in_transaction) + self.assertEqual((self.conn.commits, self.conn.rollbacks), before) + self.assertTrue(finished.wait(2)) + worker.join(2) + self.assertFalse(worker.is_alive()) + self.assertEqual(errors, []) + self.assertEqual(entered, [True]) + self.assertEqual(owner._held_paths, {}) + + def test_cold_selected_consolidate_defers_before_model_or_engine_initialization(self) -> None: + self.fixture.select() + events = [] + report = types.SimpleNamespace(results=()) + coordinator = types.SimpleNamespace(path=None, refresh_capabilities=lambda: events.append("capabilities")) + @contextlib.contextmanager + def observation(value, **kwargs): + events.append("owner") + yield [] + lock = threading.Lock() + def run(conn, supplied, **kwargs): + self.assertFalse(lock.locked()) + with self.api.maintenance_serving_operation(conn) as supported: + self.assertFalse(supported) + events.append("deferred") + return report + self.modules["maintenance"] = types.SimpleNamespace( + MaintenanceBusyError=type("Busy", (RuntimeError,), {}), format_maintenance_report=lambda value: {"clusters": "deferred"}, + maintenance_busy_result=lambda: {}, maintenance_observation=observation, + observe_style=Mock(side_effect=AssertionError("Unexpected style observation")), + run_engine_maintenance=run, _style_unready_result=lambda value: value) + source = definitions("engine.py", {"consolidate"}, self.namespace(), methods=True) + engine = types.SimpleNamespace(conn=self.conn, _write_lock=lock, _open_connection_handle=lambda: events.append("open"), + _ensure_connection=Mock(side_effect=AssertionError("Unexpected runtime initialization")), + _get_maintenance_coordinator=lambda: coordinator, _auto_consolidate_threshold=25, + _apply_manual_maintenance_capabilities=lambda results: None) + self.assertEqual(source.consolidate(engine), {"clusters": "deferred"}) + self.assertEqual(events, ["open", "capabilities", "owner", "deferred"]) + self.assertEqual(self.fixture.loads, []) + engine._ensure_connection.assert_not_called() + + def test_selected_foreground_metadata_is_fenced_noop_preserving_caller_sql(self) -> None: + selected = self.fixture.select() + writer = self.load_application("tier_switch.writer") + activation = self.modules["tier_switch.activation"] + activation._put(self.conn, "embed_model", selected.target.model_id) + activation._put(self.conn, "embed_dim", str(selected.target.dimension)) + self.conn.commit() + module = definitions("vector_search.py", {"_write_embedder_metadata_no_commit"}, self.namespace( + EMBEDDING_MODEL=selected.target.model_id, _embedding_dim=selected.target.dimension)) + with self.api.serving_operation(self.conn): + captured = writer.capture_writer_selection(self.conn) + with writer.writer_transaction(self.conn, captured): + changes = self.conn.total_changes + module._write_embedder_metadata_no_commit(self.conn) + self.assertEqual(self.conn.total_changes, changes) + self.assertTrue(self.conn.in_transaction) + module._embedding_dim += 1 + with self.assertRaises(writer.WriterSelectionChanged): + module._write_embedder_metadata_no_commit(self.conn) + self.assertFalse(self.conn.in_transaction) + + def test_legacy_initializer_propagates_typed_guard_or_owner_refusal(self) -> None: + class Busy(RuntimeError): + pass + self.modules["maintenance"].MaintenanceBusyError = Busy + enter_context(self, patch.dict(sys.modules, {"truememory.maintenance": self.modules["maintenance"]})) + def imports(name, globals=None, locals=None, fromlist=(), level=0): + if name == "sqlite_vec": + return types.SimpleNamespace(load=lambda conn: None) + return self.imports(name, globals, locals, fromlist, level) + self.conn.enable_load_extension = lambda value: None + module = definitions("engine.py", {"_initialize_connection"}, dict( + __builtins__=dict(vars(builtins), __import__=imports), _HAS_VECTOR=True, + TrueMemoryMigrationError=type("Migration", (RuntimeError,), {})), methods=True) + engine = types.SimpleNamespace(conn=self.conn, ready=False, _runtime_initialized=False, + _init_lock=threading.Lock(), _write_lock=threading.Lock(), _has_vectors=False) + for error in (self.api.TierRuntimeError("synthetic changed"), Busy("synthetic owner busy")): + module.init_vec_table = Mock(side_effect=error) + with self.api.serving_operation(self.conn): + with self.assertRaises(type(error)) as raised: + module._initialize_connection(engine, _suppress_maintenance=True) + self.assertIs(raised.exception, error) + self.assertFalse(engine.ready) + self.assertFalse(engine._runtime_initialized) + + def test_hybrid_optional_separation_refusal_does_not_degrade(self) -> None: + source = definitions("hybrid.py", {"search_hybrid"}, self.namespace(database_operation=lambda function: function)) + error = self.api.TierRuntimeError("synthetic stale separation") + self.modules["fts_search"] = types.SimpleNamespace(search_fts=Mock()) + self.modules["vector_search"] = types.SimpleNamespace(search_vector=Mock(), search_vector_separation=Mock(), + _active_sep_table=Mock(side_effect=error)) + with self.assertRaises(self.api.TierRuntimeError) as raised: + source.search_hybrid(self.conn, "synthetic") + self.assertIs(raised.exception, error) + self.modules["vector_search"].search_vector.assert_not_called() + self.modules["fts_search"].search_fts.assert_not_called() + + + def test_policy_initialization_validates_exact_pair_without_ddl_or_metadata(self) -> None: + policy = self.seed_policy() + self.install_policy_projectors() + self.api.apply_runtime_policy(policy) + for table in policy.tables: + self.conn.execute(f"CREATE TABLE {table}(embedding BLOB)") + original = type(self.conn).execute + def execute(conn, sql, parameters=()): + if sql == "SELECT sql FROM main.sqlite_master WHERE name=? COLLATE NOCASE" and parameters[0] in policy.tables: + return BRIDGE["JOURNAL"]["Rows"]([(f"CREATE VIRTUAL TABLE {parameters[0]} USING vec0(embedding float[256] distance_metric=cosine)",)]) + return original(conn, sql, parameters) + module = definitions("vector_search.py", {"init_vec_table"}, self.namespace()) + statements = [] + self.conn.set_trace_callback(statements.append) + with patch.object(type(self.conn), "execute", execute): + with self.api.serving_operation(self.conn): + module.init_vec_table(self.conn) + with self.assertRaises(ValueError): + module.init_vec_table(self.conn, tier_group="basepro") + self.assertFalse(any(sql.lstrip().split(" ", 1)[0].upper() in {"INSERT", "UPDATE", "DELETE", "CREATE", "DROP", "ALTER"} + for sql in statements)) + self.assertFalse(self.conn.in_transaction) + + def test_policy_loaded_runtime_cannot_be_inherited_by_unselected_database(self) -> None: + policy = self.seed_policy() + self.install_policy_projectors() + self.api.apply_runtime_policy(policy) + other = sqlite3.connect(":memory:") + self.addCleanup(other.close) + with self.assertRaises(self.api.TierRuntimeError): + with self.api.serving_operation(other): + self.fail("Unselected database silently inherited another policy") + + + def maintenance_runner(self, *, owner=None, run_layers=None): + owner = owner or (lambda path: contextlib.nullcontext()) + module = definitions("maintenance.py", { + "LayerResult", "MaintenanceReport", "SCHEDULED_PREFERENCES_UNAVAILABLE", + "_maintenance_embedding_scope", "_maintenance_completion", "run_engine_maintenance", + }, self.namespace( + NamedTuple=__import__("typing").NamedTuple, contextmanager=contextlib.contextmanager, + maintenance_owner=owner, connection_database_path=lambda conn: None, + engine_layer_specs=lambda *args, **kwargs: (), all_layer_specs=lambda conn: (), + run_layers=run_layers or (lambda *args, **kwargs: ()), time=time, + importlib=types.SimpleNamespace(import_module=lambda name: types.SimpleNamespace(extract_preferences=lambda conn: None)), + )) + return module + + def memory_connection(self) -> sqlite3.Connection: + conn = sqlite3.connect(":memory:", check_same_thread=False, + factory=BRIDGE["JOURNAL"]["JournalConnection"]) + self.conn.backup(conn) + self.addCleanup(conn.close) + return conn + + def real_memory_owner(self, acquire): + return definitions("maintenance.py", {"MaintenanceOwnership", "MaintenanceBusyError", + "canonical_database_path", "_bind_owner", "maintenance_owner", "_active_initialization_receipt"}, self.namespace( + NamedTuple=__import__("typing").NamedTuple, contextmanager=contextlib.contextmanager, + Path=Path, os=os, uuid=uuid, _thread_owners=threading.local(), weakref=weakref, + _registry_lock=threading.Lock(), _coordinators=weakref.WeakValueDictionary(), _held_paths={}, + _acquire_owner=acquire, _release_owner=lambda fd: None)) + + def test_maintenance_authority_waits_for_managed_writer_before_sql(self) -> None: + conn = self.memory_connection() + engine = self.fixture.lifecycle_engine("_open_connection_handle", conn) + owned, waiting, release, finished = (threading.Event() for _ in range(4)) + failures, reads = [], [] + api = self.api + + class ObservedLock(api.ConnectionWriteLock): + def acquire(self, blocking=True, timeout=-1): + if self.owns_other_transaction(conn): + waiting.set() + return super().acquire(blocking, timeout) + + engine._write_lock = lock = ObservedLock(engine) + original = api._read_selection + + def read(connection): + self.assertFalse(lock.owns_other_transaction(connection)) + self.assertTrue(lock.locked()) + reads.append(connection) + return original(connection) + + def writer(): + try: + with lock: + conn.execute("INSERT INTO messages(id,content) VALUES(991,'synthetic writer')") + owned.set() + if not release.wait(2): + raise AssertionError("Synthetic writer was not released") + conn.commit() + except BaseException as error: + failures.append(error) + + def reader(): + try: + with api.maintenance_serving_operation(conn, connection_lock=lock, + deadline=time.monotonic() + 2) as allowed: + self.assertTrue(allowed) + self.assertFalse(lock.locked()) + finished.set() + except BaseException as error: + failures.append(error) + + first, second = threading.Thread(target=writer), threading.Thread(target=reader) + with patch.object(api, "_read_selection", side_effect=read): + first.start() + try: + self.assertTrue(owned.wait(2)) + second.start() + self.assertTrue(waiting.wait(2)) + self.assertEqual(reads, []) + finally: + release.set() + first.join(2) + if second.ident is not None: + second.join(2) + self.assertFalse(first.is_alive()) + self.assertFalse(second.is_alive()) + self.assertEqual(failures, []) + self.assertTrue(finished.is_set()) + self.assertGreaterEqual(len(reads), 3) + + def test_maintenance_borrowed_busy_cancel_and_deadline_preserve_sql(self) -> None: + engine = self.fixture.lifecycle_engine("_open_connection_handle") + self.conn.execute("INSERT INTO messages(id,content) VALUES(991,'synthetic caller')") + before = self.conn.commits, self.conn.rollbacks + with engine._write_lock: + with self.assertRaises(self.api.TierRuntimeError): + with self.api.maintenance_serving_operation(self.conn, connection_lock=engine._write_lock): + self.fail("Borrowed busy admission entered maintenance") + cancelled = threading.Event() + cancelled.set() + for controls, kind in (({"cancelled": cancelled}, self.fixture.gate.ServingLeaseCancelled), + ({"deadline": time.monotonic() - 1}, self.fixture.gate.ServingLeaseTimeout)): + with self.subTest(kind=kind.__name__), self.assertRaises(kind): + with self.api.maintenance_serving_operation(self.conn, connection_lock=engine._write_lock, **controls): + self.fail("Rejected admission entered maintenance") + self.assertTrue(self.conn.in_transaction) + self.assertEqual((self.conn.commits, self.conn.rollbacks), before) + self.assertEqual(self.conn.execute("SELECT content FROM messages WHERE id=991").fetchone()[0], "synthetic caller") + self.assertFalse(engine._write_lock.locked()) + + def test_nested_maintenance_reuses_lease_without_sql_or_writer_reacquisition(self) -> None: + engine = self.fixture.lifecycle_engine("_open_connection_handle") + with self.api.maintenance_serving_operation(self.conn, connection_lock=engine._write_lock) as allowed: + self.assertTrue(allowed) + operation = self.api.current_operation(self.conn) + with engine._write_lock, \ + patch.object(self.api, "_read_selection", side_effect=AssertionError("Nested authority SQL")), \ + patch.object(self.api, "_read_policy", side_effect=AssertionError("Nested policy SQL")), \ + patch.object(self.api, "_connection_read", side_effect=AssertionError("Nested writer acquisition")): + with self.api.maintenance_serving_operation(self.conn, connection_lock=engine._write_lock) as nested: + self.assertTrue(nested) + self.assertIs(self.api.current_operation(self.conn), operation) + + def test_runtime_final_table_and_database_snapshots_hold_writer(self) -> None: + engine = self.fixture.lifecycle_engine("_open_connection_handle") + observed = [] + + def snapshot(name, value): + def read(conn): + self.assertTrue(engine._write_lock.locked()) + observed.append(name) + return value + return read + + with patch.object(self.fixture.vector, "_active_vec_table", side_effect=snapshot("normal", "vec_messages")), \ + patch.object(self.fixture.vector, "_active_sep_table", side_effect=snapshot("separation", "vec_messages_sep")), \ + patch.object(self.api, "_database_identity", side_effect=snapshot("database", ("memory", self.conn))): + with self.api.serving_operation(self.conn, connection_lock=engine._write_lock) as operation: + self.assertEqual(operation.tables, ("vec_messages", "vec_messages_sep")) + self.assertFalse(engine._write_lock.locked()) + self.assertEqual(observed, ["normal", "separation", "database"]) + + def test_runtime_preparation_and_gate_admission_release_writer(self) -> None: + self.fixture.select() + engine = self.fixture.lifecycle_engine("_open_connection_handle") + prepare, lease = self.api.apply_frozen_selection, self.fixture.gate.operation_lease + + def project(*args, **kwargs): + self.assertFalse(engine._write_lock.locked()) + return prepare(*args, **kwargs) + + @contextlib.contextmanager + def admit(*args, **kwargs): + self.assertFalse(engine._write_lock.locked()) + with lease(*args, **kwargs) as value: + yield value + + with patch.object(self.api, "apply_frozen_selection", side_effect=project), \ + patch.object(self.fixture.gate, "operation_lease", side_effect=admit): + with self.api.serving_operation(self.conn, connection_lock=engine._write_lock): + self.assertFalse(engine._write_lock.locked()) + self.assertEqual(len(self.fixture.loads), 2) + + def test_runner_completion_cleans_owned_sql_under_lock_and_keeps_borrowed_sql(self) -> None: + engine = self.fixture.lifecycle_engine("_open_connection_handle") + + def layers(conn, *args, **kwargs): + self.assertTrue(engine._write_lock.locked()) + conn.execute("INSERT INTO messages(id,content) VALUES(992,'synthetic unfinished')") + return () + + module = self.maintenance_runner(run_layers=layers) + rollback = self.conn.rollback + calls = [] + + def clean(): + calls.append(engine._write_lock.locked()) + return rollback() + + with patch.object(self.conn, "rollback", side_effect=clean): + with self.assertRaisesRegex(RuntimeError, "unfinished transaction"): + module.run_engine_maintenance(self.conn, object(), prepare_extensions=False, + _connection_lock=engine._write_lock) + self.assertTrue(calls) + self.assertTrue(all(calls)) + self.assertFalse(self.conn.in_transaction) + self.assertEqual(self.conn.execute("SELECT count(*) FROM messages WHERE id=992").fetchone()[0], 0) + self.conn.execute("INSERT INTO messages(id,content) VALUES(991,'synthetic caller')") + before = self.conn.commits, self.conn.rollbacks + module.run_engine_maintenance(self.conn, object(), prepare_extensions=False, + _connection_lock=engine._write_lock, allow_caller_transaction=True) + self.assertTrue(self.conn.in_transaction) + self.assertEqual((self.conn.commits, self.conn.rollbacks), before) + self.assertEqual(self.conn.execute("SELECT count(*) FROM messages WHERE id IN (991,992)").fetchone()[0], 2) + + + def test_opener_owns_canonical_path_before_writer_and_reenters_real_owner(self) -> None: + replacement = self.memory_connection() + engine = self.fixture.lifecycle_engine("_open_connection_handle") + engine.conn = None + engine.db_path = Path("~synthetic-unexpanded/store.sqlite") + enter_context(self, patch.object(Path, "resolve", _REAL_PATH_RESOLVE)) + expected = engine.db_path.resolve() + acquired, created = [], [] + + def acquire(path): + self.assertTrue(engine._init_lock.locked()) + self.assertFalse(engine._write_lock.locked()) + self.assertEqual(path, expected) + acquired.append(path) + return 17 + + owner = self.real_memory_owner(acquire) + self.modules["maintenance"] = owner + + def create(path): + self.assertEqual(path, expected) + self.assertTrue(engine._init_lock.locked()) + self.assertTrue(engine._write_lock.locked()) + self.assertIn(expected, owner._thread_owners.owners) + created.append(path) + return replacement + + engine._open_connection_handle.__globals__["create_db"] = create + with patch.object(Path, "mkdir") as mkdir, patch.object(Path, "chmod") as chmod, \ + patch.object(Path, "exists", return_value=False): + engine._open_connection_handle() + self.assertEqual(acquired, [expected]) + self.assertEqual(created, [expected]) + self.assertIs(engine.conn, replacement) + self.assertEqual(owner._thread_owners.owners, {}) + self.assertFalse(engine.ready) + self.assertFalse(engine._runtime_initialized) + self.assertFalse(engine._init_lock.locked()) + self.assertFalse(engine._write_lock.locked()) + mkdir.assert_called_once_with(parents=True, exist_ok=True) + chmod.assert_called_once_with(0o700) + + def test_opener_healthy_and_borrowed_handles_do_not_request_owner(self) -> None: + owner = Mock(side_effect=AssertionError("Healthy handle requested maintenance owner")) + self.modules["maintenance"].maintenance_owner = owner + engine = self.fixture.lifecycle_engine("_open_connection_handle") + engine._open_connection_handle() + self.conn.execute("INSERT INTO messages(id,content) VALUES(991,'synthetic caller')") + before = self.conn.commits, self.conn.rollbacks + engine._open_connection_handle() + error = sqlite3.OperationalError("disk I/O error") + self.conn.armed, self.conn.fault_contains, self.conn.fault = True, "PRAGMA schema_version", error + with patch.object(self.conn, "close", wraps=self.conn.close) as close: + with self.assertRaises(sqlite3.OperationalError) as raised: + engine._open_connection_handle() + close.assert_not_called() + self.assertIs(raised.exception, error) + self.assertIs(engine.conn, self.conn) + self.assertTrue(self.conn.in_transaction) + self.assertEqual((self.conn.commits, self.conn.rollbacks), before) + self.assertTrue(engine.ready) + owner.assert_not_called() + + def test_opener_owner_busy_preserves_ioerr_handle_and_ready_state(self) -> None: + owner = self.real_memory_owner(lambda path: None) + self.modules["maintenance"] = owner + engine = self.fixture.lifecycle_engine("_open_connection_handle") + engine.db_path = Path("synthetic/store.sqlite") + self.conn.armed, self.conn.fault_contains = True, "PRAGMA schema_version" + self.conn.fault = sqlite3.OperationalError("disk I/O error") + before = self.conn.commits, self.conn.rollbacks + with patch.object(Path, "mkdir"), patch.object(Path, "chmod"), \ + patch.object(self.conn, "close", wraps=self.conn.close) as close: + with self.assertRaises(owner.MaintenanceBusyError): + engine._open_connection_handle() + close.assert_not_called() + self.assertIs(engine.conn, self.conn) + self.assertTrue(engine.ready) + self.assertTrue(engine._runtime_initialized) + self.assertIs(engine._runtime_vector_connection, self.conn) + self.assertEqual((self.conn.commits, self.conn.rollbacks), before) + self.assertFalse(engine._write_lock.locked()) + self.assertFalse(engine._init_lock.locked()) + + def test_opener_second_probe_does_not_retire_a_new_healthy_handle(self) -> None: + replacement = self.memory_connection() + engine = self.fixture.lifecycle_engine("_open_connection_handle") + engine.db_path = Path("synthetic/store.sqlite") + self.conn.armed, self.conn.fault_contains = True, "PRAGMA schema_version" + self.conn.fault = sqlite3.OperationalError("disk I/O error") + + def acquire(path): + self.assertFalse(engine._write_lock.locked()) + engine.conn = replacement + return 17 + + self.modules["maintenance"] = self.real_memory_owner(acquire) + create = Mock(side_effect=AssertionError("Rechecked healthy connection was replaced")) + engine._open_connection_handle.__globals__["create_db"] = create + with patch.object(Path, "mkdir"), patch.object(Path, "chmod"), \ + patch.object(self.conn, "close", wraps=self.conn.close) as close: + engine._open_connection_handle() + close.assert_not_called() + self.assertIs(engine.conn, replacement) + create.assert_not_called() + + def test_concurrent_first_open_uses_one_factory_and_no_owner_contention(self) -> None: + replacement = self.memory_connection() + engine = self.fixture.lifecycle_engine("_open_connection_handle") + engine.conn, engine.db_path = None, Path("synthetic/store.sqlite") + acquired, created, failures = [], [], [] + factory_entered, release, second_waiting = (threading.Event() for _ in range(3)) + real_init = engine._init_lock + + class ObservedInit: + def acquire(self, blocking=True, timeout=-1): + if real_init.locked(): + second_waiting.set() + return real_init.acquire(blocking, timeout) + + def release(self): + real_init.release() + + def locked(self): + return real_init.locked() + + engine._init_lock = ObservedInit() + + def acquire(path): + self.assertFalse(engine._write_lock.locked()) + acquired.append(path) + return 17 + + self.modules["maintenance"] = self.real_memory_owner(acquire) + + def create(path): + created.append(path) + factory_entered.set() + if not release.wait(2): + raise AssertionError("Synthetic factory release timed out") + return replacement + + engine._open_connection_handle.__globals__["create_db"] = create + + def opening(): + try: + engine._open_connection_handle() + except BaseException as error: + failures.append(error) + + first, second = threading.Thread(target=opening), threading.Thread(target=opening) + with patch.object(Path, "mkdir"), patch.object(Path, "chmod"), patch.object(Path, "exists", return_value=False): + first.start() + try: + self.assertTrue(factory_entered.wait(2)) + second.start() + self.assertTrue(second_waiting.wait(2)) + self.assertEqual(len(acquired), 1) + self.assertEqual(len(created), 1) + finally: + release.set() + first.join(2) + if second.ident is not None: + second.join(2) + self.assertFalse(first.is_alive()) + self.assertFalse(second.is_alive()) + self.assertEqual(failures, []) + self.assertEqual(len(acquired), 1) + self.assertEqual(len(created), 1) + self.assertIs(engine.conn, replacement) + + + def test_consolidate_guards_authority_and_style_table_snapshots(self) -> None: + self.fixture.select() + engine = self.fixture.lifecycle_engine("_open_connection_handle") + engine._open_connection_handle = lambda: None + engine._ensure_connection = Mock(side_effect=AssertionError("Controlled store initialized models")) + engine._auto_consolidate_threshold = 25 + engine._get_maintenance_coordinator = lambda: types.SimpleNamespace(path=None, refresh_capabilities=lambda: None) + engine._apply_manual_maintenance_capabilities = lambda results: None + report = types.SimpleNamespace(results=()) + + def run(conn, coordinator, **kwargs): + self.assertFalse(engine._write_lock.locked()) + self.assertIs(kwargs["_connection_lock"], engine._write_lock) + return report + + self.modules["maintenance"] = types.SimpleNamespace( + MaintenanceBusyError=type("Busy", (RuntimeError,), {}), format_maintenance_report=lambda value: {}, + maintenance_busy_result=lambda: {}, maintenance_observation=lambda *args, **kwargs: contextlib.nullcontext([]), + observe_style=Mock(side_effect=AssertionError("Unexpected file style probe")), + run_engine_maintenance=run, _style_unready_result=lambda value: value) + source = definitions("engine.py", {"consolidate"}, self.namespace(), methods=True) + execute = self.conn.execute + observed = [] + + def guarded(sql, *args, **kwargs): + self.assertTrue(engine._write_lock.locked()) + observed.append(sql) + return execute(sql, *args, **kwargs) + + with patch.object(self.conn, "execute", side_effect=guarded): + self.assertEqual(source.consolidate(engine), {}) + self.assertTrue(any("tier_selected_v1" in str(args) for args in observed) + or any("main.metadata" in sql for sql in observed)) + self.assertTrue(any("entity_style_vectors" in sql for sql in observed)) + engine._ensure_connection.assert_not_called() + + def test_consolidate_release_handoff_preserves_next_writer_with_negative_control(self) -> None: + for restore_old_cleanup in (False, True): + with self.subTest(restore_old_cleanup=restore_old_cleanup): + conn = self.memory_connection() + engine = self.fixture.lifecycle_engine("_open_connection_handle", conn) + engine._open_connection_handle = lambda: None + engine._ensure_connection = lambda **kwargs: None + engine._auto_consolidate_threshold = 25 + engine._has_style_vec = False + coordinator = types.SimpleNamespace(path=None, refresh_capabilities=lambda: None) + engine._get_maintenance_coordinator = lambda: coordinator + engine._apply_manual_maintenance_capabilities = lambda results: None + armed, released, writer_started, writer_release = (threading.Event() for _ in range(4)) + failures = [] + main_thread = threading.get_ident() + api = self.api + owner = self + + class HandoffLock(api.ConnectionWriteLock): + def release(self): + handoff = armed.is_set() and threading.get_ident() == main_thread + if handoff: + armed.clear() + super().release() + if handoff: + released.set() + owner.assertTrue(writer_started.wait(2), "Synthetic next writer did not start") + + engine._write_lock = lock = HandoffLock(engine) + + def layers(*args, **kwargs): + self.assertTrue(lock.locked()) + armed.set() + return () + + runner = self.maintenance_runner(run_layers=layers) + self.modules["maintenance"] = types.SimpleNamespace( + MaintenanceBusyError=type("Busy", (RuntimeError,), {}), + format_maintenance_report=lambda report: {"result": "synthetic complete"}, maintenance_busy_result=lambda: {}, + maintenance_observation=lambda *args, **kwargs: contextlib.nullcontext([]), + observe_style=Mock(side_effect=AssertionError("Unexpected style probe")), + run_engine_maintenance=runner.run_engine_maintenance, _style_unready_result=lambda value: value) + source = definitions("engine.py", {"consolidate"}, self.namespace(), methods=True) + if restore_old_cleanup: + tree = ast.parse((ROOT / "truememory/engine.py").read_text(encoding="utf-8")) + cls = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == "TrueMemoryEngine") + method = next(node for node in cls.body if isinstance(node, ast.FunctionDef) and node.name == "consolidate") + + class RestoreCleanup(ast.NodeTransformer): + inserted = 0 + + def visit_Assign(self, node): + if (isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Name) + and node.value.func.id == "run_engine_maintenance" + and any(item.arg == "_connection_lock" for item in node.value.keywords)): + self.inserted += 1 + old = ast.parse("if not borrowed and self.conn.in_transaction:\n" + " self.conn.rollback()\n" + " raise RuntimeError('Maintenance work left an unfinished transaction')").body[0] + return [node, old] + return node + + restore = RestoreCleanup() + method = restore.visit(method) + self.assertEqual(restore.inserted, 1) + exec(compile(ast.fix_missing_locations(ast.Module(body=[method], type_ignores=[])), + "synthetic-old-cleanup-negative-control", "exec"), source.__dict__) + + def next_writer(): + try: + if not released.wait(2): + raise AssertionError("Maintenance mutation lock was not released") + with lock: + conn.execute("INSERT INTO messages(id,content) VALUES(993,'synthetic next writer')") + writer_started.set() + if not writer_release.wait(2): + raise AssertionError("Synthetic next writer was not released") + conn.rollback() + except BaseException as error: + failures.append(error) + + thread = threading.Thread(target=next_writer) + thread.start() + try: + if restore_old_cleanup: + with self.assertRaisesRegex(RuntimeError, "unfinished transaction"): + source.consolidate(engine) + self.assertFalse(conn.in_transaction) + self.assertEqual(conn.execute("SELECT count(*) FROM messages WHERE id=993").fetchone()[0], 0) + else: + self.assertEqual(source.consolidate(engine), {"result": "synthetic complete"}) + self.assertTrue(conn.in_transaction) + self.assertEqual(conn.execute("SELECT content FROM messages WHERE id=993").fetchone()[0], + "synthetic next writer") + finally: + writer_release.set() + thread.join(2) + self.assertFalse(thread.is_alive()) + self.assertEqual(failures, []) + self.assertTrue(writer_started.is_set()) + + + def test_already_cancelled_runner_does_not_admit_or_wait_for_managed_writer(self) -> None: + conn = self.memory_connection() + engine = self.fixture.lifecycle_engine("_open_connection_handle", conn) + owned, release = threading.Event(), threading.Event() + failures = [] + cancel = threading.Event() + cancel.set() + module = self.maintenance_runner(owner=Mock(side_effect=AssertionError("Cancelled work requested owner"))) + module.connection_database_path = Mock(side_effect=AssertionError("Cancelled work read database identity")) + module.run_routed_style = Mock(side_effect=AssertionError("Cancelled work called mutable style route")) + + def writer() -> None: + try: + with engine._write_lock: + conn.execute("INSERT INTO messages(id,content) VALUES(994,'synthetic managed writer')") + owned.set() + if not release.wait(2): + raise AssertionError("Synthetic writer was not released") + conn.rollback() + except BaseException as error: + failures.append(error) + + thread = threading.Thread(target=writer) + thread.start() + try: + self.assertTrue(owned.wait(2)) + before = conn.commits, conn.rollbacks + with patch.object(self.api, "_connection_read", side_effect=AssertionError("Cancelled work entered lock admission")), \ + patch.object(conn, "execute", side_effect=AssertionError("Cancelled work executed SQL")): + for include_style in (False, True): + with self.subTest(include_style=include_style): + report = module.run_engine_maintenance(conn, object(), cancel=cancel, + include_style=include_style, _connection_lock=engine._write_lock) + self.assertEqual(report.results, ()) + self.assertEqual(report.preferences, "CANCELLED") + if include_style: + self.assertEqual(tuple(report.style_result), ( + "style_vectors", "style_vectors", "deferred", None, 0.0, + "StyleCancelled", False, "unverified", True)) + else: + self.assertIsNone(report.style_result) + self.assertTrue(engine._write_lock.owns_other_transaction(conn)) + self.assertTrue(conn.in_transaction) + self.assertEqual((conn.commits, conn.rollbacks), before) + self.assertEqual(conn.execute("SELECT content FROM messages WHERE id=994").fetchone()[0], + "synthetic managed writer") + module.maintenance_owner.assert_not_called() + module.connection_database_path.assert_not_called() + module.run_routed_style.assert_not_called() + finally: + release.set() + thread.join(2) + self.assertFalse(thread.is_alive()) + self.assertEqual(failures, []) + + def test_cancelled_snapshot_stays_terminal_when_event_clears(self) -> None: + module = self.maintenance_runner(owner=Mock(side_effect=AssertionError("Cancelled snapshot requested owner"))) + module.connection_database_path = Mock(side_effect=AssertionError("Cancelled snapshot read database identity")) + module.run_routed_style = Mock(side_effect=AssertionError("Cleared event reached style SQL")) + for borrowed in (False, True): + if borrowed: + self.conn.execute("INSERT INTO messages(id,content) VALUES(994,'synthetic caller SQL')") + before = self.conn.commits, self.conn.rollbacks + for include_style in (False, True): + with self.subTest(borrowed=borrowed, include_style=include_style): + cancel = threading.Event() + cancel.set() + + def capture_and_clear() -> bool: + cancel.clear() + return True + + with patch.object(cancel, "is_set", side_effect=capture_and_clear) as checked, \ + patch.object(self.api, "_connection_read", side_effect=AssertionError("Cancelled snapshot waited")), \ + patch.object(self.conn, "execute", side_effect=AssertionError("Cancelled snapshot executed SQL")): + report = module.run_engine_maintenance(self.conn, object(), cancel=cancel, + include_style=include_style, allow_caller_transaction=borrowed) + checked.assert_called_once_with() + self.assertFalse(cancel.is_set()) + self.assertEqual(report.results, ()) + self.assertEqual(report.preferences, "CANCELLED") + if include_style: + self.assertEqual(tuple(report.style_result), ( + "style_vectors", "style_vectors", "deferred", None, 0.0, + "StyleCancelled", False, "unverified", borrowed)) + else: + self.assertIsNone(report.style_result) + self.assertEqual(self.conn.in_transaction, borrowed) + self.assertEqual((self.conn.commits, self.conn.rollbacks), before) + if borrowed: + self.assertEqual(self.conn.execute("SELECT content FROM messages WHERE id=994").fetchone()[0], + "synthetic caller SQL") + self.conn.rollback() + module.maintenance_owner.assert_not_called() + module.connection_database_path.assert_not_called() + module.run_routed_style.assert_not_called() + + +class TestManagedModelAdmission(unittest.TestCase): + """Actual model guard classifies the owner of SQL on a shared handle.""" + + setUp = TestPublicBoundaries.setUp + memory_connection = TestPublicBoundaries.memory_connection + + def writer_lock(self, conn): + class Engine: + pass + engine = Engine() + engine.conn = conn + lock = self.api.ConnectionWriteLock(engine) + return engine, lock + + def test_model_guard_reprobes_owner_release_without_using_stale_sql_flag(self): + # Negative control is the exact pre-fix ordering: read SQL first, then + # ask whether the competing managed writer still owns the handle. + def previous_guard(operation, lock): + if operation.borrowed or operation._connection.in_transaction and not ( + isinstance(lock, self.api.ConnectionWriteLock) + and lock.owns_other_transaction(operation._connection)): + raise self.api.TierRuntimeError("Model loading requires a clean connection") + for previous in (True, False): + with self.subTest(previous_order=previous): + conn = self.memory_connection() + engine, lock = self.writer_lock(conn) + entered, release, finished = threading.Event(), threading.Event(), threading.Event() + errors, observations = [], [] + def writer(): + try: + with lock: + conn.execute("BEGIN IMMEDIATE") + entered.set() + if not release.wait(3): + raise AssertionError("Synthetic release race timed out") + conn.commit() + except BaseException as error: + errors.append(error) + entered.set() + finally: + finished.set() + actual_owner_check = lock.owns_other_transaction + def owner_check(connection): + observations.append(connection.in_transaction) + release.set() + if not finished.wait(2): + raise AssertionError("Synthetic owner did not finish") + return actual_owner_check(connection) + with self.api.serving_operation(conn, connection_lock=lock) as operation: + thread = threading.Thread(target=writer) + thread.start() + try: + self.assertTrue(entered.wait(1)) + self.assertEqual(errors, []) + with patch.object(lock, "owns_other_transaction", side_effect=owner_check), \ + patch.object(lock, "acquire", wraps=lock.acquire) as acquire: + if previous: + with self.assertRaisesRegex(self.api.TierRuntimeError, "clean connection"): + previous_guard(operation, lock) + acquire.assert_not_called() + else: + self.api.require_model_load_allowed() + acquire.assert_called_once_with(blocking=False) + self.assertEqual(observations, [True]) + self.assertFalse(conn.in_transaction) + self.assertFalse(lock.locked()) + self.assertIs(engine.conn, conn) + finally: + release.set() + thread.join(3) + self.assertFalse(thread.is_alive()) + self.assertEqual(errors, []) + + def test_model_guard_current_thread_clean_lock_is_not_reacquired(self): + conn = self.memory_connection() + engine, lock = self.writer_lock(conn) + with self.api.serving_operation(conn, connection_lock=lock): + with lock: + with patch.object(lock, "acquire", side_effect=AssertionError("Nonreentrant writer reacquired")): + self.api.require_model_load_allowed() + self.assertTrue(lock.owned_by_current_thread()) + self.assertFalse(conn.in_transaction) + self.assertFalse(lock.locked()) + self.assertIs(engine.conn, conn) + + def test_model_guard_allows_other_managed_writer_without_acquiring_its_lock(self): + conn = self.memory_connection() + engine, lock = self.writer_lock(conn) + entered, release = threading.Event(), threading.Event() + errors = [] + def writer(): + try: + with lock: + conn.execute("BEGIN IMMEDIATE") + conn.execute("INSERT INTO messages(id,content) VALUES(995,'synthetic writer')") + entered.set() + if not release.wait(3): + raise AssertionError("Synthetic writer release timed out") + conn.rollback() + except BaseException as error: + errors.append(error) + entered.set() + with self.api.serving_operation(conn, connection_lock=lock) as operation: + self.assertFalse(operation.borrowed) + self.assertIs(operation._connection_lock, lock) + thread = threading.Thread(target=writer) + thread.start() + try: + self.assertTrue(entered.wait(1)) + self.assertEqual(errors, []) + self.assertTrue(lock.owns_other_transaction(conn)) + with patch.object(lock, "acquire", side_effect=AssertionError("Model guard waited on writer")): + self.api.require_model_load_allowed() + self.assertTrue(conn.in_transaction) + self.assertEqual(conn.execute("SELECT content FROM messages WHERE id=995").fetchone()[0], "synthetic writer") + self.assertIs(engine.conn, conn) + finally: + release.set() + thread.join(3) + self.assertFalse(thread.is_alive()) + self.assertEqual(errors, []) + + def test_model_guard_preserves_initial_borrowed_refusal_after_caller_commit(self): + conn = self.memory_connection() + engine, lock = self.writer_lock(conn) + conn.execute("BEGIN") + with self.api.serving_operation(conn, connection_lock=lock) as operation: + self.assertTrue(operation.borrowed) + with self.assertRaisesRegex(self.api.TierRuntimeError, "clean connection"): + self.api.require_model_load_allowed() + self.assertTrue(conn.in_transaction) + conn.commit() + with self.assertRaisesRegex(self.api.TierRuntimeError, "clean connection"): + self.api.require_model_load_allowed() + self.assertIs(engine.conn, conn) + + def test_model_guard_refuses_current_thread_owned_sql_without_ending_it(self): + conn = self.memory_connection() + engine, lock = self.writer_lock(conn) + with self.api.serving_operation(conn, connection_lock=lock): + with lock: + conn.execute("BEGIN") + try: + with self.assertRaisesRegex(self.api.TierRuntimeError, "clean connection"): + self.api.require_model_load_allowed() + self.assertTrue(conn.in_transaction) + finally: + conn.rollback() + self.assertIs(engine.conn, conn) + + def test_model_guard_refuses_unmanaged_or_duck_typed_transaction_ownership(self): + class UnmanagedLock: + def owns_other_transaction(self, conn): + raise AssertionError("Foreign ownership callback was consulted") + conn = self.memory_connection() + for lock in (None, threading.Lock(), UnmanagedLock()): + with self.subTest(lock=type(lock).__name__), self.api.serving_operation(conn) as operation: + replacement = dataclasses.replace(operation, _connection_lock=lock) + with self.api._bound(replacement): + conn.execute("BEGIN") + try: + with self.assertRaisesRegex(self.api.TierRuntimeError, "clean connection"): + self.api.require_model_load_allowed() + self.assertTrue(conn.in_transaction) + finally: + conn.rollback() + + def test_model_guard_does_not_use_managed_owner_for_another_handle(self): + conn, other = self.memory_connection(), self.memory_connection() + engine, lock = self.writer_lock(other) + entered, release = threading.Event(), threading.Event() + errors = [] + def writer(): + try: + with lock: + other.execute("BEGIN") + entered.set() + if not release.wait(3): + raise AssertionError("Synthetic stale-handle writer release timed out") + other.rollback() + except BaseException as error: + errors.append(error) + entered.set() + with self.api.serving_operation(conn, connection_lock=lock): + thread = threading.Thread(target=writer) + thread.start() + try: + self.assertTrue(entered.wait(1)) + self.assertEqual(errors, []) + self.assertTrue(lock.owns_other_transaction(other)) + self.assertFalse(lock.owns_other_transaction(conn)) + conn.execute("BEGIN") + with self.assertRaisesRegex(self.api.TierRuntimeError, "clean connection"): + self.api.require_model_load_allowed() + self.assertTrue(conn.in_transaction) + conn.rollback() + finally: + release.set() + thread.join(3) + self.assertFalse(thread.is_alive()) + self.assertEqual(errors, []) + self.assertIs(engine.conn, other) + + def test_model_guard_closed_handle_error_is_not_reclassified_or_suppressed(self): + conn = self.memory_connection() + with self.api.serving_operation(conn): + conn.close() + with self.assertRaisesRegex(sqlite3.ProgrammingError, "closed database"): + self.api.require_model_load_allowed() + + def test_runtime_child_carries_lock_only_for_original_handle(self): + conn, other = self.memory_connection(), self.memory_connection() + engine, lock = self.writer_lock(conn) + joined, errors = [], [] + with patch.object(self.api, "_database_identity", return_value=("file", "synthetic-database")): + with self.api.serving_operation(conn, connection_lock=lock) as operation: + same_child, other_child = operation.fork_child(), operation.fork_child() + def join(child, connection, expected_lock): + try: + with child.join(connection) as current: + self.assertIs(current._connection_lock, expected_lock) + self.assertIs(current._connection, connection) + connection.execute("BEGIN") + try: + with self.assertRaisesRegex(self.api.TierRuntimeError, "clean connection"): + self.api.require_model_load_allowed() + finally: + connection.rollback() + joined.append(connection) + except BaseException as error: + errors.append(error) + # Sequential children isolate their SQL while retaining the + # production thread-owned child admission protocol. + for child, connection, expected in ((same_child, conn, lock), (other_child, other, None)): + thread = threading.Thread(target=join, args=(child, connection, expected)) + thread.start() + thread.join(3) + self.assertFalse(thread.is_alive()) + self.assertEqual(errors, []) + self.assertEqual(joined, [conn, other]) + self.assertIs(engine.conn, conn) + + +class TestEngineReconnectReceipt(unittest.TestCase): + setUp = TestPublicBoundaries.setUp + imports = TestPublicBoundaries.imports + namespace = TestPublicBoundaries.namespace + memory_connection = TestPublicBoundaries.memory_connection + real_memory_owner = TestPublicBoundaries.real_memory_owner + + def engine(self, *, vectors=False): + enter_context(self, patch.object(Path, "resolve", _REAL_PATH_RESOLVE)) + self.stat = types.SimpleNamespace(st_dev=7, st_ino=11) + enter_context(self, patch.object(Path, "stat", return_value=self.stat)) + enter_context(self, patch.object(Path, "mkdir")) + enter_context(self, patch.object(Path, "chmod")) + self.modules["storage"] = definitions("storage.py", {"DatabaseOpenError", "_integrity_message", "_validate_db_path"}, + self.namespace(sqlite3=sqlite3, Path=Path, os=os, newest_backup=lambda path: None)) + namespace = self.namespace(sqlite3=sqlite3, Path=Path, logger=logging.getLogger("synthetic-reconnect"), + _HAS_VECTOR=vectors, DEFAULT_BUSY_TIMEOUT_MS=3000, + _ALL_VEC_TABLES=("vec_messages", "vec_messages_sep", "vec_messages_edge", "vec_messages_sep_edge", + "vec_messages_basepro", "vec_messages_sep_basepro"), + create_db=Mock(side_effect=AssertionError("Unexpected schema factory"))) + tree = ast.parse((ROOT / "truememory/engine.py").read_text(encoding="utf-8")) + cls = next(n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == "TrueMemoryEngine") + names = {"_open_connection_handle", "_reconnect_registry", "_capture_reconnect_receipt", + "_reopen_initialized_connection", "close"} + cls.body = [n for n in cls.body if isinstance(n, ast.FunctionDef) and n.name in names] + future = ast.ImportFrom(module="__future__", names=[ast.alias(name="annotations")], level=0) + exec(compile(ast.fix_missing_locations(ast.Module(body=[future, cls], type_ignores=[])), + "actual-engine-reconnect", "exec"), namespace) + engine = namespace["TrueMemoryEngine"]() + engine.conn, engine.db_path = self.conn, Path("/synthetic/reconnect.sqlite").absolute() + engine._init_lock = threading.Lock() + engine._write_lock = self.api.ConnectionWriteLock(engine) + engine.ready = engine._runtime_initialized = True + engine._has_vectors = vectors + engine._runtime_vector_connection = self.conn + engine._runtime_vector_generation = None + engine._reconnect_receipt = None + self.fixture.vector._active_tier_group = lambda: "edge" + self.conn.execute("INSERT OR REPLACE INTO metadata(key,value) VALUES('l4_entity_profile_migration_done','1')") + self.conn.execute("DELETE FROM metadata WHERE key IN ('embed_model','embed_dim')") + if vectors: + self.conn.execute("CREATE TABLE IF NOT EXISTS vec_messages(rowid INTEGER PRIMARY KEY)") + self.conn.execute("CREATE TABLE IF NOT EXISTS vec_messages_sep(rowid INTEGER PRIMARY KEY)") + self.conn.commit() + operation = types.SimpleNamespace(key=self.api._legacy_key(), _database=("file", str(engine.db_path))) + with engine._write_lock: + engine._capture_reconnect_receipt(operation) + self.assertIsNotNone(engine._reconnect_receipt) + self.assertFalse(self.conn.in_transaction) + self.assertEqual(len(engine._reconnect_receipt), 10) + self.engine_namespace = namespace + return engine + + def candidate(self, engine): + candidate = self.memory_connection() + # SQLite backup assigns a new schema cookie; simulate reopening the same file. + schema = self.conn.execute("PRAGMA schema_version").fetchone()[0] + candidate.execute(f"PRAGMA schema_version={schema}") + connect = Mock(return_value=candidate) + proxy = types.SimpleNamespace(**{name: getattr(sqlite3, name) for name in + ("Connection", "ProgrammingError", "OperationalError", "DatabaseError", "Error")}, connect=connect) + self.engine_namespace["sqlite3"] = proxy + self.connect = connect + return candidate + + def forbid_owner(self): + owner = Mock(side_effect=AssertionError("Validated reconnect requested maintenance ownership")) + self.modules["maintenance"].maintenance_owner = owner + return owner + + def assert_closed(self, conn): + with self.assertRaises(sqlite3.ProgrammingError): + conn.execute("SELECT 1") + + def test_extension_unavailable_fts_receipt_preserves_capability_and_source(self): + engine = self.engine() + self.engine_namespace["_HAS_VECTOR"] = True + self.conn.enable_load_extension = None + self.conn.execute("INSERT INTO messages(id,content) VALUES(883,'synthetic FTS source')") + self.conn.commit() + operation = types.SimpleNamespace(key=self.api._legacy_key(), _database=("file", str(engine.db_path))) + engine._capture_reconnect_receipt(operation) + self.assertIsNotNone(engine._reconnect_receipt) + self.assertEqual(engine._reconnect_receipt[7:9], (False, True)) + candidate = self.candidate(engine) + candidate.enable_load_extension = None + owner = self.forbid_owner() + self.conn.close() + engine._open_connection_handle() + self.assertIs(engine.conn, candidate) + self.assertTrue(engine.ready) + self.assertTrue(engine._runtime_initialized) + self.assertFalse(engine._has_vectors) + self.assertFalse(candidate.in_transaction) + self.assertEqual(candidate.execute("SELECT content FROM messages WHERE id=883").fetchone()[0], + "synthetic FTS source") + owner.assert_not_called() + + def test_fts_receipt_cannot_skip_new_extension_capability(self): + engine = self.engine() + self.engine_namespace["_HAS_VECTOR"] = True + self.conn.enable_load_extension = None + operation = types.SimpleNamespace(key=self.api._legacy_key(), _database=("file", str(engine.db_path))) + engine._capture_reconnect_receipt(operation) + receipt = engine._reconnect_receipt + self.assertIsNotNone(receipt) + candidate = self.candidate(engine) + candidate.enable_load_extension = lambda enabled: None + self.assertIsNone(engine._reopen_initialized_connection(engine.db_path)) + self.assert_closed(candidate) + self.assertIs(engine.conn, self.conn) + self.assertIs(engine._reconnect_receipt, receipt) + self.assertFalse(self.conn.in_transaction) + + def test_other_vector_initialization_failure_does_not_publish_fts_receipt(self): + engine = self.engine() + self.engine_namespace["_HAS_VECTOR"] = True + self.conn.enable_load_extension = lambda enabled: None + operation = types.SimpleNamespace(key=self.api._legacy_key(), _database=("file", str(engine.db_path))) + queries = [] + self.conn.set_trace_callback(queries.append) + engine._capture_reconnect_receipt(operation) + self.assertIsNone(engine._reconnect_receipt) + self.assertEqual(queries, []) + self.assertFalse(self.conn.in_transaction) + + def test_closed_initialized_fts_handle_reopens_without_owner_or_schema_writes(self): + engine = self.engine() + self.conn.execute("INSERT INTO messages(id,content) VALUES(881,'synthetic prior source')") + self.conn.commit() + candidate = self.candidate(engine) + queries = [] + candidate.set_trace_callback(queries.append) + owner = self.forbid_owner() + self.conn.close() + engine._open_connection_handle() + self.assertIs(engine.conn, candidate) + self.assertTrue(engine.ready) + self.assertTrue(engine._runtime_initialized) + self.assertFalse(engine._has_vectors) + self.assertFalse(candidate.in_transaction) + self.assertEqual(candidate.execute("SELECT content FROM messages WHERE id=881").fetchone()[0], "synthetic prior source") + self.connect.assert_called_once_with(engine.db_path.as_uri() + "?mode=rw", uri=True, check_same_thread=False) + self.assertIn("PRAGMA quick_check(1)", queries) + self.assertIn("PRAGMA foreign_keys=ON", queries) + self.assertFalse(any(q.lstrip().split(" ", 1)[0].upper() in {"INSERT", "UPDATE", "DELETE", "CREATE", "ALTER", "DROP"} for q in queries)) + owner.assert_not_called() + + def test_persistent_clean_ioerr_retires_old_handle_only_after_validation(self): + engine = self.engine() + candidate = self.candidate(engine) + self.forbid_owner() + self.conn.armed, self.conn.fault_contains = True, "PRAGMA schema_version" + self.conn.fault = sqlite3.OperationalError("disk I/O error") + with patch.object(self.conn, "close", wraps=self.conn.close) as close: + engine._open_connection_handle() + close.assert_called_once_with() + self.assertIs(engine.conn, candidate) + self.assertTrue(engine.ready) + + def test_receipt_does_not_bypass_borrowed_ioerr_or_unrelated_error(self): + engine = self.engine() + self.candidate(engine) + self.forbid_owner() + for borrowed in (False, True): + with self.subTest(borrowed=borrowed): + if borrowed: + self.conn.execute("INSERT INTO messages(id,content) VALUES(882,'synthetic pending')") + error = sqlite3.OperationalError("disk I/O error" if borrowed else "database is locked") + self.conn.armed, self.conn.fault_contains, self.conn.fault = True, "PRAGMA schema_version", error + with self.assertRaises(sqlite3.OperationalError) as raised: + engine._open_connection_handle() + self.assertIs(raised.exception, error) + self.assertEqual(self.conn.in_transaction, borrowed) + self.conn.armed = False + self.connect.assert_not_called() + self.assertEqual(self.conn.execute("SELECT content FROM messages WHERE id=882").fetchone()[0], "synthetic pending") + + def test_new_engine_without_receipt_retains_owner_required_refusal(self): + engine = self.engine() + engine.conn = None + engine._reconnect_receipt = None + owner = self.real_memory_owner(lambda path: None) + self.modules["maintenance"] = owner + self.candidate(engine) + with self.assertRaises(owner.MaintenanceBusyError): + engine._open_connection_handle() + self.assertIsNone(engine.conn) + self.connect.assert_not_called() + + def test_explicit_close_clears_receipt_and_next_open_requires_owner(self): + engine = self.engine() + engine.close() + self.assertIsNone(engine._reconnect_receipt) + owner = self.real_memory_owner(lambda path: None) + self.modules["maintenance"] = owner + with self.assertRaises(owner.MaintenanceBusyError): + engine._open_connection_handle() + self.assertIsNone(engine.conn) + + def test_healthy_ready_handle_has_no_receipt_sql_or_file_stat(self): + engine = self.engine() + self.candidate(engine) + self.forbid_owner() + queries = [] + self.conn.set_trace_callback(queries.append) + with patch.object(Path, "stat", side_effect=AssertionError("Ready path stat")): + engine._open_connection_handle() + self.assertEqual(queries, ["PRAGMA schema_version"]) + self.connect.assert_not_called() + + def test_file_schema_registry_or_embedding_change_refuses_receipt(self): + for change in ("inode", "schema", "registry", "model", "dimension", "partial", "oversized"): + with self.subTest(change=change): + engine = self.engine() + candidate = self.candidate(engine) + if change == "inode": + self.stat.st_ino += 1 + elif change == "schema": + candidate.execute("CREATE TABLE synthetic_new_schema(x)") + elif change == "registry": + candidate.execute("INSERT INTO vector_cache_registry(tier_group,vec_table,sep_table) VALUES('edge','vec_messages_edge','vec_messages_sep_edge')") + else: + candidate.execute("INSERT OR REPLACE INTO metadata(key,value) VALUES('embed_model',?)", + ("different" if change == "model" else "x" * 1024 if change == "oversized" else "model2vec",)) + if change != "partial": + candidate.execute("INSERT OR REPLACE INTO metadata(key,value) VALUES('embed_dim',?)", ("384" if change == "dimension" else "256",)) + candidate.commit() + self.assertIsNone(engine._reopen_initialized_connection(engine.db_path)) + if change == "inode": + self.connect.assert_not_called() + else: + self.assert_closed(candidate) + self.assertIs(engine.conn, self.conn) + self.assertTrue(engine.ready) + + def test_matching_metadata_published_after_initialization_is_accepted(self): + engine = self.engine() + candidate = self.candidate(engine) + candidate.executemany("INSERT INTO metadata(key,value) VALUES(?,?)", [("embed_model", "model2vec"), ("embed_dim", "256")]) + candidate.commit() + replacement = engine._reopen_initialized_connection(engine.db_path) + self.assertEqual(replacement, (candidate, False)) + self.assertFalse(candidate.in_transaction) + + def test_quick_check_failure_closes_candidate_preserves_old_handle_and_ready(self): + for failure in ("disk I/O error", "database disk image is malformed"): + with self.subTest(failure=failure): + engine = self.engine() + candidate = self.candidate(engine) + candidate.armed, candidate.fault_contains = True, "PRAGMA quick_check(1)" + candidate.fault = sqlite3.OperationalError(failure) + self.conn.armed, self.conn.fault_contains = True, "PRAGMA schema_version" + self.conn.fault = sqlite3.OperationalError("disk I/O error") + with patch.object(self.conn, "close", wraps=self.conn.close) as close: + with self.assertRaises(self.modules["storage"].DatabaseOpenError) as raised: + engine._open_connection_handle() + close.assert_not_called() + self.assertIn("Close ALL" if "I/O" in failure else "appears corrupt", str(raised.exception)) + self.assert_closed(candidate) + self.assertIs(engine.conn, self.conn) + self.assertTrue(engine.ready) + self.conn.armed = False + + def test_recheck_uses_healthy_replacement_and_closes_unused_candidate(self): + engine = self.engine() + candidate = self.candidate(engine) + healthy = self.memory_connection() + self.forbid_owner() + self.conn.armed, self.conn.fault_contains = True, "PRAGMA schema_version" + self.conn.fault = sqlite3.OperationalError("disk I/O error") + def connect(*args, **kwargs): + engine.conn = healthy + return candidate + self.connect.side_effect = connect + with patch.object(self.conn, "close", wraps=self.conn.close) as close: + engine._open_connection_handle() + close.assert_not_called() + self.assertIs(engine.conn, healthy) + self.assert_closed(candidate) + + def test_changed_file_after_candidate_validation_is_refused(self): + engine = self.engine() + candidate = self.candidate(engine) + enter_context(self, patch.object(Path, "stat", side_effect=[self.stat, types.SimpleNamespace(st_dev=7, st_ino=12)])) + self.assertIsNone(engine._reopen_initialized_connection(engine.db_path)) + self.assert_closed(candidate) + + def test_vector_metadata_absence_requires_both_tables_empty_and_extension_registration(self): + for table in (None, "vec_messages", "vec_messages_sep"): + with self.subTest(table=table): + engine = self.engine(vectors=True) + candidate = self.candidate(engine) + if table: + candidate.execute(f'INSERT INTO "{table}"(rowid) VALUES(1)') + candidate.commit() + load = Mock() + original_import = self.imports + def imports(name, *args, **kwargs): + if name == "sqlite_vec": + return types.SimpleNamespace(load=load) + return original_import(name, *args, **kwargs) + self.engine_namespace["__builtins__"]["__import__"] = imports + with patch.object(candidate, "enable_load_extension", create=True) as enabled: + result = engine._reopen_initialized_connection(engine.db_path) + load.assert_called_once_with(candidate) + self.assertEqual([c.args for c in enabled.call_args_list], [(True,), (False,)]) + if table is None: + self.assertEqual(result, (candidate, False)) + else: + self.assertIsNone(result) + self.assert_closed(candidate) + + + + def test_policy_supersedes_legacy_ready_flags_without_legacy_metadata_reuse(self): + engine = self.engine() + policy = TestPublicBoundaries.seed_policy(self) + candidate = self.candidate(engine) + self.forbid_owner() + self.conn.close() + engine._open_connection_handle() + self.assertIs(engine.conn, candidate) + self.assertFalse(engine.ready) + self.assertFalse(engine._runtime_initialized) + self.assertFalse(engine._has_vectors) + self.assertIsNone(engine._reconnect_receipt) + self.assertIsNone(engine._runtime_vector_connection) + self.assertEqual(self.api._read_policy(candidate), policy) + self.assertEqual(self.fixture.loads, []) + + def test_registry_progress_mutation_invalidates_same_schema_receipt(self): + engine = self.engine() + self.conn.execute("INSERT INTO vector_cache_registry(tier_group,vec_table,sep_table) VALUES('edge','vec_messages_edge','vec_messages_sep_edge')") + self.conn.commit() + operation = types.SimpleNamespace(key=self.api._legacy_key(), _database=("file", str(engine.db_path))) + with engine._write_lock: + engine._capture_reconnect_receipt(operation) + candidate = self.candidate(engine) + candidate.execute("UPDATE vector_cache_registry SET vector_count=1 WHERE tier_group='edge'") + candidate.commit() + self.assertEqual(candidate.execute("PRAGMA schema_version").fetchone()[0], engine._reconnect_receipt[3]) + self.assertIsNone(engine._reopen_initialized_connection(engine.db_path)) + self.assert_closed(candidate) + + def test_registry_record_bounds_and_pending_caller_sql_prevent_capture(self): + engine = self.engine() + operation = types.SimpleNamespace(key=self.api._legacy_key(), _database=("file", str(engine.db_path))) + self.conn.execute("INSERT INTO vector_cache_registry(tier_group,vec_table,sep_table,model_name) VALUES('edge','vec_messages_edge','vec_messages_sep_edge',?)", ("x" * 513,)) + self.conn.commit() + with engine._write_lock: + engine._capture_reconnect_receipt(operation) + self.assertIsNone(engine._reconnect_receipt) + self.conn.execute("INSERT INTO messages(id,content) VALUES(883,'synthetic caller')") + before = self.conn.commits, self.conn.rollbacks + with engine._write_lock: + engine._capture_reconnect_receipt(operation) + self.assertIsNone(engine._reconnect_receipt) + self.assertTrue(self.conn.in_transaction) + self.assertEqual((self.conn.commits, self.conn.rollbacks), before) + + def test_bad_integrity_result_and_late_stat_failure_close_only_candidate(self): + for fault in ("quick_check", "stat"): + with self.subTest(fault=fault): + engine = self.engine() + candidate = self.candidate(engine) + execute = candidate.execute + def checked(sql, *args, **kwargs): + if sql == "PRAGMA quick_check(1)": + return types.SimpleNamespace(fetchone=lambda: ("synthetic corruption",)) + return execute(sql, *args, **kwargs) + with contextlib.ExitStack() as stack: + if fault == "quick_check": + stack.enter_context(patch.object(candidate, "execute", side_effect=checked)) + expected = self.modules["storage"].DatabaseOpenError + else: + stack.enter_context(patch.object(Path, "stat", side_effect=[self.stat, OSError("synthetic unavailable file")])) + expected = OSError + with self.assertRaises(expected): + engine._reopen_initialized_connection(engine.db_path) + self.assert_closed(candidate) + self.assertIs(engine.conn, self.conn) + self.assertTrue(engine.ready) + self.assertFalse(self.conn.in_transaction) + + def test_deprecated_open_legacy_callback_is_existing_only_without_migrations(self): + engine = self.engine() + candidate = self.candidate(engine) + queries = [] + candidate.set_trace_callback(queries.append) + module = definitions("engine.py", {"open"}, self.namespace(Path=Path, + sqlite3=self.engine_namespace["sqlite3"], DEFAULT_BUSY_TIMEOUT_MS=3000, + warnings=types.SimpleNamespace(warn=lambda *args, **kwargs: None)), methods=True) + class Completed(Exception): + pass + def opener(path, callback): + self.assertIs(callback(engine.db_path), candidate) + raise Completed() + with patch.object(self.api, "open_serving_connection", side_effect=opener), patch.object(Path, "exists", return_value=True): + with self.assertRaises(Completed): + module.open(engine) + self.connect.assert_called_once_with(engine.db_path.as_uri() + "?mode=rw", uri=True, check_same_thread=False) + self.assertEqual(queries, ["PRAGMA journal_mode=WAL", "PRAGMA busy_timeout=3000", "PRAGMA foreign_keys=ON", + "PRAGMA synchronous=NORMAL", "PRAGMA cache_size=-64000", "PRAGMA mmap_size=268435456"]) + self.assertIs(engine.conn, self.conn) + self.assertIsNone(candidate.row_factory) + + def test_deprecated_open_legacy_callback_failure_closes_only_candidate(self): + engine = self.engine() + candidate = self.candidate(engine) + candidate.armed, candidate.fault_contains = True, "PRAGMA foreign_keys=ON" + candidate.fault = sqlite3.OperationalError("synthetic pragma failure") + module = definitions("engine.py", {"open"}, self.namespace(Path=Path, + sqlite3=self.engine_namespace["sqlite3"], DEFAULT_BUSY_TIMEOUT_MS=3000, + warnings=types.SimpleNamespace(warn=lambda *args, **kwargs: None)), methods=True) + def opener(path, callback): + return callback(engine.db_path) + with patch.object(self.api, "open_serving_connection", side_effect=opener), patch.object(Path, "exists", return_value=True): + with self.assertRaises(sqlite3.OperationalError): + module.open(engine) + self.assert_closed(candidate) + self.assertIs(engine.conn, self.conn) + self.assertTrue(engine.ready) + + + + def test_nul_tail_metadata_and_registry_are_rejected_before_materialization(self): + engine = self.engine() + candidate = self.candidate(engine) + candidate.executemany("INSERT INTO metadata(key,value) VALUES(?,?)", [ + ("embed_model", "model2vec\0" + "x" * 10000), ("embed_dim", "256")]) + candidate.commit() + self.assertIsNone(engine._reopen_initialized_connection(engine.db_path)) + self.assert_closed(candidate) + self.conn.execute("INSERT INTO vector_cache_registry(tier_group,vec_table,sep_table,model_name) VALUES('edge','vec_messages_edge','vec_messages_sep_edge',?)", + ("synthetic\0" + "x" * 10000,)) + self.conn.commit() + statements = [] + self.conn.set_trace_callback(statements.append) + self.assertIsNone(engine._reconnect_registry(self.conn)) + self.assertFalse(any(sql.startswith("SELECT tier_group,vec_table") for sql in statements)) + + def test_capture_cleanup_failure_never_publishes_receipt(self): + engine = self.engine() + operation = types.SimpleNamespace(key=self.api._legacy_key(), _database=("file", str(engine.db_path))) + with engine._write_lock, patch.object(self.conn, "rollback", side_effect=sqlite3.OperationalError("synthetic rollback failure")): + with self.assertRaises(sqlite3.OperationalError): + engine._capture_reconnect_receipt(operation) + self.assertIsNone(engine._reconnect_receipt) + self.assertTrue(self.conn.in_transaction) + self.assertIs(engine.conn, self.conn) + self.conn.rollback() + + def test_registry_rows_are_copied_to_primitive_tuples(self): + engine = self.engine() + self.conn.execute("INSERT INTO vector_cache_registry(tier_group,vec_table,sep_table) VALUES('edge','vec_messages_edge','vec_messages_sep_edge')") + self.conn.commit() + self.conn.row_factory = sqlite3.Row + operation = types.SimpleNamespace(key=self.api._legacy_key(), _database=("file", str(engine.db_path))) + with engine._write_lock: + engine._capture_reconnect_receipt(operation) + receipt = engine._reconnect_receipt + self.assertIsNotNone(receipt) + self.assertIs(type(receipt[6]), tuple) + self.assertIs(type(receipt[6][0]), tuple) + self.assertTrue(all(type(value) in (str, int, float, type(None)) for value in receipt[6][0])) + + + + def test_silent_capture_rollback_cannot_publish_receipt(self): + engine = self.engine() + operation = types.SimpleNamespace(key=self.api._legacy_key(), _database=("file", str(engine.db_path))) + with engine._write_lock, patch.object(self.conn, "rollback", return_value=None) as rollback: + with self.assertRaises(self.api.TierRuntimeError): + engine._capture_reconnect_receipt(operation) + rollback.assert_called_once_with() + self.assertIsNone(engine._reconnect_receipt) + self.assertTrue(self.conn.in_transaction) + self.assertIs(engine.conn, self.conn) + self.conn.rollback() + + def test_silent_candidate_rollback_closes_only_candidate(self): + engine = self.engine() + candidate = self.candidate(engine) + self.conn.armed, self.conn.fault_contains = True, "PRAGMA schema_version" + self.conn.fault = sqlite3.OperationalError("disk I/O error") + before = engine._reconnect_receipt + with patch.object(candidate, "rollback", return_value=None) as rollback, \ + patch.object(self.conn, "close", wraps=self.conn.close) as close: + with self.assertRaises(self.api.TierRuntimeError): + engine._open_connection_handle() + rollback.assert_called_once_with() + close.assert_not_called() + self.assert_closed(candidate) + self.assertIs(engine.conn, self.conn) + self.assertEqual(engine._reconnect_receipt, before) + self.assertTrue(engine.ready) + self.assertTrue(engine._runtime_initialized) + self.assertFalse(self.conn.in_transaction) + + + +class TestColdPublicInitialization(unittest.TestCase): + imports = TestPublicBoundaries.imports + namespace = TestPublicBoundaries.namespace + + def setUp(self): + TestPublicBoundaries.setUp(self) + import re + self.events = [] + self.conn.enable_load_extension = lambda value: self.events.append(("extension", value)) + native = types.SimpleNamespace(load=lambda conn: self.events.append(("load", conn))) + original_import = self.imports + def imports(name, globals=None, locals=None, fromlist=(), level=0): + if name == "sqlite_vec": + return native + return original_import(name, globals, locals, fromlist, level) + self.imports = imports + original_execute = self.conn.execute + def execute(sql, parameters=()): + match = re.fullmatch(r"CREATE VIRTUAL TABLE IF NOT EXISTS (\w+) USING vec0\(embedding float\[(\d+)\] distance_metric=cosine\)", sql) + if match: + sql = f'CREATE TABLE IF NOT EXISTS {match[1]} (rowid INTEGER PRIMARY KEY, embedding "float[{match[2]}] distance_metric=cosine")' + return original_execute(sql, parameters) + self.conn.execute = execute + known = ("vec_messages", "vec_messages_sep", "vec_messages_edge", "vec_messages_sep_edge", + "vec_messages_basepro", "vec_messages_sep_basepro") + for table in (*known, "vec_messages_custom", "vec_messages_sep_custom"): + self.conn.execute(f'DROP TABLE IF EXISTS "{table}"') + self.conn.commit() + self.vector = definitions("vector_search.py", { + "_active_vec_table", "_active_sep_table", "_ensure_metadata_table", "_read_embedder_metadata", + "_detect_existing_vec_dim", "_migration_hint", "_check_embedder_compatibility", "_table_uses_cosine", + "_vec0_column_decl", "init_vec_table", "migrate_to_cosine_metric", "migrate_legacy_vec_tables", + }, self.namespace(sqlite3=sqlite3, re=re, EMBEDDING_MODEL="model2vec", _embedding_dim=256, + _KNOWN_VEC_TABLES=known, _VALID_GROUPS={"edge", "basepro"}, _MODEL_TO_GROUP={"model2vec": "edge"}, + _VEC_DISTANCE_METRIC="cosine", _active_tier_group=lambda: "edge", + logger=logging.getLogger("synthetic-initialization"), TrueMemoryMigrationError=RuntimeError)) + self.fixture.vector._active_vec_table = self.vector._active_vec_table + self.fixture.vector._active_sep_table = self.vector._active_sep_table + self.modules["vector_search"] = self.vector + self.vector.search_vector_raw = lambda conn, query, limit: conn.execute( + f'SELECT rowid, embedding FROM "{self.vector._active_vec_table(conn)}" LIMIT ?', (limit,)).fetchall() + self.namespace_values = self.namespace(_HAS_VECTOR=True, _HAS_HYBRID=True, + init_vec_table=self.vector.init_vec_table, TrueMemoryMigrationError=RuntimeError, + resolve_tier=lambda: "edge", logger=logging.getLogger("synthetic-initialization"), + engine_operation=self.api.engine_operation) + self.methods = definitions("engine.py", {"_ensure_connection", "_initialize_connection", "_maybe_auto_consolidate", + "_maybe_startup_consolidate", "search_vectors_raw"}, self.namespace_values, methods=True) + self.engine = types.SimpleNamespace(conn=self.conn, ready=False, _runtime_initialized=False, + _has_vectors=False, _has_hybrid=False, _has_consolidation=False, _has_style_vec=False, + _init_lock=threading.Lock(), _write_lock=threading.Lock(), _open_connection_handle=lambda: None, + _purge_legacy_entity_profile_summaries=Mock(), _reconnect_receipt=None) + for name in ("_ensure_connection", "_initialize_connection", "_maybe_auto_consolidate", + "_maybe_startup_consolidate", "search_vectors_raw"): + setattr(self.engine, name, types.MethodType(getattr(self.methods, name), self.engine)) + self.conn.execute("INSERT OR REPLACE INTO metadata(key,value) VALUES('embed_model','model2vec')") + self.conn.execute("INSERT OR REPLACE INTO metadata(key,value) VALUES('embed_dim','256')") + self.conn.commit() + + def generic(self, *, populated=True): + for table in ("vec_messages", "vec_messages_sep"): + self.conn.execute(f'CREATE TABLE {table}(rowid INTEGER PRIMARY KEY, embedding "float[256] distance_metric=cosine")') + if populated: + self.conn.execute(f"INSERT INTO {table}(rowid,embedding) VALUES(7,?)", (b"synthetic vector",)) + self.conn.commit() + + def test_public_search_admits_post_migration_pair(self): + self.generic() + captured = [] + actual = self.vector.search_vector_raw + def search(*args, **kwargs): + captured.append(self.api.current_operation(self.conn)) + return actual(*args, **kwargs) + self.vector.search_vector_raw = search + rows = self.engine.search_vectors_raw("synthetic", limit=3) + self.assertEqual(rows, [(7, b"synthetic vector")]) + self.assertEqual(captured[0].tables, ("vec_messages_edge", "vec_messages_sep_edge")) + self.assertTrue(captured[0]._defer_maintenance) + self.assertIsNone(self.conn.execute("SELECT 1 FROM sqlite_master WHERE name='vec_messages'").fetchone()) + self.assertTrue(self.engine.ready) + self.assertIsNone(self.api.current_operation(self.conn)) + + def test_fact_scope_is_after_migration_and_before_storage(self): + self.generic() + with self.api.fact_operation(types.SimpleNamespace(_engine=self.engine)) as operation: + self.assertEqual(operation.tables, ("vec_messages_edge", "vec_messages_sep_edge")) + self.assertEqual(self.engine.search_vectors_raw("synthetic"), [(7, b"synthetic vector")]) + self.assertIs(self.api.current_operation(self.conn), operation) + + def test_nested_populated_generic_refuses_before_mutation(self): + self.generic() + queries = [] + self.conn.set_trace_callback(queries.append) + with self.api.serving_operation(self.conn) as operation: + with self.assertRaisesRegex(self.api.TierRuntimeError, "before entering"): + self.engine._ensure_connection() + self.assertIs(self.api.current_operation(self.conn), operation) + self.assertEqual(operation.tables, ("vec_messages", "vec_messages_sep")) + self.assertFalse(any(sql.lstrip().split()[0].upper() in {"CREATE", "DROP", "ALTER", "INSERT", "DELETE", "UPDATE"} for sql in queries)) + self.assertFalse(self.engine.ready) + self.assertEqual(self.conn.execute("SELECT embedding FROM vec_messages").fetchall(), [(b"synthetic vector",)]) + + def test_nested_first_empty_generic_can_initialize(self): + with self.api.serving_operation(self.conn) as operation: + self.engine._ensure_connection(_suppress_maintenance=True) + self.assertIs(self.api.current_operation(self.conn), operation) + self.assertEqual(operation.tables, ("vec_messages", "vec_messages_sep")) + self.assertTrue(self.engine.ready) + self.assertEqual(self.conn.execute("SELECT COUNT(*) FROM vec_messages").fetchone(), (0,)) + + def test_nested_absent_generic_preserves_unrelated_cached_pair(self): + self.conn.execute('CREATE TABLE vec_messages_custom(embedding BLOB)') + self.conn.execute('CREATE TABLE vec_messages_sep_custom(embedding BLOB)') + self.conn.execute("INSERT INTO vec_messages_custom VALUES(?)", (b"inactive synthetic",)) + self.conn.commit() + with self.api.serving_operation(self.conn) as operation: + self.engine._ensure_connection(_suppress_maintenance=True) + self.assertEqual(operation.tables, ("vec_messages", "vec_messages_sep")) + self.assertEqual(self.conn.execute("SELECT embedding FROM vec_messages_custom").fetchall(), [(b"inactive synthetic",)]) + self.assertTrue(self.engine.ready) + self.assertTrue(self.engine._has_vectors) + + def test_nested_stable_tiered_pair_only_loads_connection_extension(self): + self.generic() + self.engine._ensure_connection(_suppress_maintenance=True) + self.engine.ready = self.engine._runtime_initialized = False + queries = [] + self.conn.set_trace_callback(queries.append) + with self.api.serving_operation(self.conn) as operation: + self.engine._ensure_connection(_suppress_maintenance=True) + self.assertEqual(operation.tables, ("vec_messages_edge", "vec_messages_sep_edge")) + self.assertTrue(self.engine.ready) + self.assertFalse(any(sql.lstrip().split()[0].upper() in {"CREATE", "DROP", "ALTER", "INSERT", "DELETE", "UPDATE"} for sql in queries)) + + def test_maintenance_waits_for_outer_scope_and_nested_crud_calls(self): + self.generic() + scheduled = [] + self.engine._has_consolidation = True + self.modules["maintenance"].engine_layer_specs = lambda conn, coordinator: () + self.modules["maintenance"].observe_style = Mock(side_effect=AssertionError("Unexpected style")) + self.modules["maintenance"].plan_layers = lambda *args, **kwargs: scheduled.append(self.api.current_runtime_operation()) or () + self.engine._get_maintenance_coordinator = lambda: object() + self.engine._auto_consolidate_threshold = 32 + with self.api.engine_serving_operation(self.engine) as operation: + self.engine._maybe_auto_consolidate() + self.engine._ensure_connection() + with self.api.engine_serving_operation(self.engine) as nested: + self.assertIs(nested, operation) + self.engine._maybe_auto_consolidate() + self.assertEqual(scheduled, []) + self.assertEqual(scheduled, [None]) + + def test_borrowed_cold_initialization_preserves_caller_sql(self): + self.conn.execute("INSERT INTO messages(id,content) VALUES(91,'synthetic caller')") + before = (self.conn.commits, self.conn.rollbacks) + with self.assertRaisesRegex(self.api.TierRuntimeError, "clean connection"): + self.engine._ensure_connection() + self.assertTrue(self.conn.in_transaction) + self.assertEqual((self.conn.commits, self.conn.rollbacks), before) + self.assertFalse(self.engine.ready) + + def test_nested_custom_pair_preserves_tables_and_metadata(self): + self.generic() + self.engine._ensure_connection(_suppress_maintenance=True) + self.conn.execute("ALTER TABLE vec_messages_edge RENAME TO vec_messages_custom") + self.conn.execute("ALTER TABLE vec_messages_sep_edge RENAME TO vec_messages_sep_custom") + self.conn.execute("UPDATE vector_cache_registry SET vec_table='vec_messages_custom', sep_table='vec_messages_sep_custom'") + self.conn.commit() + self.engine.ready = self.engine._runtime_initialized = False + before = self.conn.total_changes + with self.api.serving_operation(self.conn) as operation: + self.engine._ensure_connection(_suppress_maintenance=True) + self.assertEqual(operation.tables, ("vec_messages_custom", "vec_messages_sep_custom")) + self.assertEqual(self.conn.total_changes, before) + self.assertTrue(self.engine.ready) + + def test_nested_cosine_migration_refuses_without_changing_source(self): + self.conn.execute('CREATE TABLE vec_messages(embedding "float[256]")') + self.conn.execute('CREATE TABLE vec_messages_sep(embedding "float[256]")') + with self.api.serving_operation(self.conn) as operation: + before = self.conn.execute("PRAGMA schema_version").fetchone() + with self.assertRaisesRegex(self.api.TierRuntimeError, "completed vector migration"): + self.engine._ensure_connection() + self.assertIs(self.api.current_operation(self.conn), operation) + self.assertEqual(self.conn.execute("PRAGMA schema_version").fetchone(), before) + self.assertFalse(self.engine.ready) + + def test_child_inherits_deferral_and_cannot_schedule_before_parent_exit(self): + self.generic() + scheduled, failures = [], [] + self.engine._has_consolidation = True + self.modules["maintenance"].engine_layer_specs = lambda conn, coordinator: () + self.modules["maintenance"].observe_style = Mock(side_effect=AssertionError("Unexpected style")) + self.modules["maintenance"].plan_layers = lambda *args, **kwargs: scheduled.append(self.api.current_runtime_operation()) or () + self.engine._get_maintenance_coordinator = lambda: object() + self.engine._auto_consolidate_threshold = 32 + with self.api.engine_serving_operation(self.engine) as operation: + child = operation.fork_child() + def run(): + try: + with child.join() as inherited: + self.assertTrue(inherited._defer_maintenance) + self.assertEqual(inherited.tables, operation.tables) + self.engine._maybe_auto_consolidate() + except BaseException as error: + failures.append(error) + worker = threading.Thread(target=run) + worker.start() + worker.join(2) + self.assertFalse(worker.is_alive()) + self.assertEqual(failures, []) + self.assertEqual(scheduled, []) + self.assertEqual(scheduled, [None]) + + def test_synchronous_nan_repair_precedes_legacy_name_migration(self): + self.generic() + self.engine.db_path = Path(":memory:") + self.vector.EMBEDDING_MODEL = self.fixture.vector.EMBEDDING_MODEL = "qwen3_256" + self.vector._MODEL_TO_GROUP["qwen3_256"] = "basepro" + self.vector._active_tier_group = lambda: "basepro" + self.fixture.vector.resolve_tier = self.methods.resolve_tier = lambda: "base" + self.conn.execute("UPDATE metadata SET value='qwen3_256' WHERE key='embed_model'") + self.conn.execute("INSERT INTO messages(id,content) VALUES(7,'synthetic repair source')") + self.conn.commit() + repaired = [] + def rebuild(conn, *, separation=False): + table = self.vector._active_sep_table(conn) if separation else self.vector._active_vec_table(conn) + repaired.append(table) + conn.execute(f'DELETE FROM "{table}"') + conn.execute(f'INSERT INTO "{table}"(rowid,embedding) VALUES(7,?)', (b"synthetic repaired",)) + conn.commit() + self.vector.build_vectors = rebuild + self.vector.build_separation_vectors = lambda conn: rebuild(conn, separation=True) + with patch.object(sys, "platform", "darwin"): + with self.api.engine_serving_operation(self.engine) as operation: + self.assertEqual(operation.tables, ("vec_messages_basepro", "vec_messages_sep_basepro")) + self.assertEqual(self.conn.execute("SELECT embedding FROM vec_messages_basepro").fetchall(), [(b"synthetic repaired",)]) + self.assertEqual(self.conn.execute("SELECT embedding FROM vec_messages_sep_basepro").fetchall(), [(b"synthetic repaired",)]) + self.assertEqual(repaired, ["vec_messages", "vec_messages_sep"]) + self.assertEqual(self.conn.execute("SELECT value FROM metadata WHERE key='qwen3_nan_fix_applied'").fetchone(), ("1",)) + self.assertIsNone(self.conn.execute("SELECT 1 FROM sqlite_master WHERE name='vec_messages'").fetchone()) + + +class TestBorrowedPublicCompatibility(unittest.TestCase): + imports = TestPublicBoundaries.imports + namespace = TestPublicBoundaries.namespace + + def setUp(self): + TestPublicBoundaries.setUp(self) + storage = self.modules["storage"] + self.conn = storage.create_db(":memory:") + self.addCleanup(self.conn.close) + self.storage = storage + fts = definitions("fts_search.py", {"search_fts", "_build_safe_query", "_fts_select", + "_rows_to_results", "_normalize_scores"}, self.namespace(sqlite3=sqlite3, + _deserialize_metadata=storage._deserialize_metadata, directive_filter_sql=storage.directive_filter_sql, + select_message_cols=storage.select_message_cols)) + source = ast.parse((ROOT / "truememory/engine.py").read_text(encoding="utf-8")) + cap = next(node.value for node in source.body if isinstance(node, ast.Assign) + and any(isinstance(target, ast.Name) and target.id == "MAX_CONTENT_LENGTH" for target in node.targets)) + self.limit = ast.literal_eval(cap) + validators = definitions("engine.py", {"_validate_add_content", "_validate_delete_user"}, + self.namespace(MAX_CONTENT_LENGTH=self.limit)) + self.methods = definitions("engine.py", {"_open_connection_handle", "_initialize_connection", + "add", "delete_all", "search_simple"}, self.namespace( + _validate_add_content=validators._validate_add_content, _validate_delete_user=validators._validate_delete_user, + engine_operation=self.api.engine_operation, engine_handle_operation=self.api.engine_handle_operation, + search_fts=fts.search_fts, sqlite3=sqlite3, _HAS_VECTOR=False, _HAS_HYBRID=False), methods=True) + self.engine = types.SimpleNamespace(conn=self.conn, ready=False, _runtime_initialized=False, + _write_lock=threading.Lock(), _init_lock=threading.Lock(), _has_vectors=False, + _has_consolidation=False, _has_style_vec=False, + _ensure_connection=Mock(side_effect=AssertionError("Unexpected body initialization")), + _maybe_auto_consolidate=Mock(side_effect=AssertionError("Unexpected maintenance")), + _maybe_startup_consolidate=Mock(side_effect=AssertionError("Unexpected startup maintenance"))) + for name in ("_open_connection_handle", "_initialize_connection", "add", "delete_all", "search_simple"): + setattr(self.engine, name, types.MethodType(getattr(self.methods, name), self.engine)) + + def borrow(self): + self.conn.execute("BEGIN") + identity = self.storage.insert_message(self.conn, {"content": "syntheticpending borrowed lexical source"}) + self.assertTrue(self.conn.in_transaction) + self.queries = [] + self.conn.set_trace_callback(self.queries.append) + return identity + + def assert_borrowed(self, identity): + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.conn.execute("SELECT content FROM messages WHERE id=?", (identity,)).fetchone(), + ("syntheticpending borrowed lexical source",)) + self.assertFalse(any(query.lstrip().split()[0].upper() in {"BEGIN", "COMMIT", "ROLLBACK", "SAVEPOINT", "RELEASE"} + for query in self.queries)) + self.assertFalse(self.engine.ready) + self.assertFalse(self.engine._runtime_initialized) + self.engine._maybe_auto_consolidate.assert_not_called() + self.engine._maybe_startup_consolidate.assert_not_called() + + def test_simple_search_reads_borrowed_fts_without_vector_initialization(self): + identity = self.borrow() + with patch.object(self.engine, "_initialize_connection", side_effect=AssertionError("Unexpected vector initialization")) as initialize: + results = self.engine.search_simple("syntheticpending") + self.assertEqual([(row["id"], row["source"]) for row in results], [(identity, "fts")]) + self.assertEqual(results[0]["score"], 1.0) + initialize.assert_not_called() + self.assert_borrowed(identity) + + def test_simple_search_clean_cold_engine_stays_fts_only(self): + identity = self.storage.insert_message(self.conn, {"content": "synthetic lexical only"}) + self.conn.commit() + with patch.object(self.engine, "_initialize_connection", side_effect=AssertionError("Unexpected vector initialization")) as initialize: + results = self.engine.search_simple("synthetic") + self.assertEqual([(row["id"], row["source"]) for row in results], [(identity, "fts")]) + initialize.assert_not_called() + self.assertFalse(self.engine.ready) + self.engine._maybe_auto_consolidate.assert_not_called() + self.engine._maybe_startup_consolidate.assert_not_called() + + def test_simple_search_propagates_typed_refusal_without_ending_caller_sql(self): + identity = self.borrow() + error = self.api.TierRuntimeError("synthetic admission changed") + self.methods.search_fts = Mock(side_effect=error) + with self.assertRaises(self.api.TierRuntimeError) as raised: + self.engine.search_simple("syntheticpending") + self.assertIs(raised.exception, error) + self.assert_borrowed(identity) + + def test_invalid_add_precedes_any_handle_or_sql_admission(self): + identity = self.borrow() + with patch.object(self.engine, "_open_connection_handle", side_effect=AssertionError("Unexpected handle admission")) as opening: + with self.assertRaises(TypeError) as raised: + self.engine.add(None) + self.assertEqual(str(raised.exception), "content must be a string, got NoneType") + self.assertEqual(self.queries, []) + opening.assert_not_called() + self.assert_borrowed(identity) + + def test_oversized_add_keeps_exact_limit_error_and_caller_sql(self): + identity = self.borrow() + with patch.object(self.engine, "_open_connection_handle", side_effect=AssertionError("Unexpected handle admission")) as opening: + with self.assertRaises(ValueError) as raised: + self.engine.add(content="x" * (self.limit + 1)) + self.assertEqual(str(raised.exception), f"Content too large ({self.limit + 1} chars). Maximum is {self.limit}.") + self.assertEqual(self.queries, []) + opening.assert_not_called() + self.assert_borrowed(identity) + + def test_valid_add_arguments_still_refuse_cold_vector_initialization_in_caller_sql(self): + identity = self.borrow() + for content in ("", "synthetic", "x" * self.limit): + with self.subTest(size=len(content)), self.assertRaisesRegex(self.api.TierRuntimeError, "clean connection"): + self.engine.add(content) + self.assert_borrowed(identity) + + def test_invalid_delete_scope_precedes_handle_or_sql_admission(self): + identity = self.borrow() + for value, error_type, message in ((3, TypeError, "user_id must be a string or None, got int"), + ([], TypeError, "user_id must be a string or None, got list"), + ("", ValueError, "user_id cannot be an empty string"), + (" \t", ValueError, "user_id cannot be an empty string")): + with self.subTest(value=value), patch.object(self.engine, "_open_connection_handle", side_effect=AssertionError("Unexpected handle admission")) as opening: + with self.assertRaises(error_type) as raised: + self.engine.delete_all(user_id=value) + self.assertEqual(str(raised.exception), message) + opening.assert_not_called() + self.assertEqual(self.queries, []) + self.assert_borrowed(identity) + + def test_valid_delete_scope_still_requires_clean_cold_initialization(self): + identity = self.borrow() + for value in (None, "synthetic"): + with self.subTest(value=value), self.assertRaisesRegex(self.api.TierRuntimeError, "clean connection"): + self.engine.delete_all(user_id=value) + self.assertFalse(any(query.lstrip().upper().startswith("DELETE") for query in self.queries)) + self.assert_borrowed(identity) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-tier-rebuild-atomicity-752.py b/tests/test-tier-rebuild-atomicity-752.py new file mode 100644 index 00000000..69d1d260 --- /dev/null +++ b/tests/test-tier-rebuild-atomicity-752.py @@ -0,0 +1,454 @@ +"""Paired tier-rebuild durability with synthetic models and real SQLite/vec0.""" +from __future__ import annotations + +import ast +from contextlib import nullcontext +from pathlib import Path +import sqlite3 +import struct +import sys +import tempfile +import types +import unittest +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] +VEC = "vec_messages_edge" +SEP = "vec_messages_sep_edge" + + +def load_worker() -> tuple[types.ModuleType, types.ModuleType]: + modules = [] + for name in ("cache", "worker"): + path = ROOT / "truememory/tier_switch" / (name + ".py") + tree = ast.parse(path.read_text(), filename=str(path)) + tree.body = [node for node in tree.body if not ( + isinstance(node, ast.ImportFrom) and (node.module or "").startswith("truememory.") + ) and not (isinstance(node, ast.Import) and any(item.name == "psutil" for item in node.names))] + module = types.ModuleType("synthetic_rebuild_" + name) + if name == "worker": + module.VectorCacheRegistry = modules[0].VectorCacheRegistry + module.DynamicThrottler = FakeThrottler + exec(compile(tree, str(path), "exec"), module.__dict__) + modules.append(module) + return modules[1], modules[0] + + +class FakeThrottler: + batch_size = 2 + + def __init__(self) -> None: + self.ooms = 0 + self.completed = [] + + def before_batch(self) -> tuple[int, dict]: + return self.batch_size, {} + + def after_batch(self, count: int, duration: float) -> None: + self.completed.append(count) + + def on_oom(self) -> None: + self.ooms += 1 + if self.ooms > 1: + raise AssertionError("synthetic OOM retry did not recover") + self.batch_size = 1 + + def should_flush_cache(self) -> bool: + return False + + @staticmethod + def flush_gpu_cache() -> None: + pass + + def get_eta_seconds(self, remaining: int) -> float: + return float(remaining) + + def get_throughput(self) -> float: + return 1.0 + + +def serialize(vector: list[float]) -> bytes: + return struct.pack("256f", *vector) + + +class WorkerAtomicity(unittest.TestCase): + use_vec = False + + def setUp(self) -> None: + directory = tempfile.TemporaryDirectory(prefix="synthetic-tier-752-") + self.addCleanup(directory.cleanup) + self.path = Path(directory.name) / "synthetic.sqlite" + self.conn = self.open_connection() + self.conn.execute("PRAGMA journal_mode=WAL") + self.conn.executescript(""" + CREATE TABLE notes(value TEXT); + CREATE TABLE vector_cache_registry ( + tier_group TEXT PRIMARY KEY, last_embedded_id INTEGER, + vector_count INTEGER, last_updated REAL + ); + INSERT INTO vector_cache_registry VALUES ('edge',0,0,0); + CREATE TABLE rebuild_status ( + id INTEGER PRIMARY KEY, status TEXT, processed_messages INTEGER, + progress_pct REAL, eta_seconds REAL, batch_size INTEGER, + throughput_ips REAL, ram_pct REAL, last_heartbeat REAL, + completed_at REAL, error TEXT + ); + INSERT INTO rebuild_status(id,status,processed_messages) VALUES (1,'running',0); + """) + for table in (VEC, SEP): + if self.use_vec: + self.conn.execute(f"CREATE VIRTUAL TABLE {table} USING vec0(embedding float[256] distance_metric=cosine)") + else: + self.conn.execute(f"CREATE TABLE {table}(rowid INTEGER PRIMARY KEY,embedding BLOB)") + self.conn.commit() + self.module, self.cache = load_worker() + self.throttler = FakeThrottler() + self.worker = self.module.RebuildWorker(self.conn, "edge", "edge", self.throttler, status_id=1) + self.messages = [{"id": index, "content": f"synthetic record {index}", + "sender": "synthetic sender", "recipient": "synthetic recipient", + "timestamp": "2026-01-01"} for index in range(1, 5)] + self.calls = [] + self.encode_hook = None + self.serialize_hook = None + + def encode(texts: list[str], **kwargs: object) -> list[list[float]]: + self.calls.append({"texts": list(texts), "in_transaction": self.conn.in_transaction}) + if self.encode_hook is not None: + result = self.encode_hook(len(self.calls), texts) + if result is not None: + return result + return [[float(index + 1)] + [0.0] * 255 for index in range(len(texts))] + + self.model = types.SimpleNamespace(encode=encode) + vector = types.ModuleType("truememory.vector_search") + vector.get_model = lambda: self.model + vector.set_embedding_model = lambda tier: None + vector.init_vec_table = lambda conn, tier_group: None + vector._build_sep_text = lambda sender, recipient, timestamp, content: "separation " + content + + def selected_serialize(value: list[float]) -> bytes: + if self.serialize_hook is not None: + self.serialize_hook(value) + return serialize(value) + + vector.serialize_f32 = selected_serialize + self.vector = vector + ownership = types.ModuleType("truememory.mps_utils") + ownership.encode_with_model_ownership = lambda model, texts, **kwargs: model.encode(texts, **kwargs) + torch = types.ModuleType("torch") + torch.no_grad = nullcontext + psutil = types.ModuleType("psutil") + psutil.virtual_memory = lambda: types.SimpleNamespace(percent=10.0) + self.dependencies = patch.dict(sys.modules, { + "truememory.vector_search": vector, "truememory.mps_utils": ownership, + "torch": torch, "psutil": psutil, + }) + self.dependencies.start() + self.addCleanup(self.dependencies.stop) + + def open_connection(self) -> sqlite3.Connection: + conn = sqlite3.connect(self.path, timeout=0.5) + self.addCleanup(conn.close) + if self.use_vec: + try: + import sqlite_vec + except ImportError: + self.skipTest("sqlite-vec package is unavailable") + if not hasattr(conn, "enable_load_extension"): + self.skipTest("SQLite cannot load extensions") + conn.enable_load_extension(True) + try: + sqlite_vec.load(conn) + finally: + conn.enable_load_extension(False) + return conn + + def ids(self, conn: sqlite3.Connection | None = None) -> tuple[list[int], list[int]]: + connection = conn or self.conn + return tuple([row[0] for row in connection.execute(f"SELECT rowid FROM {table} ORDER BY rowid")] + for table in (VEC, SEP)) + + def checkpoint(self, conn: sqlite3.Connection | None = None) -> tuple[int, int] | None: + return (conn or self.conn).execute( + "SELECT last_embedded_id,vector_count FROM vector_cache_registry WHERE tier_group='edge'" + ).fetchone() + + def seed_prior(self) -> None: + for table in (VEC, SEP): + self.conn.execute(f"INSERT INTO {table}(rowid,embedding) VALUES (?,?)", (1, serialize([1.0] + [0.0] * 255))) + self.conn.execute("UPDATE vector_cache_registry SET last_embedded_id=1,vector_count=1") + self.conn.commit() + + def process(self, messages: list[dict] | None = None) -> bool: + batch = messages or self.messages[:2] + return self.worker._process_batch( + batch, self.model, VEC, SEP, self.vector.serialize_f32, self.vector._build_sep_text, + record_progress=lambda: self.cache.VectorCacheRegistry.update_progress( + self.worker.conn, "edge", batch[-1]["id"], self.worker._count_vectors(VEC), commit=False), + ) + + def fail_sql_once(self, stage: str, error: BaseException) -> list[str]: + connection = self.conn + failures = [] + + class FaultConnection: + def __getattr__(self, name: str) -> object: + return getattr(connection, name) + + def execute(self, sql: str, *args: object) -> sqlite3.Cursor: + prefixes = {"checkpoint": "UPDATE vector_cache_registry", "commit": "COMMIT", + "clear_separation": f"DELETE FROM {SEP}", "status": "UPDATE rebuild_status"} + if stage in prefixes and sql.startswith(prefixes[stage]) and not failures: + failures.append(stage) + if stage != "commit": + connection.execute(sql, *args) + raise error + return connection.execute(sql, *args) + + def executemany(self, sql: str, values: list[tuple]) -> sqlite3.Cursor: + table = VEC if stage == "completion" else SEP if stage == "separation" else None + if table is not None and sql.startswith(f"INSERT INTO {table}(") and not failures: + failures.append(stage) + connection.execute(sql, values[0]) + raise error + return connection.executemany(sql, values) + + self.worker.conn = FaultConnection() + return failures + + def assert_complete(self, expected: list[int]) -> None: + self.assertEqual(self.ids(), (expected, expected)) + self.assertEqual(self.checkpoint(), (expected[-1], len(expected))) + self.assertFalse(self.conn.in_transaction) + self.assertEqual(self.conn.execute("SELECT status FROM rebuild_status").fetchone()[0], "complete") + + def test_oom_at_either_encode_retries_cleanly_without_writer_lock(self) -> None: + for failing_call in (1, 2): + with self.subTest(failing_call=failing_call): + def fail(call: int, texts: list[str]) -> None: + if call == failing_call: + raise RuntimeError("synthetic out of memory") + self.encode_hook = fail + self.assertEqual(self.worker.run(self.messages[:2], True), (True, 2)) + self.assert_complete([1, 2]) + self.assertEqual(self.throttler.ooms, 1) + self.assertTrue(all(not call["in_transaction"] for call in self.calls)) + self.calls.clear() + self.throttler.ooms = 0 + self.throttler.batch_size = 2 + + def test_oom_at_each_write_or_commit_retries_pair_and_checkpoint(self) -> None: + for stage in ("completion", "separation", "checkpoint", "commit"): + with self.subTest(stage=stage): + failures = self.fail_sql_once(stage, RuntimeError("synthetic out of memory")) + self.assertEqual(self.worker.run(self.messages[:2], False), (True, 2)) + self.assertEqual(failures, [stage]) + self.assert_complete([1, 2]) + self.assertEqual(self.throttler.ooms, 1) + self.worker.conn = self.conn + with self.conn: + self.conn.execute(f"DELETE FROM {VEC}") + self.conn.execute(f"DELETE FROM {SEP}") + self.conn.execute("UPDATE vector_cache_registry SET last_embedded_id=0,vector_count=0") + self.throttler.ooms = 0 + self.throttler.batch_size = 2 + + def test_non_oom_partial_insert_failure_status_cannot_publish_half_pair(self) -> None: + self.seed_prior() + self.fail_sql_once("separation", sqlite3.IntegrityError("synthetic insertion failure")) + self.assertEqual(self.worker.run(self.messages[1:3], False), (False, 0)) + self.conn.commit() + other = self.open_connection() + self.assertEqual(self.ids(other), ([1], [1])) + self.assertEqual(self.checkpoint(other), (1, 1)) + self.assertEqual(other.execute("SELECT status,processed_messages FROM rebuild_status").fetchone(), ("failed", 0)) + + def test_cancellation_during_either_insert_cannot_escape_through_status_commit(self) -> None: + self.seed_prior() + for stage in ("completion", "separation", "checkpoint", "commit"): + with self.subTest(stage=stage): + self.fail_sql_once(stage, KeyboardInterrupt("synthetic cancellation")) + with self.assertRaisesRegex(KeyboardInterrupt, "synthetic cancellation"): + self.process(self.messages[1:3]) + self.worker._update_status("cancelled", 0, 2) + self.conn.commit() + self.assertEqual(self.ids(), ([1], [1])) + self.assertEqual(self.checkpoint(), (1, 1)) + self.assertFalse(self.conn.in_transaction) + self.worker.conn = self.conn + + def test_cancellation_between_encodes_leaves_no_partial_work(self) -> None: + def cancel(call: int, texts: list[str]) -> None: + if call == 2: + raise KeyboardInterrupt("synthetic encode cancellation") + self.encode_hook = cancel + with self.assertRaises(KeyboardInterrupt): + self.process() + self.worker._update_status("cancelled", 0, 2) + self.assertEqual(self.ids(), ([], [])) + self.assertEqual(self.checkpoint(), (0, 0)) + + def test_cancel_flag_finishes_current_pair_and_reports_committed_progress(self) -> None: + def cancel(call: int, texts: list[str]) -> None: + if call == 2: + self.worker.cancel() + self.encode_hook = cancel + self.assertEqual(self.worker.run(self.messages, False), (False, 2)) + self.assertEqual(self.ids(), ([1, 2], [1, 2])) + self.assertEqual(self.checkpoint(), (2, 2)) + self.assertEqual(self.conn.execute("SELECT status,processed_messages FROM rebuild_status").fetchone(), ("cancelled", 2)) + + def test_full_rebuild_clear_failure_restores_pair_and_checkpoint(self) -> None: + self.seed_prior() + self.fail_sql_once("clear_separation", sqlite3.OperationalError("synthetic clear failure")) + self.assertEqual(self.worker.run(self.messages, True), (False, 0)) + self.conn.commit() + self.assertEqual(self.ids(), ([1], [1])) + self.assertEqual(self.checkpoint(), (1, 1)) + self.assertEqual(self.calls, []) + self.assertEqual(self.conn.execute("SELECT status FROM rebuild_status").fetchone()[0], "failed") + + def test_status_failure_after_checkpoint_does_not_retry_the_batch(self) -> None: + self.fail_sql_once("status", sqlite3.OperationalError("synthetic status failure")) + self.assertEqual(self.worker.run(self.messages[:2], False), (True, 2)) + self.assert_complete([1, 2]) + self.assertEqual(len(self.calls), 2) + self.assertEqual(self.throttler.ooms, 0) + + def test_real_commit_denial_cannot_be_published_by_failure_status(self) -> None: + denied = [] + + def deny_commit(action: int, operation: str | None, *unused: str | None) -> int: + if action == sqlite3.SQLITE_TRANSACTION and operation == "COMMIT" and not denied: + denied.append(True) + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + + self.conn.set_authorizer(deny_commit) + try: + self.assertEqual(self.worker.run(self.messages[:2], False), (False, 0)) + finally: + # Disabling with None is supported only on Python 3.11+. + self.conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.assertEqual(denied, [True]) + self.conn.commit() + other = self.open_connection() + self.assertEqual(self.ids(other), ([], [])) + self.assertEqual(self.checkpoint(other), (0, 0)) + self.assertEqual(other.execute("SELECT status FROM rebuild_status").fetchone()[0], "failed") + + def test_malformed_output_counts_fail_before_any_vector_write(self) -> None: + for stage in (1, 2): + with self.subTest(stage=stage): + self.encode_hook = lambda call, texts: [] if call == stage else None + self.assertEqual(self.worker.run(self.messages[:2], False), (False, 0)) + self.assertEqual(self.ids(), ([], [])) + self.assertEqual(self.checkpoint(), (0, 0)) + self.assertFalse(self.conn.in_transaction) + self.calls.clear() + + def test_serialization_failure_precedes_writer_and_leaves_no_rows(self) -> None: + for target in (2, 4): + with self.subTest(target=target): + serializations = [] + def fail(vector: list[float]) -> None: + serializations.append(1) + if len(serializations) == target: + raise ValueError("synthetic serialization failure") + self.serialize_hook = fail + self.assertEqual(self.worker.run(self.messages[:2], False), (False, 0)) + self.assertEqual(self.ids(), ([], [])) + self.assertEqual(self.checkpoint(), (0, 0)) + self.assertTrue(all(not call["in_transaction"] for call in self.calls)) + + def test_pair_and_checkpoint_become_visible_in_the_same_commit(self) -> None: + other = self.open_connection() + observed = [] + original = self.cache.VectorCacheRegistry.update_progress + def witness(*args: object, **kwargs: object) -> None: + original(*args, **kwargs) + observed.append((self.ids(other), self.checkpoint(other))) + with patch.object(self.cache.VectorCacheRegistry, "update_progress", side_effect=witness): + self.assertTrue(self.process()) + self.assertEqual(observed, [(([], []), (0, 0))]) + self.assertEqual(self.ids(other), ([1, 2], [1, 2])) + self.assertEqual(self.checkpoint(other), (2, 2)) + + def test_nested_batch_success_preserves_caller_transaction(self) -> None: + self.conn.execute("INSERT INTO notes VALUES ('synthetic caller work')") + self.assertTrue(self.process()) + self.assertTrue(self.conn.in_transaction) + other = self.open_connection() + self.assertEqual(self.ids(other), ([], [])) + self.assertEqual(self.checkpoint(other), (0, 0)) + self.conn.rollback() + self.assertEqual(self.ids(), ([], [])) + self.assertEqual(self.checkpoint(), (0, 0)) + self.assertEqual(self.conn.execute("SELECT count(*) FROM notes").fetchone()[0], 0) + + def test_nested_batch_failure_preserves_unrelated_caller_work(self) -> None: + self.conn.execute("INSERT INTO notes VALUES ('synthetic caller work')") + self.fail_sql_once("separation", sqlite3.IntegrityError("synthetic nested failure")) + with self.assertRaises(sqlite3.IntegrityError): + self.process() + self.assertTrue(self.conn.in_transaction) + self.conn.commit() + self.assertEqual(self.ids(), ([], [])) + self.assertEqual(self.checkpoint(), (0, 0)) + self.assertEqual(self.conn.execute("SELECT count(*) FROM notes").fetchone()[0], 1) + + def test_real_nested_release_denial_preserves_unrelated_caller_work(self) -> None: + self.conn.execute("INSERT INTO notes VALUES ('synthetic caller work')") + denied = [] + + def deny_release(action: int, operation: str | None, name: str | None, *unused: str | None) -> int: + if (action == sqlite3.SQLITE_SAVEPOINT and operation == "RELEASE" + and name == "truememory_rebuild_batch" and not denied): + denied.append(True) + return sqlite3.SQLITE_DENY + return sqlite3.SQLITE_OK + + self.conn.set_authorizer(deny_release) + try: + with self.assertRaises(sqlite3.DatabaseError): + self.process() + finally: + # Disabling with None is supported only on Python 3.11+. + self.conn.set_authorizer(lambda *_args: sqlite3.SQLITE_OK) + self.assertEqual(denied, [True]) + self.assertTrue(self.conn.in_transaction) + self.conn.commit() + self.assertEqual(self.ids(), ([], [])) + self.assertEqual(self.checkpoint(), (0, 0)) + self.assertEqual(self.conn.execute("SELECT count(*) FROM notes").fetchone()[0], 1) + + def test_run_rejects_uncommitted_caller_work_before_model_encode(self) -> None: + self.conn.execute("INSERT INTO notes VALUES ('synthetic caller work')") + with self.assertRaisesRegex(RuntimeError, "no active transaction"): + self.worker.run(self.messages, False) + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.calls, []) + self.assertEqual(self.open_connection().execute("SELECT count(*) FROM notes").fetchone()[0], 0) + + def test_missing_registry_row_keeps_existing_full_restart_contract(self) -> None: + self.conn.execute("DELETE FROM vector_cache_registry") + self.conn.commit() + self.assertEqual(self.worker.run(self.messages[:2], True), (True, 2)) + self.assertEqual(self.ids(), ([1, 2], [1, 2])) + self.assertIsNone(self.checkpoint()) + + def test_registry_default_commit_remains_compatible(self) -> None: + self.conn.execute("INSERT INTO notes VALUES ('synthetic caller work')") + self.cache.VectorCacheRegistry.update_progress(self.conn, "edge", 7, 7) + self.assertFalse(self.conn.in_transaction) + self.assertEqual(self.open_connection().execute("SELECT count(*) FROM notes").fetchone()[0], 1) + + +class SqliteVecWorkerAtomicity(WorkerAtomicity): + """The same rollback/retry contract exercised against the actual extension.""" + use_vec = True + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-tier-rebuild-oom-recovery-297.py b/tests/test-tier-rebuild-oom-recovery-297.py new file mode 100644 index 00000000..ba93ed6d --- /dev/null +++ b/tests/test-tier-rebuild-oom-recovery-297.py @@ -0,0 +1,418 @@ +"""Bounded rebuild recovery with synthetic models and in-memory SQLite only.""" +from __future__ import annotations + +import ast +import gc +import sqlite3 +import sys +import types +import unittest +import weakref +from collections.abc import Callable +from contextlib import nullcontext +from pathlib import Path +from unittest.mock import Mock, patch + + +SOURCE = Path(__file__).resolve().parents[1] / "truememory/tier_switch/worker.py" +SOURCE_TEXT: str | None = None +VEC = "vec_messages_base" +SEP = "vec_messages_sep_base" + + +def load_worker(registry: type, throttler: type) -> types.ModuleType: + tree = ast.parse(SOURCE_TEXT if SOURCE_TEXT is not None else SOURCE.read_text()) + tree.body = [node for node in tree.body if not ( + isinstance(node, ast.ImportFrom) and (node.module or "").startswith("truememory.") + )] + module = types.ModuleType("synthetic_bounded_rebuild") + module.VectorCacheRegistry = registry + module.DynamicThrottler = throttler + exec(compile(tree, str(SOURCE), "exec"), module.__dict__) + module.log.disabled = True + return module + + +class Workspace: + pass + + +class TestRebuildOOMRecovery(unittest.TestCase): + def setUp(self) -> None: + self.conn = sqlite3.connect(":memory:") + self.addCleanup(self.conn.close) + self.conn.executescript(f""" + CREATE TABLE {VEC}(rowid INTEGER PRIMARY KEY, embedding BLOB); + CREATE TABLE {SEP}(rowid INTEGER PRIMARY KEY, embedding BLOB); + CREATE TABLE progress(last_id INTEGER, n INTEGER); + INSERT INTO progress VALUES(0,0); + CREATE TABLE notes(value TEXT); + CREATE TABLE rebuild_status( + id INTEGER PRIMARY KEY, status TEXT, processed_messages INTEGER, + progress_pct REAL, eta_seconds REAL, batch_size INTEGER, + throughput_ips REAL, ram_pct REAL, last_heartbeat REAL, + completed_at REAL, error TEXT + ); + INSERT INTO rebuild_status(id,status,processed_messages) VALUES(1,'running',0); + """) + self.now = 0.0 + self.admissions: list[int] = [] + self.attempts: list[list[str]] = [] + self.forwards: list[tuple[str, list[str]]] = [] + self.sql: list[str] = [] + self.refs: list[weakref.ReferenceType] = [] + self.cleanup: list[tuple[str, bool, bool]] = [] + self.completed: list[int] = [] + self.ooms = 0 + self.sizes: list[int] = [1] + self.halve = False + self.stage = "" + self.fail_stage = "" + self.failed_attempts: set[int] | None = {1} + self.error_factory: Callable[[], Exception] = lambda: RuntimeError("synthetic out of memory") + self.rollback_error: Exception | None = None + self.before_hook: Callable[[], None] = lambda: None + self.cleanup_hook: Callable[[str], None] = lambda kind: None + self.status_failure = False + self.status_failed = False + owner = self + + class Registry: + @staticmethod + def update_progress(conn: object, group: str, last_id: int, count: int, commit: bool = False) -> None: + owner.assertFalse(commit) + conn.execute("UPDATE progress SET last_id=?,n=?", (last_id, count)) + + class Throttler: + batch_size = 1 + + def before_batch(self) -> tuple[int, dict]: + if len(owner.admissions) >= 96: + raise AssertionError("Synthetic admission call cap reached") + if owner.sizes: + self.batch_size = owner.sizes.pop(0) + owner.admissions.append(self.batch_size) + owner.now += 0.01 + if len(owner.admissions) >= 32: + owner.now = 9001.0 + owner.before_hook() + return self.batch_size, {} + + def on_oom(self) -> None: + owner.ooms += 1 + owner.observe_cleanup("backoff") + if owner.halve: + self.batch_size = max(1, self.batch_size // 2) + + def after_batch(self, count: int, duration: float) -> None: + owner.completed.append(count) + + def should_flush_cache(self) -> bool: + return False + + @staticmethod + def flush_gpu_cache() -> None: + owner.observe_cleanup("flush") + + def get_eta_seconds(self, remaining: int) -> float: + return float(remaining) + + def get_throughput(self) -> float: + return 1.0 + + class Connection: + def __getattr__(self, name: str) -> object: + return getattr(owner.conn, name) + + def execute(self, sql: str, *args: object) -> sqlite3.Cursor: + owner.sql.append(sql) + if sql.startswith("UPDATE rebuild_status") and owner.status_failure and not owner.status_failed: + owner.status_failed = True + owner.conn.execute(sql, *args) + raise sqlite3.OperationalError("synthetic status write failure") + if sql == "COMMIT": + owner.maybe_fail("commit") + cursor = owner.conn.execute(sql, *args) + if sql.startswith("UPDATE progress"): + owner.maybe_fail("checkpoint") + if sql == f"DELETE FROM {SEP}": + owner.maybe_fail("full_clear") + return cursor + + def executemany(self, sql: str, rows: list[tuple]) -> sqlite3.Cursor: + owner.sql.append(sql) + stage = "insert_completion" if VEC in sql else "insert_separation" + if owner.should_fail(stage): + owner.conn.execute(sql, rows[0]) + owner.maybe_fail(stage) + return owner.conn.executemany(sql, rows) + + def rollback(self) -> None: + owner.sql.append("ROLLBACK") + if owner.rollback_error is not None: + raise owner.rollback_error + owner.conn.rollback() + + class Model: + def encode(self, texts: list[str], **kwargs: object) -> list[list[float]]: + owner.assertEqual(kwargs, {"show_progress_bar": False}) + owner.assertFalse(owner.conn.in_transaction) + owner.stage = "separation" if texts[0].startswith("sep:") else "completion" + if owner.stage == "completion": + owner.attempts.append(list(texts)) + owner.forwards.append((owner.stage, list(texts))) + owner.maybe_fail(owner.stage) + return [[1.0, float(index)] for index in range(len(texts))] + + def serialize(row: list[float]) -> bytes: + self.maybe_fail("serialize_" + self.stage) + return b"synthetic vector" + + package = types.ModuleType("truememory") + package.__path__ = [] + vector = types.ModuleType("truememory.vector_search") + vector.get_model = Model + vector.set_embedding_model = lambda tier: None + vector.init_vec_table = lambda conn, tier_group: None + vector.serialize_f32 = serialize + vector._build_sep_text = lambda sender, recipient, timestamp, content: "sep:" + content + ownership = types.ModuleType("truememory.mps_utils") + ownership.encode_with_model_ownership = lambda model, texts, **kwargs: model.encode(texts, **kwargs) + torch = types.ModuleType("torch") + torch.no_grad = nullcontext + torch.cuda = types.SimpleNamespace(OutOfMemoryError=type("CudaOOM", (RuntimeError,), {})) + psutil = types.ModuleType("psutil") + psutil.virtual_memory = lambda: types.SimpleNamespace(percent=0.0) + modules = patch.dict(sys.modules, {"truememory": package, "truememory.vector_search": vector, + "truememory.mps_utils": ownership, "torch": torch, "psutil": psutil}) + modules.start() + self.addCleanup(modules.stop) + self.module = load_worker(Registry, Throttler) + self.module.time = types.SimpleNamespace(time=lambda: self.now) + self.throttler = Throttler() + self.worker = self.module.RebuildWorker(Connection(), "base", "base", self.throttler, status_id=1) + + def should_fail(self, stage: str) -> bool: + return stage == self.fail_stage and ( + self.failed_attempts is None or len(self.attempts) in self.failed_attempts + ) + + def maybe_fail(self, stage: str) -> None: + if self.should_fail(stage): + workspace = Workspace() + self.refs.append(weakref.ref(workspace)) + raise self.error_factory() + + def observe_cleanup(self, kind: str) -> None: + gc.collect() + self.cleanup.append((kind, sys.exc_info()[0] is None, all(ref() is None for ref in self.refs))) + self.cleanup_hook(kind) + + def run_worker(self, count: int, full: bool = False) -> tuple[bool, int]: + return self.worker.run([{"id": index, "content": f"item-{index}"} + for index in range(1, count + 1)], full) + + def ids(self) -> tuple[list[int], list[int]]: + return tuple([row[0] for row in self.conn.execute(f"SELECT rowid FROM {table} ORDER BY rowid")] + for table in (VEC, SEP)) + + def status(self) -> tuple: + return self.conn.execute("SELECT status,processed_messages,error FROM rebuild_status").fetchone() + + def seed_prior(self) -> None: + for table in (VEC, SEP): + self.conn.execute(f"INSERT INTO {table} VALUES(99,?)", (b"prior",)) + self.conn.execute("UPDATE progress SET last_id=99,n=1") + self.conn.commit() + + def test_transient_oom_releases_each_stage_traceback_before_cleanup_and_recovers(self) -> None: + for stage in ("completion", "separation", "serialize_completion", "serialize_separation", + "insert_completion", "insert_separation", "checkpoint", "commit"): + with self.subTest(stage=stage): + self.fail_stage = stage + self.attempts.clear() + self.refs.clear() + self.cleanup.clear() + self.ooms = 0 + for table in (VEC, SEP): + self.conn.execute(f"DELETE FROM {table}") + self.conn.execute("UPDATE progress SET last_id=0,n=0") + self.conn.commit() + self.assertEqual(self.run_worker(1), (True, 1)) + self.assertEqual(self.ooms, 1) + self.assertEqual(self.cleanup, [("backoff", True, True), ("flush", True, True)]) + self.assertEqual(self.ids(), ([1], [1])) + self.assertEqual(self.conn.execute("SELECT * FROM progress").fetchone(), (1, 1)) + self.assertFalse(self.conn.in_transaction) + + def test_persistent_singleton_stops_after_cleanup_retry_with_prior_rows_intact(self) -> None: + self.seed_prior() + self.fail_stage = "separation" + self.failed_attempts = None + self.assertEqual(self.run_worker(1), (False, 0)) + self.assertEqual(self.attempts, [["item-1"], ["item-1"]]) + self.assertEqual(self.ooms, 2) + self.assertEqual(self.ids(), ([99], [99])) + self.assertEqual(self.conn.execute("SELECT * FROM progress").fetchone(), (99, 1)) + self.assertEqual(self.status(), ("failed", 0, + "Rebuild ran out of memory twice at batch size 1; retry after freeing memory.")) + self.assertTrue(all(clear and released for _, clear, released in self.cleanup)) + + def test_tail_eight_configured_thirty_two_can_shrink_and_complete(self) -> None: + self.sizes = [32, 16, 8, 4] + self.fail_stage = "completion" + self.failed_attempts = {1, 2} + self.assertEqual(self.run_worker(8), (True, 8)) + self.assertEqual([len(batch) for batch in self.attempts], [8, 8, 4, 4]) + self.assertEqual(self.ids(), (list(range(1, 9)), list(range(1, 9)))) + + def test_adversarial_admissions_cannot_renew_halving_credits(self) -> None: + for sizes in ([16] * 20, list(range(16, 0, -1)), [16, 32, 15, 32, 7, 32, 3, 32, 1, 32]): + with self.subTest(sizes=sizes): + self.sizes = list(sizes) + self.admissions.clear() + self.attempts.clear() + self.fail_stage = "completion" + self.failed_attempts = None + self.assertEqual(self.run_worker(16), (False, 0)) + counts = [len(batch) for batch in self.attempts] + if sizes == [16] * 20: + self.assertEqual(counts, [16, 16, 8, 8, 4, 4, 2, 2, 1, 1]) + self.assertLessEqual(len(counts), 2 * (16).bit_length()) + self.assertEqual(counts[-2:], [1, 1]) + self.assertTrue(all(right <= left for left, right in zip(counts, counts[1:]))) + self.assertTrue(all(batch[0] == "item-1" for batch in self.attempts)) + + def test_direct_sixteen_to_one_skip_retains_singleton_cleanup_retry(self) -> None: + self.sizes = [16, 1, 32] + self.fail_stage = "completion" + self.failed_attempts = None + self.assertEqual(self.run_worker(16), (False, 0)) + self.assertEqual([len(batch) for batch in self.attempts], [16, 1, 1]) + + def test_recovery_budget_resets_only_after_committed_pair(self) -> None: + self.sizes = [8] * 20 + self.fail_stage = "completion" + self.failed_attempts = {1, 2, 4, 5, 6, 7, 8, 9} + self.assertEqual(self.run_worker(8), (False, 4)) + self.assertEqual([len(batch) for batch in self.attempts], [8, 8, 4, 4, 4, 2, 2, 1, 1]) + self.assertEqual([batch[0] for batch in self.attempts], ["item-1"] * 3 + ["item-5"] * 6) + self.assertEqual(self.completed, [4]) + self.assertEqual(self.ids(), ([1, 2, 3, 4], [1, 2, 3, 4])) + self.assertEqual(self.conn.execute("SELECT * FROM progress").fetchone(), (4, 4)) + self.assertEqual(self.status()[:2], ("failed", 4)) + + def test_successful_batches_keep_original_admissions_order_and_count(self) -> None: + self.sizes = [3, 5, 2] + self.assertEqual(self.run_worker(10), (True, 10)) + self.assertEqual([len(batch) for batch in self.attempts], [3, 5, 2]) + self.assertEqual([text for batch in self.attempts for text in batch], [f"item-{i}" for i in range(1, 11)]) + self.assertEqual(self.completed, [3, 5, 2]) + self.assertEqual(self.cleanup, []) + + def test_cancel_and_timeout_during_recovery_or_next_admission_prevent_native_retry(self) -> None: + for event in ("cancel", "timeout"): + for phase in ("backoff", "flush", "admission"): + with self.subTest(event=event, phase=phase): + self.worker._cancelled = False + self.now = 0.0 + self.attempts.clear() + self.admissions.clear() + self.fail_stage = "completion" + self.failed_attempts = None + + def stop() -> None: + if event == "cancel": + self.worker.cancel() + else: + self.now = 9001.0 + + self.cleanup_hook = lambda kind: stop() if kind == phase else None + self.before_hook = lambda: stop() if phase == "admission" and len(self.admissions) == 2 else None + self.assertEqual(self.run_worker(1), (False, 0)) + self.assertEqual(len(self.attempts), 1) + self.assertEqual(self.status()[0], "cancelled" if event == "cancel" else "timeout") + + def test_bare_memory_error_classification_remains_terminal(self) -> None: + self.fail_stage = "insert_separation" + self.error_factory = MemoryError + self.assertEqual(self.run_worker(1), (False, 0)) + self.assertEqual(self.ooms, 0) + self.assertEqual(self.cleanup, []) + self.assertEqual(self.ids(), ([], [])) + self.assertFalse(self.conn.in_transaction) + + def test_message_classified_write_memory_error_retries_only_after_clean_rollback(self) -> None: + self.fail_stage = "insert_separation" + self.error_factory = lambda: MemoryError("synthetic out of memory") + self.assertEqual(self.run_worker(1), (True, 1)) + self.assertEqual(self.ooms, 1) + self.assertEqual(self.cleanup, [("backoff", True, True), ("flush", True, True)]) + self.assertEqual(self.ids(), ([1], [1])) + self.assertFalse(self.conn.in_transaction) + + def test_uncertain_batch_or_full_clear_rollback_never_classifies_retries_or_publishes_status(self) -> None: + for stage in ("insert_separation", "full_clear"): + with self.subTest(stage=stage): + self.conn.rollback() + for table in (VEC, SEP): + self.conn.execute(f"DELETE FROM {table}") + self.conn.commit() + self.seed_prior() + self.sql.clear() + self.attempts.clear() + self.fail_stage = stage + self.failed_attempts = None + self.rollback_error = MemoryError("synthetic out of memory during rollback") + with patch.object(self.worker, "_is_oom_error", Mock(side_effect=AssertionError("Classified uncertain failure"))) as classify, \ + patch.object(self.worker, "_update_status", Mock(side_effect=AssertionError("Wrote uncertain status"))) as status: + with self.assertRaises(MemoryError) as raised: + self.run_worker(1, full=stage == "full_clear") + self.assertIs(raised.exception, self.rollback_error) + classify.assert_not_called() + status.assert_not_called() + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.sql[-1], "ROLLBACK") + self.assertEqual(len(self.attempts), 0 if stage == "full_clear" else 1) + self.assertEqual(self.cleanup, []) + self.conn.rollback() + self.assertEqual(self.ids(), ([99], [99])) + self.rollback_error = None + + def test_uncertain_status_rollback_stops_before_another_batch_or_false_complete(self) -> None: + self.status_failure = True + self.rollback_error = sqlite3.OperationalError("synthetic status rollback failure") + with self.assertRaises(sqlite3.OperationalError) as raised: + self.run_worker(2) + self.assertIs(raised.exception, self.rollback_error) + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.sql[-1], "ROLLBACK") + self.assertEqual(len(self.attempts), 1) + self.assertEqual(self.cleanup, []) + self.conn.rollback() + self.assertEqual(self.ids(), ([1], [1])) + self.assertEqual(self.conn.execute("SELECT * FROM progress").fetchone(), (1, 1)) + self.assertEqual(self.status()[:2], ("running", 0)) + + def test_clean_status_rollback_remains_best_effort(self) -> None: + self.status_failure = True + self.assertEqual(self.run_worker(2), (True, 2)) + self.assertEqual(len(self.attempts), 2) + self.assertEqual(self.ooms, 0) + self.assertEqual(self.status()[:2], ("complete", 2)) + self.assertFalse(self.conn.in_transaction) + + def test_borrowed_status_savepoint_preserves_caller_work(self) -> None: + self.conn.execute("INSERT INTO notes VALUES('synthetic caller')") + self.status_failure = True + self.worker._update_status("failed", 0, 1) + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.conn.execute("SELECT * FROM notes").fetchall(), [("synthetic caller",)]) + self.assertEqual(self.status()[:2], ("running", 0)) + self.assertFalse(any(sql == "COMMIT" for sql in self.sql)) + self.conn.rollback() + self.assertEqual(self.conn.execute("SELECT * FROM notes").fetchall(), []) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-tier-rebuild-source-756.py b/tests/test-tier-rebuild-source-756.py new file mode 100644 index 00000000..883ae16b --- /dev/null +++ b/tests/test-tier-rebuild-source-756.py @@ -0,0 +1,524 @@ +"""Synthetic in-memory source/pair lifecycle; no package or model initialization.""" + +import builtins +import json +import os +import sqlite3 +import struct +import sys +import types +import unittest +from dataclasses import replace +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +TARGETS = ("vec_messages_synthetic", "vec_messages_sep_synthetic") + + +def load_modules(): + modules = {} + + def safe_import(name, globals=None, locals=None, fromlist=(), level=0): + if name in {"numpy", "torch", "sentence_transformers", "sqlite_vec"}: + raise AssertionError("Native/model imports are excluded from the adapter") + if name.startswith("truememory."): + short = name.removeprefix("truememory.") + if short in modules: + return modules[short] + raise AssertionError("Unexpected application import: " + name) + return builtins.__import__(name, globals, locals, fromlist, level) + + for name in ("storage", "_platform", "maintenance", "rebuild_source", "tier_switch.source"): + module = types.ModuleType("synthetic_tier_source_" + name.replace(".", "_")) + sys.modules[module.__name__] = module + module.__dict__["__builtins__"] = dict(vars(builtins), __import__=safe_import) + path = ROOT / "truememory" / (name.replace(".", "/") + ".py") + exec(compile(path.read_text(encoding="utf-8"), str(path), "exec"), module.__dict__) + modules[name] = module + return modules + + +class FaultConnection(sqlite3.Connection): + fail_sql = None + fail_commit = False + + def execute(self, sql, parameters=()): + if self.fail_sql is not None and self.fail_sql in sql: + self.fail_sql = None + raise sqlite3.OperationalError("synthetic pair write failure") + return super().execute(sql, parameters) + + def commit(self): + if self.fail_commit: + self.fail_commit = False + raise sqlite3.OperationalError("synthetic commit failure") + return super().commit() + + +class TestTierSource(unittest.TestCase): + def setUp(self): + self.modules = load_modules() + self.api = self.modules["tier_switch.source"] + self.source = self.modules["rebuild_source"] + self.conn = sqlite3.connect(":memory:", factory=FaultConnection) + self.addCleanup(self.conn.close) + self.create_schema(self.conn) + + def create_schema(self, conn): + conn.executescript(""" + CREATE TABLE messages(id INTEGER PRIMARY KEY, content TEXT, sender TEXT, recipient TEXT, timestamp TEXT); + CREATE TABLE metadata(key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT); + CREATE TABLE vec_messages_synthetic(embedding BLOB); + CREATE TABLE vec_messages_sep_synthetic(embedding BLOB); + """) + self.source.ensure_rebuild_tracking(conn) + + def seed(self, ids=(-3, 0, 2, 8)): + self.conn.executemany("INSERT INTO messages VALUES (?,?,?,?,?)", [ + (mid, f"synthetic-{mid}", "sender", "recipient", "2026-01-02") for mid in ids + ]) + self.conn.commit() + + def plan(self, **kwargs): + return self.api.plan_tier_source(self.conn, model_id="synthetic-model", dimension=2, targets=TARGETS, **kwargs) + + def start(self): + return self.api.initialize_tier_source(self.conn, self.plan()) + + def vectors(self, count): + return [struct.pack("2f", 1.0, 0.0)] * count, [struct.pack("2f", 0.0, 1.0)] * count + + def publish(self, plan, page, count=None): + left, right = self.vectors(len(page.rows) if count is None else count) + return self.api.publish_tier_prefix(self.conn, plan, page, completion=left, separation=right) + + def build(self, plan=None): + plan = self.start() if plan is None else plan + while plan.manifest.consumed < plan.manifest.total: + page = self.api.read_tier_page(self.conn, plan, size=2) + plan, pending = self.publish(plan, page) + self.assertEqual(pending.rows, ()) + return self.api.finish_tier_source(self.conn, plan) + + def ids(self, table=TARGETS[0]): + return [row[0] for row in self.conn.execute(f'SELECT rowid FROM "{table}" ORDER BY rowid')] + + def test_plan_is_read_only_and_initialization_never_clears(self): + self.seed() + before = self.conn.total_changes + plan = self.plan() + self.assertEqual(plan.action, "full") + self.assertEqual(self.conn.total_changes, before) + self.assertIsNone(self.source.load_manifest(self.conn, self.source.manifest_key(TARGETS))) + plan = self.api.initialize_tier_source(self.conn, plan) + self.assertEqual(plan.action, "resume") + self.assertEqual(self.ids(), []) + self.assertFalse(self.conn.in_transaction) + + def test_negative_zero_sparse_ids_and_exact_metadata_survive(self): + self.seed() + plan = self.start() + page = self.api.read_tier_page(self.conn, plan, size=2) + self.assertEqual(page.messages(), [ + {"id": mid, "content": f"synthetic-{mid}", "sender": "sender", "recipient": "recipient", "timestamp": "2026-01-02"} + for mid in (-3, 0) + ]) + self.assertFalse(self.conn.in_transaction) + result = self.build(plan) + self.assertTrue(result.captured_range_complete) + self.assertTrue(result.current_source_complete) + self.assertEqual(self.ids(), [-3, 0, 2, 8]) + self.assertEqual(self.ids(TARGETS[1]), [-3, 0, 2, 8]) + self.assertEqual((result.plan.manifest.cursor, result.plan.manifest.consumed, result.plan.manifest.outputs), (8, 4, 4)) + self.assertEqual(self.plan().action, "complete") + + def test_prefix_commit_retains_exact_pending_suffix_for_reduced_batches(self): + self.seed() + plan = self.start() + page = self.api.read_tier_page(self.conn, plan, size=4) + updated, pending = self.publish(plan, page, 1) + self.assertEqual(plan.manifest.consumed, 0) + self.assertEqual(updated.manifest.cursor, -3) + self.assertEqual([row[0] for row in pending.rows], [0, 2, 8]) + self.assertEqual(page.rows[1:], pending.rows) + updated, pending = self.publish(updated, pending, 2) + self.assertEqual(updated.manifest.cursor, 2) + self.assertEqual([row[0] for row in pending.rows], [8]) + updated, pending = self.publish(updated, pending, 1) + self.assertEqual(pending.rows, ()) + self.assertEqual(self.ids(), [-3, 0, 2, 8]) + self.assertTrue(self.api.finish_tier_source(self.conn, updated).current_source_complete) + + def test_resume_recomputes_digest_once_and_not_on_each_page(self): + self.seed(range(1, 140)) + plan = self.start() + plan, _ = self.publish(plan, self.api.read_tier_page(self.conn, plan, size=70)) + calls = [] + original = self.api._audit_pair + + def audit(conn, candidate): + calls.append(candidate.manifest.consumed) + return original(conn, candidate) + + self.api._audit_pair = audit + resumed = self.plan() + self.assertEqual(calls, [70]) + self.assertEqual(resumed.manifest.generation, plan.manifest.generation) + result = self.build(resumed) + self.assertEqual(calls, [70]) + self.assertEqual(result.plan.manifest.consumed, 139) + + def test_restart_on_new_in_memory_connection_revalidates_receipt(self): + uri = "file:tier_source_reopen_synthetic?mode=memory&cache=shared" + self.conn = sqlite3.connect(uri, uri=True, factory=FaultConnection) + self.addCleanup(self.conn.close) + self.create_schema(self.conn) + self.seed() + plan = self.start() + plan, _ = self.publish(plan, self.api.read_tier_page(self.conn, plan, size=2)) + other = sqlite3.connect(uri, uri=True, factory=FaultConnection) + self.addCleanup(other.close) + with self.assertRaisesRegex(self.api.TierSourceUntrusted, "Connection changed"): + self.api.read_tier_page(other, plan) + resumed = self.api.plan_tier_source(other, model_id="synthetic-model", dimension=2, targets=TARGETS) + self.assertEqual(resumed.manifest, plan.manifest) + self.assertEqual([row[0] for row in self.api.read_tier_page(other, resumed).rows], [2, 8]) + + def test_empty_source_finishes_without_model_or_vector_work(self): + result = self.build() + self.assertTrue(result.current_source_complete) + self.assertIsNone(result.plan.manifest.cursor) + self.assertEqual(self.ids(), []) + self.assertEqual(self.plan().action, "complete") + + def test_high_tail_requires_replan_and_fixed_range_is_not_current_coverage(self): + self.seed((1, 2)) + plan = self.start() + self.seed((3, 4)) + with self.assertRaisesRegex(self.api.TierSourceUntrusted, "explicitly replan"): + self.api.read_tier_page(self.conn, plan) + resumed = self.plan() + self.assertEqual(resumed.manifest.source.high_id, 2) + result = self.build(resumed) + self.assertTrue(result.captured_range_complete) + self.assertFalse(result.current_source_complete) + delta = self.plan() + self.assertEqual((delta.action, delta.prefix_count, delta.prefix_cursor, delta.manifest.total), ("delta", 2, 2, 2)) + delta = self.api.initialize_tier_source(self.conn, delta) + self.assertEqual([row[0] for row in self.api.read_tier_page(self.conn, delta).rows], [3, 4]) + result = self.build(delta) + self.assertTrue(result.current_source_complete) + self.assertEqual(self.ids(), [1, 2, 3, 4]) + self.assertEqual(self.plan().action, "complete") + + def test_empty_complete_pair_can_begin_first_delta(self): + self.build() + self.seed((1, 2)) + delta = self.plan() + self.assertEqual((delta.action, delta.prefix_count, delta.prefix_cursor), ("delta", 0, None)) + self.assertTrue(self.build(self.api.initialize_tier_source(self.conn, delta)).current_source_complete) + self.assertEqual(self.plan().action, "complete") + + def test_correction_delete_and_low_id_insert_refuse_resume(self): + for sql in ("UPDATE messages SET content='corrected' WHERE id=2", "DELETE FROM messages WHERE id=2", + "INSERT INTO messages(id,content) VALUES (1,'late-low-id')"): + with self.subTest(sql=sql): + conn = sqlite3.connect(":memory:") + try: + self.create_schema(conn) + conn.execute("INSERT INTO messages(id,content) VALUES (2,'synthetic')") + conn.commit() + plan = self.api.plan_tier_source(conn, model_id="synthetic-model", dimension=2, targets=TARGETS) + self.api.initialize_tier_source(conn, plan) + conn.execute(sql) + conn.commit() + with self.assertRaises(self.source.RebuildSourceChanged): + self.api.plan_tier_source(conn, model_id="synthetic-model", dimension=2, targets=TARGETS) + finally: + conn.close() + + def test_existing_unreceipted_rows_are_never_certified_or_cleared(self): + self.seed((1,)) + blob = self.vectors(1)[0][0] + for table in TARGETS: + self.conn.execute(f'INSERT INTO "{table}"(rowid,embedding) VALUES (1,?)', (blob,)) + self.conn.commit() + for force in (False, True): + with self.assertRaisesRegex(self.api.TierSourceUntrusted, "empty inactive pair"): + self.plan(force=force) + self.assertEqual(self.ids(), [1]) + self.assertEqual(self.ids(TARGETS[1]), [1]) + + def test_same_count_tampered_vectors_and_wrong_width_refuse_restart(self): + self.seed((1,)) + self.build() + for blob in (struct.pack("2f", 0.5, 0.5), b"bad"): + with self.subTest(blob=blob): + self.conn.execute(f'UPDATE "{TARGETS[0]}" SET embedding=? WHERE rowid=1', (blob,)) + self.conn.commit() + with self.assertRaises(self.api.TierSourceUntrusted): + self.plan() + self.assertEqual(self.ids(), [1]) + + def test_pair_row_deletion_or_addition_refuses_restart(self): + self.seed((1, 2)) + result = self.build() + self.conn.execute(f'DELETE FROM "{TARGETS[1]}" WHERE rowid=1') + self.conn.commit() + with self.assertRaises(self.api.TierSourceUntrusted): + self.plan() + self.assertTrue(result.plan.manifest.complete) + + def test_pair_schema_drop_recreate_is_not_matching_content_proof(self): + self.seed((1,)) + self.build() + self.conn.executescript(f'DROP TABLE "{TARGETS[0]}"; CREATE TABLE "{TARGETS[0]}"(embedding BLOB);') + self.conn.execute(f'INSERT INTO "{TARGETS[0]}"(rowid,embedding) VALUES (1,?)', (self.vectors(1)[0][0],)) + self.conn.commit() + with self.assertRaisesRegex(self.api.TierSourceUntrusted, "schema"): + self.plan() + + def test_temp_case_variant_shadows_and_temp_triggers_refuse(self): + for statement in ( + 'CREATE TEMP TABLE MeSsAgEs(id INTEGER)', 'CREATE TEMP TABLE MeTaDaTa(key TEXT)', + 'CREATE TEMP TABLE VEC_MESSAGES_SYNTHETIC(embedding BLOB)', + 'CREATE TEMP TABLE maintenance_source_state(singleton INTEGER)', + 'CREATE TEMP TABLE truememory_rebuild_source_v1(singleton INTEGER)', + 'CREATE TEMP TRIGGER synthetic_shadow AFTER INSERT ON main.messages BEGIN SELECT 1; END', + ): + with self.subTest(statement=statement): + conn = sqlite3.connect(":memory:") + try: + self.create_schema(conn) + conn.execute(statement) + with self.assertRaisesRegex(self.api.TierSourceUntrusted, "TEMP"): + self.api.plan_tier_source(conn, model_id="synthetic-model", dimension=2, targets=TARGETS) + finally: + conn.close() + + def test_shadow_added_after_planning_refuses_publication(self): + self.seed((1,)) + plan = self.start() + page = self.api.read_tier_page(self.conn, plan) + self.conn.execute('CREATE TEMP TABLE VEC_MESSAGES_SEP_SYNTHETIC(embedding BLOB)') + with self.assertRaisesRegex(self.api.TierSourceUntrusted, "TEMP"): + self.publish(plan, page) + self.assertEqual(self.ids(), []) + + def test_unrelated_write_invalidates_fence_without_changing_checkpoint(self): + self.seed((1,)) + plan = self.start() + page = self.api.read_tier_page(self.conn, plan) + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('unrelated','value')") + self.conn.commit() + with self.assertRaisesRegex(self.api.TierSourceUntrusted, "Connection changed"): + self.publish(plan, page) + resumed = self.plan() + self.assertEqual(resumed.manifest, plan.manifest) + self.assertEqual(self.publish(resumed, page)[0].manifest.consumed, 1) + + def test_external_commit_data_version_requires_explicit_revalidation(self): + uri = "file:tier_source_synthetic?mode=memory&cache=shared" + first = sqlite3.connect(uri, uri=True) + second = sqlite3.connect(uri, uri=True) + try: + self.create_schema(first) + plan = self.api.plan_tier_source(first, model_id="synthetic-model", dimension=2, targets=TARGETS) + plan = self.api.initialize_tier_source(first, plan) + second.execute("INSERT INTO metadata(key,value) VALUES ('external','synthetic')") + second.commit() + with self.assertRaisesRegex(self.api.TierSourceUntrusted, "Connection changed"): + self.api.read_tier_page(first, plan) + self.assertEqual(first.total_changes, plan.changes) + finally: + second.close() + first.close() + + def test_second_pair_write_failure_rolls_back_both_rows_and_checkpoint(self): + self.seed((1, 2)) + plan = self.start() + page = self.api.read_tier_page(self.conn, plan) + self.conn.fail_sql = f'INSERT INTO main."{TARGETS[1]}"' + with self.assertRaisesRegex(sqlite3.OperationalError, "pair write"): + self.publish(plan, page) + self.assertEqual(self.ids(), []) + self.assertEqual(self.ids(TARGETS[1]), []) + self.assertFalse(self.conn.in_transaction) + resumed = self.plan() + self.assertEqual(resumed.manifest, plan.manifest) + updated, pending = self.publish(resumed, page, 1) + self.assertEqual(updated.manifest.cursor, 1) + self.assertEqual([row[0] for row in pending.rows], [2]) + + def test_commit_failure_rolls_back_pair_receipt_and_manifest(self): + self.seed((1,)) + plan = self.start() + page = self.api.read_tier_page(self.conn, plan) + self.conn.fail_commit = True + with self.assertRaisesRegex(sqlite3.OperationalError, "commit failure"): + self.publish(plan, page) + self.assertEqual(self.ids(), []) + self.assertEqual(self.plan().manifest, plan.manifest) + self.assertFalse(self.conn.in_transaction) + + def test_pending_inputs_prevent_finish_and_bad_prefixes_do_not_advance(self): + self.seed() + plan = self.start() + page = self.api.read_tier_page(self.conn, plan, size=2) + with self.assertRaises(self.api.TierSourceUntrusted): + self.api.finish_tier_source(self.conn, plan) + for left, right in (([], []), (self.vectors(1)[0], []), (self.vectors(3)[0], self.vectors(3)[1])): + with self.assertRaises(self.api.TierSourceUntrusted): + self.api.publish_tier_prefix(self.conn, plan, page, completion=left, separation=right) + with self.assertRaises(ValueError): + self.api.publish_tier_prefix(self.conn, plan, page, completion=[b"bad"], separation=[b"bad"]) + changed = replace(page, rows=((page.rows[0][0], "wrong-text", *page.rows[0][2:]), *page.rows[1:])) + with self.assertRaisesRegex(self.api.TierSourceUntrusted, "Pending page"): + self.publish(plan, changed, 1) + self.assertEqual(self.plan().manifest, plan.manifest) + self.assertEqual(self.ids(), []) + + def test_borrowed_transaction_is_never_committed_or_rolled_back(self): + plan = self.start() + self.conn.execute("INSERT INTO metadata(key,value) VALUES ('borrowed','pending')") + with self.assertRaisesRegex(self.api.TierSourceUntrusted, "owned, clean"): + self.api.read_tier_page(self.conn, plan) + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.conn.execute("SELECT value FROM metadata WHERE key='borrowed'").fetchone()[0], "pending") + self.conn.rollback() + + def test_page_fields_require_exact_sqlite_type_and_signed_real_zero(self): + self.conn = sqlite3.connect(":memory:", factory=FaultConnection) + self.addCleanup(self.conn.close) + self.conn.executescript(""" + CREATE TABLE messages(id INTEGER PRIMARY KEY, content, sender, recipient, timestamp); + CREATE TABLE metadata(key TEXT PRIMARY KEY, value TEXT NOT NULL); + CREATE TABLE vec_messages_synthetic(embedding BLOB); + CREATE TABLE vec_messages_sep_synthetic(embedding BLOB); + """) + self.source.ensure_rebuild_tracking(self.conn) + self.conn.executemany("INSERT INTO messages(id,content) VALUES (?,?)", [(1, 1), (2, -0.0)]) + self.conn.commit() + plan = self.start() + page = self.api.read_tier_page(self.conn, plan) + integer_as_real = replace(page, rows=((1, 1.0, None, None, None), page.rows[1])) + signed_zero = replace(page, rows=(page.rows[0], (2, 0.0, None, None, None))) + for changed in (integer_as_real, signed_zero): + with self.assertRaisesRegex(self.api.TierSourceUntrusted, "Pending page"): + self.publish(plan, changed) + self.assertEqual(self.ids(), []) + self.assertEqual(self.publish(plan, page)[0].manifest.consumed, 2) + + def test_malformed_manifest_types_ranges_and_complete_flags_refuse(self): + self.seed() + plan = self.start() + key = self.source.manifest_key(TARGETS) + original = json.loads(plan.saved_manifest) + mutations = ( + {"version": True}, {"dimension": True}, {"consumed": -1}, {"consumed": True}, + {"outputs": 1}, {"consumed": 5, "outputs": 5}, {"cursor": 0}, {"complete": True}, + {"complete": 1}, {"total": 5}, {"source": None}, {"schema_version": None}, + {"targets": list(reversed(TARGETS))}, {"generation": "not-a-generation"}, + ) + for update in mutations: + with self.subTest(update=update): + self.conn.execute("UPDATE metadata SET value=? WHERE key=?", (json.dumps(dict(original, **update)), key)) + self.conn.commit() + with self.assertRaises(self.api.TierSourceUntrusted): + self.plan() + + def test_missing_receipt_is_not_registry_or_manifest_compatibility(self): + self.start() + self.conn.execute("DELETE FROM metadata WHERE key=?", (self.api._receipt_key(TARGETS),)) + self.conn.commit() + with self.assertRaisesRegex(self.api.TierSourceUntrusted, "Both manifest and pair receipt"): + self.plan() + + def test_oversized_and_duplicate_metadata_are_rejected(self): + plan = self.start() + key = self.source.manifest_key(TARGETS) + for raw in ("x" * 65537, plan.saved_manifest[:-1] + ',"version":1}'): + self.conn.execute("UPDATE metadata SET value=? WHERE key=?", (raw, key)) + self.conn.commit() + with self.assertRaises(self.api.TierSourceUntrusted): + self.plan() + + def test_wrong_model_dimension_and_target_identifiers_refuse(self): + self.start() + for model, dimension, targets in ( + ("other", 2, TARGETS), ("synthetic-model", 3, TARGETS), + ("synthetic-model", 4097, TARGETS), + ("synthetic-model", True, TARGETS), ("synthetic-model", 2, ("vec_messages_bad;--", TARGETS[1])), + ): + with self.assertRaises((ValueError, self.api.TierSourceUntrusted)): + self.api.plan_tier_source(self.conn, model_id=model, dimension=dimension, targets=targets) + + def test_restart_digest_reader_uses_bounded_pages(self): + self.seed(range(1, 170)) + self.build() + batches = [] + original = self.api._bounded_rows + + def bounded(cursor): + class Reader: + def fetchmany(inner, size): + batches.append(size) + return cursor.fetchmany(size) + + def close(inner): + cursor.close() + yield from original(Reader()) + + self.api._bounded_rows = bounded + self.assertEqual(self.plan().action, "complete") + self.assertGreater(len(batches), 3) + self.assertEqual(set(batches), {64}) + + +@unittest.skipUnless(os.environ.get("TRUEMEMORY_PREPARED_NATIVE_SQLITE_TEST") == "1", "GPUBox-only native SQLite gate") +class TestNativeTierSource(unittest.TestCase): + def test_real_vec0_pair_restart_and_delta(self): + import sqlite_vec + + modules = load_modules() + api, source = modules["tier_switch.source"], modules["rebuild_source"] + conn = sqlite3.connect(":memory:") + try: + conn.enable_load_extension(True) + sqlite_vec.load(conn) + conn.enable_load_extension(False) + conn.executescript(""" + CREATE TABLE messages(id INTEGER PRIMARY KEY, content TEXT, sender TEXT, recipient TEXT, timestamp TEXT); + CREATE TABLE metadata(key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT); + CREATE VIRTUAL TABLE vec_messages_synthetic USING vec0(embedding float[2] distance_metric=cosine); + CREATE VIRTUAL TABLE vec_messages_sep_synthetic USING vec0(embedding float[2] distance_metric=cosine); + """) + source.ensure_rebuild_tracking(conn) + conn.executemany("INSERT INTO messages(id,content) VALUES (?,?)", [(-3, "synthetic-negative"), (0, "synthetic-zero")]) + conn.commit() + + def plan(): + return api.plan_tier_source(conn, model_id="synthetic-model", dimension=2, targets=TARGETS) + + current = api.initialize_tier_source(conn, plan()) + page = api.read_tier_page(conn, current) + left, right = [struct.pack("2f", 1., 0.)] * 2, [struct.pack("2f", 0., 1.)] * 2 + current, pending = api.publish_tier_prefix(conn, current, page, completion=left[:1], separation=right[:1]) + self.assertEqual(plan().manifest, current.manifest) + current, pending = api.publish_tier_prefix(conn, current, pending, completion=left[1:], separation=right[1:]) + self.assertTrue(api.finish_tier_source(conn, current).current_source_complete) + conn.execute("INSERT INTO messages(id,content) VALUES (2,'synthetic-tail')") + conn.commit() + current = api.initialize_tier_source(conn, plan()) + page = api.read_tier_page(conn, current) + current, _ = api.publish_tier_prefix(conn, current, page, completion=left[:1], separation=right[:1]) + self.assertTrue(api.finish_tier_source(conn, current).current_source_complete) + self.assertEqual(plan().action, "complete") + for table in TARGETS: + self.assertEqual([row[0] for row in conn.execute(f'SELECT rowid FROM "{table}" ORDER BY rowid')], [-3, 0, 2]) + finally: + conn.close() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-tier-reconnect-admission-684.py b/tests/test-tier-reconnect-admission-684.py new file mode 100644 index 00000000..35340824 --- /dev/null +++ b/tests/test-tier-reconnect-admission-684.py @@ -0,0 +1,746 @@ +"""Poisoned-handle public admission using production AST and synthetic owners.""" +from __future__ import annotations + +import ast +from collections.abc import Callable, Iterator +from contextlib import contextmanager, nullcontext +import gc +import os +from pathlib import Path +import sqlite3 +import subprocess +import sys +import threading +import types +from typing import NamedTuple +import unittest +import uuid +import weakref +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] + + +def source(path: str) -> str: + commit = os.environ.get("TRUEMEMORY_RECONNECT_SOURCE_COMMIT") + if commit: + return subprocess.run( + ["git", "show", f"{commit}:truememory/{path}"], cwd=ROOT, + check=True, capture_output=True, text=True, + ).stdout + return (ROOT / "truememory" / path).read_text(encoding="utf-8") + + +def definitions(path: str, names: set[str], namespace: dict[str, object]) -> types.ModuleType: + tree = ast.parse(source(path)) + selected = [node for node in ast.walk(tree) + if isinstance(node, (ast.FunctionDef, ast.ClassDef)) and node.name in names] + if {node.name for node in selected} != names: + raise AssertionError("Missing production definitions") + module = types.ModuleType("synthetic_reconnect_684") + module.__dict__.update(namespace) + selected.insert(0, ast.ImportFrom(module="__future__", names=[ast.alias(name="annotations")], level=0)) + loaded = ast.fix_missing_locations(ast.Module(body=selected, type_ignores=[])) + exec(compile(loaded, path, "exec"), module.__dict__) + return module + + +class Clock: + def __init__(self) -> None: + self.now = 100.0 + + def monotonic(self) -> float: + return self.now + + +class Signal: + def __init__(self, action: Callable[[float], None] | None = None) -> None: + self.set_value = False + self.action = action + self.waits: list[float] = [] + + def is_set(self) -> bool: + return self.set_value + + def set(self) -> None: + self.set_value = True + + def wait(self, timeout: float) -> bool: + self.waits.append(timeout) + if self.action is not None: + self.action(timeout) + return self.set_value + + +class Writer: + def __init__(self) -> None: + self.owned = False + + def owned_by_current_thread(self) -> bool: + return self.owned + + def locked(self) -> bool: + return self.owned + + +class SyntheticPath: + def __init__(self) -> None: + self.mkdir_calls = 0 + self.chmod_calls = 0 + + def __str__(self) -> str: + return "/synthetic/reconnect-684.db" + + def resolve(self) -> SyntheticPath: + return self + + @property + def parent(self) -> SyntheticPath: + return self + + def mkdir(self, *, parents: bool, exist_ok: bool) -> None: + self.mkdir_calls += 1 + + def chmod(self, mode: int) -> None: + self.chmod_calls += 1 + + +class Handle: + def __init__(self, reason: str | None = None) -> None: + self.reason = reason + self._in_transaction = False + self.probes = 0 + self.closes = 0 + + def execute(self, sql: str) -> None: + if sql != "PRAGMA schema_version": + raise AssertionError("Unexpected synthetic SQL") + self.probes += 1 + if self.reason == "closed": + raise sqlite3.ProgrammingError("Cannot operate on a closed database.") + if self.reason == "io_error": + raise sqlite3.OperationalError("disk I/O error") + if self.reason == "other_error": + raise sqlite3.OperationalError("synthetic unrelated error") + + def close(self) -> None: + self.reason = "closed" + self.closes += 1 + + @property + def in_transaction(self) -> bool: + if self.reason == "closed": + raise sqlite3.ProgrammingError("Cannot operate on a closed database.") + return self._in_transaction + + @in_transaction.setter + def in_transaction(self, borrowed: bool) -> None: + self._in_transaction = borrowed + + +class SyntheticEngine: + pass + + +class Fixture: + def __init__(self, reason: str | None = "closed", *, actual_lock: bool = False) -> None: + self.clock = Clock() + self.path = SyntheticPath() + self.scope_depth = 0 + self.wait_scopes: list[int] = [] + self.deadlines: list[float] = [] + self.local = types.SimpleNamespace(operation=None) + self.admitted = False + self.admission_error = False + self.factory_calls = 0 + self.acquisitions = 0 + self.open_calls = 0 + self.previous_modules: dict[str, types.ModuleType | None] = {} + self.registry = threading.Lock() + self.held_paths: dict[object, int] = {} + self.coordinators: dict[object, object] = {} + self.handle = Handle(reason) + self.replacement = Handle() + self.reopen_calls: list[tuple[object, object]] = [] + self.reopened: tuple[Handle, bool] | None = None + self.gate_error = type("ServingLeaseError", (RuntimeError,), {}) + self.runtime_error = type("TierRuntimeError", (RuntimeError,), {}) + self.engine = SyntheticEngine() + self.engine.__dict__.update( + conn=self.handle, db_path=self.path, _write_lock=Writer(), _init_lock=object(), + _reconnect_receipt=None, ready=True, _runtime_initialized=True, _has_vectors=True, + _runtime_vector_generation="synthetic-generation", _runtime_vector_connection=self.handle, + ) + + @contextmanager + def connection_read(*args: object, **kwargs: object) -> Iterator[None]: + self.scope_depth += 1 + try: + yield + finally: + self.scope_depth -= 1 + + def factory(path: object, create: object) -> Handle: + self.factory_calls += 1 + return self.replacement + + def file_lock(path: object) -> int: + self.acquisitions += 1 + return 901 + + def release(fd: int) -> None: + for path, claimed in tuple(self.held_paths.items()): + if claimed == fd: + del self.held_paths[path] + + self.maintenance = definitions("maintenance.py", { + "MaintenanceBusyError", "MaintenanceOwnership", "_AutomaticAdmission", + "_automatic_admission_locked", "_acquire_owner", "_bind_owner", "maintenance_owner", + "wait_for_automatic_owner", + }, { + "os": os, "weakref": weakref, "uuid": uuid, "NamedTuple": NamedTuple, + "contextmanager": contextmanager, "time": self.clock, + "threading": types.SimpleNamespace(Event=Signal, TIMEOUT_MAX=threading.TIMEOUT_MAX), + "Path": lambda value: value, "_registry_lock": self.registry, + "_coordinators": self.coordinators, "_held_paths": self.held_paths, + "_held_fds": set(), "_thread_owners": threading.local(), + "try_file_lock": file_lock, "_release_owner": release, + "canonical_database_path": lambda value: value, + }) + self.maintenance._active_initialization_receipt = lambda path: nullcontext(None) + self.runtime = definitions("tier_switch/runtime.py", {"open_engine_for_operation"}, { + "time": self.clock, "ConnectionWriteLock": Writer, "_local": self.local, + "TierRuntimeError": self.runtime_error, + "serving": types.SimpleNamespace(current_thread_admitted=self.currently_admitted, + ServingLeaseError=self.gate_error), + }) + self.runtime._connection_read = connection_read + self.runtime._open_serving_connection_at_path = factory + if actual_lock: + locks = definitions("tier_switch/runtime.py", { + "ConnectionWriteLock", "_connection_read", "_check", "_remaining", + }, { + "threading": threading, "sqlite3": sqlite3, "time": self.clock, + "contextmanager": contextmanager, "TierRuntimeError": self.runtime_error, + "serving": self.runtime.serving, + }) + self.runtime.ConnectionWriteLock = locks.ConnectionWriteLock + self.runtime._connection_read = locks._connection_read + self.engine._write_lock = locks.ConnectionWriteLock(self.engine) + self.engine._init_lock = threading.Lock() + self.storage = types.ModuleType("truememory.storage") + storage_tree = ast.parse(source("storage.py")) + self.storage.DEFAULT_BUSY_TIMEOUT_MS = next( + ast.literal_eval(node.value) for node in storage_tree.body + if isinstance(node, ast.Assign) + and any(isinstance(target, ast.Name) and target.id == "DEFAULT_BUSY_TIMEOUT_MS" + for target in node.targets) + ) + engine_module = definitions("engine.py", {"_open_connection_handle"}, { + "sqlite3": sqlite3, "create_db": lambda path: self.replacement, "_HAS_HYBRID": False, + }) + actual_open = engine_module._open_connection_handle + + def opening() -> None: + self.open_calls += 1 + actual_open(self.engine) + + def reopen(path: object, *, receipt: object) -> tuple[Handle, bool] | None: + self.reopen_calls.append((path, receipt)) + return self.reopened + + self.engine._open_connection_handle = opening + self.engine._reopen_initialized_connection = reopen + actual_wait = self.maintenance.wait_for_automatic_owner + + def waiting(error: BaseException, *, deadline: float) -> None: + self.wait_scopes.append(self.scope_depth) + self.deadlines.append(deadline) + actual_wait(error, deadline=deadline) + + self.maintenance.wait_for_automatic_owner = waiting + for name, module in { + "truememory": types.ModuleType("truememory"), + "truememory.tier_switch": types.ModuleType("truememory.tier_switch"), + "truememory.tier_switch.runtime": self.runtime, + "truememory.maintenance": self.maintenance, "truememory.storage": self.storage, + }.items(): + self.previous_modules[name] = sys.modules.get(name) + sys.modules[name] = module + + def close(self) -> None: + for name, previous in self.previous_modules.items(): + if previous is None: + sys.modules.pop(name, None) + else: + sys.modules[name] = previous + + def currently_admitted(self) -> bool: + if self.admission_error: + raise self.gate_error("synthetic gate unavailable") + return self.admitted + + def automatic_owner(self, *, release_after: float = 1.0) -> object: + coordinator = types.SimpleNamespace( + _mutex=threading.Lock(), _pid=os.getpid(), _active=True, _cancel=Signal(), + ) + # SimpleNamespace has no weak-reference slot; the callable preserves identity. + released = Signal() + origin = self.maintenance._AutomaticAdmission( + lambda: coordinator, self.path, os.getpid(), 71, "synthetic-launch", released, Signal(), + ) + coordinator._admission_owner = origin + self.coordinators[self.path] = coordinator + self.held_paths[self.path] = 71 + + def release(timeout: float) -> None: + self.clock.now += min(timeout, release_after) + if timeout >= release_after: + self.held_paths.clear() + coordinator._active = False + released.set() + + released.action = release + return origin + + +class TestReconnectAdmission(unittest.TestCase): + def fixture(self, reason: str | None = "closed", *, actual_lock: bool = False) -> Fixture: + fixture = Fixture(reason, actual_lock=actual_lock) + self.addCleanup(fixture.close) + return fixture + + def test_closed_handle_retries_exact_automatic_owner_outside_engine_locks(self) -> None: + self.assertTrue(gc.isenabled()) + fixture = self.fixture() + origin = fixture.automatic_owner() + fixture.runtime.open_engine_for_operation(fixture.engine) + self.assertEqual(origin.released.waits, [10.0]) + self.assertEqual(fixture.wait_scopes, [0]) + self.assertEqual(fixture.deadlines, [110.0]) + self.assertIs(fixture.engine.conn, fixture.replacement) + self.assertEqual(fixture.factory_calls, 1) + self.assertEqual(fixture.open_calls, 2) + self.assertFalse(fixture.engine.ready) + self.assertFalse(fixture.engine._runtime_initialized) + + def test_io_error_handle_retries_same_strict_automatic_owner(self) -> None: + self.assertTrue(gc.isenabled()) + fixture = self.fixture("io_error") + origin = fixture.automatic_owner() + fixture.runtime.open_engine_for_operation(fixture.engine) + self.assertEqual(origin.released.waits, [10.0]) + self.assertEqual(fixture.handle.closes, 1) + self.assertEqual(fixture.wait_scopes, [0]) + self.assertIs(fixture.engine.conn, fixture.replacement) + + def test_healthy_handle_never_claims_maintenance_or_waits(self) -> None: + fixture = self.fixture(None) + origin = fixture.automatic_owner() + fixture.runtime.open_engine_for_operation(fixture.engine) + self.assertIs(fixture.engine.conn, fixture.handle) + self.assertTrue(fixture.engine.ready) + self.assertEqual(fixture.handle.probes, 1) + self.assertEqual(fixture.open_calls, 1) + self.assertEqual(fixture.factory_calls, 0) + self.assertEqual(fixture.acquisitions, 0) + self.assertEqual(origin.released.waits, []) + self.assertEqual(fixture.deadlines, []) + + def test_private_opener_attests_exact_failed_handle_without_waiting(self) -> None: + fixture = self.fixture() + origin = fixture.automatic_owner() + with self.assertRaises(fixture.maintenance.MaintenanceBusyError) as raised: + fixture.engine._open_connection_handle() + self.assertIs(raised.exception._automatic_owner, origin) + self.assertIs(getattr(raised.exception, "_reconnect_engine", None), fixture.engine) + self.assertIs(getattr(raised.exception, "_reconnect_handle", None), fixture.handle) + self.assertIs(fixture.engine.conn, fixture.handle) + self.assertEqual(fixture.wait_scopes, []) + self.assertEqual(origin.released.waits, []) + self.assertEqual(fixture.factory_calls, 0) + + def test_missing_handle_keeps_existing_exact_origin_retry(self) -> None: + fixture = self.fixture() + fixture.engine.conn = None + origin = fixture.automatic_owner() + fixture.runtime.open_engine_for_operation(fixture.engine) + self.assertEqual(origin.released.waits, [10.0]) + self.assertEqual(fixture.wait_scopes, [0]) + self.assertIs(fixture.engine.conn, fixture.replacement) + self.assertEqual(fixture.open_calls, 2) + + def test_manual_receiptless_foreign_and_cancelled_owners_never_wait(self) -> None: + for kind in ("manual", "receiptless", "foreign_pid", "cancelled", "foreign_descriptor"): + with self.subTest(kind=kind): + fixture = self.fixture() + origin = fixture.automatic_owner() + coordinator = fixture.coordinators[fixture.path] + if kind == "manual": + fixture.coordinators.clear() + elif kind == "receiptless": + coordinator._admission_owner = None + elif kind == "foreign_pid": + coordinator._pid = -1 + elif kind == "cancelled": + origin.cancelled.set() + else: + fixture.held_paths[fixture.path] = 991 + with self.assertRaises(fixture.maintenance.MaintenanceBusyError): + fixture.runtime.open_engine_for_operation(fixture.engine) + self.assertEqual(origin.released.waits, []) + self.assertIs(fixture.engine.conn, fixture.handle) + self.assertEqual(fixture.factory_calls, 0) + self.assertEqual(fixture.open_calls, 1) + + def test_serving_runtime_raw_or_unqueryable_scopes_refuse_before_wait(self) -> None: + for kind in ("runtime", "raw_or_exclusive", "gate_error", "writer"): + with self.subTest(kind=kind): + fixture = self.fixture() + origin = fixture.automatic_owner() + if kind == "runtime": + fixture.local.operation = object() + elif kind == "raw_or_exclusive": + fixture.admitted = True + elif kind == "gate_error": + fixture.admission_error = True + else: + fixture.engine._write_lock.owned = True + with self.assertRaises(fixture.maintenance.MaintenanceBusyError): + fixture.runtime.open_engine_for_operation(fixture.engine) + self.assertEqual(origin.released.waits, []) + self.assertEqual(fixture.wait_scopes, []) + self.assertEqual(fixture.factory_calls, 0) + self.assertIs(fixture.engine.conn, fixture.handle) + + def test_fresh_bound_writer_is_refused_before_opening(self) -> None: + fixture = self.fixture() + fixture.engine.conn = None + fixture.engine._write_lock.owned = True + with self.assertRaises(fixture.maintenance.MaintenanceBusyError): + fixture.runtime.open_engine_for_operation(fixture.engine) + self.assertEqual(fixture.open_calls, 0) + self.assertEqual(fixture.wait_scopes, []) + + def test_healthy_or_borrowed_unmarked_busy_error_never_waits(self) -> None: + for borrowed in (False, True): + with self.subTest(borrowed=borrowed): + fixture = self.fixture(None) + fixture.handle.in_transaction = borrowed + origin = fixture.automatic_owner() + error = fixture.maintenance.MaintenanceBusyError("synthetic unmarked failure") + error._automatic_owner = origin + + def reject() -> None: + raise error + + fixture.engine._open_connection_handle = reject + with self.assertRaises(fixture.maintenance.MaintenanceBusyError) as raised: + fixture.runtime.open_engine_for_operation(fixture.engine) + self.assertIs(raised.exception, error) + self.assertEqual(fixture.wait_scopes, []) + self.assertEqual(origin.released.waits, []) + self.assertIs(fixture.engine.conn, fixture.handle) + self.assertEqual(fixture.handle.in_transaction, borrowed) + + def test_validated_receipt_reopen_preserves_existing_ownership_free_path(self) -> None: + for controlled in (False, True): + with self.subTest(controlled=controlled): + fixture = self.fixture() + receipt = object() + fixture.engine._reconnect_receipt = receipt + fixture.reopened = (fixture.replacement, controlled) + origin = fixture.automatic_owner() + fixture.runtime.open_engine_for_operation(fixture.engine) + self.assertEqual(fixture.reopen_calls, [(fixture.path, receipt)]) + self.assertIs(fixture.engine.conn, fixture.replacement) + self.assertEqual(origin.released.waits, []) + self.assertEqual(fixture.wait_scopes, []) + self.assertEqual(fixture.factory_calls, 0) + self.assertEqual(fixture.engine.ready, not controlled) + self.assertIs(fixture.engine._reconnect_receipt, None if controlled else receipt) + + def test_rejected_receipt_uses_owned_fallback_and_resets_readiness(self) -> None: + fixture = self.fixture() + receipt = object() + fixture.engine._reconnect_receipt = receipt + origin = fixture.automatic_owner() + fixture.runtime.open_engine_for_operation(fixture.engine) + self.assertEqual(fixture.reopen_calls, [(fixture.path, receipt), (fixture.path, receipt)]) + self.assertEqual(origin.released.waits, [10.0]) + self.assertIs(fixture.engine.conn, fixture.replacement) + self.assertIsNone(fixture.engine._reconnect_receipt) + self.assertIsNone(fixture.engine._runtime_vector_generation) + self.assertIsNone(fixture.engine._runtime_vector_connection) + self.assertFalse(fixture.engine._has_vectors) + self.assertFalse(fixture.engine.ready) + + def test_successive_verified_owners_share_one_ten_second_budget(self) -> None: + fixture = self.fixture() + first = fixture.automatic_owner(release_after=3.0) + release = first.released.action + successors: list[object] = [] + + def next_owner(timeout: float) -> None: + release(timeout) + successors.append(fixture.automatic_owner(release_after=2.0)) + + first.released.action = next_owner + fixture.runtime.open_engine_for_operation(fixture.engine) + self.assertEqual(fixture.deadlines, [110.0, 110.0]) + self.assertEqual(first.released.waits, [10.0]) + self.assertEqual(successors[0].released.waits, [7.0]) + self.assertEqual(fixture.clock.now, 105.0) + self.assertEqual(fixture.wait_scopes, [0, 0]) + self.assertEqual(fixture.open_calls, 3) + self.assertEqual(fixture.factory_calls, 1) + + def test_timed_out_owner_does_not_reset_budget_or_poisoned_state(self) -> None: + fixture = self.fixture() + origin = fixture.automatic_owner(release_after=20.0) + with self.assertRaises(fixture.maintenance.MaintenanceBusyError): + fixture.runtime.open_engine_for_operation(fixture.engine) + self.assertEqual(origin.released.waits, [10.0]) + self.assertEqual(fixture.clock.now, 110.0) + self.assertEqual(fixture.deadlines, [110.0]) + self.assertEqual(fixture.open_calls, 1) + self.assertEqual(fixture.factory_calls, 0) + self.assertIs(fixture.engine.conn, fixture.handle) + self.assertTrue(fixture.engine.ready) + + def test_initial_attempt_consumes_the_same_budget(self) -> None: + fixture = self.fixture() + origin = fixture.automatic_owner() + opening = fixture.engine._open_connection_handle + + def delayed() -> None: + fixture.clock.now += 3.0 + opening() + + fixture.engine._open_connection_handle = delayed + fixture.runtime.open_engine_for_operation(fixture.engine) + self.assertEqual(origin.released.waits, [7.0]) + self.assertEqual(fixture.deadlines, [110.0]) + + def test_swapped_handle_before_wait_cannot_reuse_cached_failure(self) -> None: + for replacement in (None, Handle()): + with self.subTest(replacement=replacement): + fixture = self.fixture() + origin = fixture.automatic_owner() + + def swap() -> bool: + fixture.engine.conn = replacement + return False + + fixture.runtime.serving.current_thread_admitted = swap + with self.assertRaises(fixture.maintenance.MaintenanceBusyError): + fixture.runtime.open_engine_for_operation(fixture.engine) + self.assertEqual(origin.released.waits, []) + self.assertEqual(fixture.open_calls, 1) + self.assertEqual(fixture.factory_calls, 0) + self.assertIs(fixture.engine.conn, replacement) + + def test_swapped_handle_during_wait_is_rejected_before_retry(self) -> None: + for replacement in (None, Handle()): + with self.subTest(replacement=replacement): + fixture = self.fixture() + origin = fixture.automatic_owner() + release = origin.released.action + + def swap(timeout: float) -> None: + release(timeout) + fixture.engine.conn = replacement + + origin.released.action = swap + with self.assertRaises(fixture.maintenance.MaintenanceBusyError): + fixture.runtime.open_engine_for_operation(fixture.engine) + self.assertEqual(origin.released.waits, [10.0]) + self.assertEqual(fixture.open_calls, 1) + self.assertEqual(fixture.factory_calls, 0) + self.assertIs(fixture.engine.conn, replacement) + + def test_wrong_engine_or_wrong_handle_attestation_never_grants_wait(self) -> None: + for kind in ("engine", "handle"): + with self.subTest(kind=kind): + fixture = self.fixture() + origin = fixture.automatic_owner() + error = fixture.maintenance.MaintenanceBusyError("synthetic stale failure") + error._automatic_owner = origin + error._reconnect_engine = fixture.engine if kind == "handle" else object() + error._reconnect_handle = fixture.handle if kind == "engine" else Handle() + + def reject() -> None: + raise error + + fixture.engine._open_connection_handle = reject + with self.assertRaises(fixture.maintenance.MaintenanceBusyError) as raised: + fixture.runtime.open_engine_for_operation(fixture.engine) + self.assertIs(raised.exception, error) + self.assertEqual(origin.released.waits, []) + self.assertEqual(fixture.wait_scopes, []) + + def test_receiptless_successor_cannot_inherit_previous_origin(self) -> None: + fixture = self.fixture() + origin = fixture.automatic_owner() + release = origin.released.action + + def receiptless(timeout: float) -> None: + release(timeout) + fixture.held_paths[fixture.path] = 991 + fixture.coordinators.clear() + + origin.released.action = receiptless + with self.assertRaises(fixture.maintenance.MaintenanceBusyError) as raised: + fixture.runtime.open_engine_for_operation(fixture.engine) + self.assertIsNone(raised.exception._automatic_owner) + self.assertEqual(origin.released.waits, [10.0]) + self.assertEqual(fixture.deadlines, [110.0, 110.0]) + self.assertEqual(fixture.open_calls, 2) + self.assertEqual(fixture.factory_calls, 0) + + def test_unrelated_sql_failure_and_borrowed_io_error_are_not_retried(self) -> None: + for reason, borrowed in (("other_error", False), ("io_error", True)): + with self.subTest(reason=reason, borrowed=borrowed): + fixture = self.fixture(reason) + fixture.handle.in_transaction = borrowed + origin = fixture.automatic_owner() + with self.assertRaises(sqlite3.OperationalError): + fixture.runtime.open_engine_for_operation(fixture.engine) + self.assertEqual(origin.released.waits, []) + self.assertEqual(fixture.wait_scopes, []) + self.assertEqual(fixture.factory_calls, 0) + self.assertEqual(fixture.open_calls, 1) + + def test_cancelled_origin_during_wait_never_retries(self) -> None: + fixture = self.fixture() + origin = fixture.automatic_owner() + + def cancel(timeout: float) -> None: + fixture.clock.now += 1.0 + origin.cancelled.set() + origin.released.set() + + origin.released.action = cancel + with self.assertRaises(fixture.maintenance.MaintenanceBusyError): + fixture.runtime.open_engine_for_operation(fixture.engine) + self.assertEqual(origin.released.waits, [10.0]) + self.assertEqual(fixture.open_calls, 1) + self.assertEqual(fixture.factory_calls, 0) + self.assertIs(fixture.engine.conn, fixture.handle) + + def test_registry_or_coordinator_contention_does_not_block_wait_eligibility(self) -> None: + for kind in ("registry", "coordinator"): + with self.subTest(kind=kind): + fixture = self.fixture() + origin = fixture.automatic_owner() + lock = fixture.registry if kind == "registry" else fixture.coordinators[fixture.path]._mutex + opening = fixture.engine._open_connection_handle + + def occupied_after_failure() -> None: + try: + opening() + except fixture.maintenance.MaintenanceBusyError: + lock.acquire() + raise + + fixture.engine._open_connection_handle = occupied_after_failure + try: + with self.assertRaises(fixture.maintenance.MaintenanceBusyError): + fixture.runtime.open_engine_for_operation(fixture.engine) + self.assertEqual(origin.released.waits, []) + self.assertEqual(fixture.open_calls, 1) + self.assertEqual(fixture.factory_calls, 0) + finally: + lock.release() + + def test_expired_initial_attempt_never_enters_wait(self) -> None: + fixture = self.fixture() + origin = fixture.automatic_owner() + opening = fixture.engine._open_connection_handle + + def expired() -> None: + fixture.clock.now += 10.0 + opening() + + fixture.engine._open_connection_handle = expired + with self.assertRaises(fixture.maintenance.MaintenanceBusyError): + fixture.runtime.open_engine_for_operation(fixture.engine) + self.assertEqual(origin.released.waits, []) + self.assertEqual(fixture.wait_scopes, []) + self.assertEqual(fixture.factory_calls, 0) + self.assertEqual(fixture.open_calls, 1) + + def test_poisoned_current_writer_refuses_before_actual_nonreentrant_acquire(self) -> None: + self.assertTrue(gc.isenabled()) + for reason in ("closed", "io_error"): + with self.subTest(reason=reason): + fixture = self.fixture(reason, actual_lock=True) + origin = fixture.automatic_owner() + lock = fixture.engine._write_lock + with lock: + with patch.object(lock, "acquire", side_effect=AssertionError("Nonreentrant writer reacquired")) as acquired: + with self.assertRaises(fixture.maintenance.MaintenanceBusyError): + fixture.runtime.open_engine_for_operation(fixture.engine) + acquired.assert_not_called() + self.assertTrue(lock.owned_by_current_thread()) + self.assertIs(fixture.engine.conn, fixture.handle) + self.assertEqual(fixture.open_calls, 0) + self.assertEqual(origin.released.waits, []) + self.assertEqual(fixture.wait_scopes, []) + self.assertEqual(fixture.factory_calls, 0) + + def test_healthy_unowned_actual_writer_reuses_handle_inside_nested_admission(self) -> None: + for admitted in (False, True): + with self.subTest(admitted=admitted): + fixture = self.fixture(None, actual_lock=True) + fixture.admitted = admitted + fixture.local.operation = object() if admitted else None + origin = fixture.automatic_owner() + fixture.runtime.open_engine_for_operation(fixture.engine) + self.assertIs(fixture.engine.conn, fixture.handle) + self.assertEqual(fixture.handle.probes, 1) + self.assertEqual(fixture.open_calls, 1) + self.assertEqual(fixture.factory_calls, 0) + self.assertEqual(origin.released.waits, []) + self.assertFalse(fixture.engine._write_lock.locked()) + self.assertTrue(fixture.engine.ready) + + def test_healthy_borrowed_actual_writer_preserves_unowned_caller_transaction(self) -> None: + for admitted in (False, True): + with self.subTest(admitted=admitted): + fixture = self.fixture(None, actual_lock=True) + fixture.handle.in_transaction = True + fixture.admitted = admitted + fixture.local.operation = object() if admitted else None + origin = fixture.automatic_owner() + fixture.runtime.open_engine_for_operation(fixture.engine) + self.assertTrue(fixture.handle.in_transaction) + self.assertIs(fixture.engine.conn, fixture.handle) + self.assertEqual(fixture.handle.probes, 1) + self.assertEqual(fixture.open_calls, 1) + self.assertEqual(fixture.factory_calls, 0) + self.assertEqual(origin.released.waits, []) + self.assertFalse(fixture.engine._write_lock.locked()) + + def test_current_actual_writer_refuses_before_healthy_or_borrowed_opening(self) -> None: + for borrowed in (False, True): + with self.subTest(borrowed=borrowed): + fixture = self.fixture(None, actual_lock=True) + fixture.handle.in_transaction = borrowed + lock = fixture.engine._write_lock + with lock: + with patch.object(lock, "acquire", side_effect=AssertionError("Owned writer reacquired")) as acquired: + with self.assertRaises(fixture.maintenance.MaintenanceBusyError): + fixture.runtime.open_engine_for_operation(fixture.engine) + acquired.assert_not_called() + self.assertTrue(lock.owned_by_current_thread()) + self.assertEqual(fixture.handle.in_transaction, borrowed) + self.assertEqual(fixture.handle.probes, 0) + self.assertEqual(fixture.open_calls, 0) + self.assertEqual(fixture.factory_calls, 0) + self.assertEqual(fixture.wait_scopes, []) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-tier-runtime-bridge-795.py b/tests/test-tier-runtime-bridge-795.py new file mode 100644 index 00000000..ec796592 --- /dev/null +++ b/tests/test-tier-runtime-bridge-795.py @@ -0,0 +1,1204 @@ +"""Runtime selection with in-memory journals and inert model-slot doubles.""" + +from __future__ import annotations + +import ast +import builtins +import dataclasses +import gc +import os +import runpy +import sqlite3 +import sys +import threading +import types +import unittest +import weakref +from collections.abc import Callable, Iterator +from contextlib import contextmanager, ExitStack +from pathlib import Path +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] +JOURNAL = runpy.run_path(str(Path(__file__).with_name("test-tier-activation-journal-795.py"))) +GATE = runpy.run_path(str(Path(__file__).with_name("test-tier-serving-gate-795.py"))) +PREPARED = runpy.run_path(str(Path(__file__).with_name("test-prepared-target-795.py"))) + + +class Cancelled(BaseException): + pass + + +class TestRuntimeBridge(unittest.TestCase): + def setUp(self) -> None: + self.fixture = JOURNAL["TestActivationJournal"]() + self.addCleanup(self.fixture.doCleanups) + self.fixture.setUp() + self.conn = self.fixture.conn + self.modules = self.fixture.modules + self.gate = GATE["load"]() + self.modules["tier_switch.serving"] = self.gate + self.loads = [] + self.vector = types.SimpleNamespace( + EMBEDDING_MODEL="model2vec", _embedding_dim=256, _model=object(), + _frozen_embedding_target=None, resolve_tier=lambda: "edge", + _active_vec_table=lambda conn: "vec_messages", _active_sep_table=lambda conn: "vec_messages_sep", + ) + self.reranker = types.SimpleNamespace( + _active_tier="edge", _model_name="synthetic/default", _model=object(), _frozen_reranker_id=None, + _model_certified=False, + get_current_reranker_name=lambda: "synthetic/default", + ) + + def embedding(target, **kwargs): + self.assertFalse(self.conn.in_transaction) + self.loads.append(("embedding", target)) + self.vector._frozen_embedding_target = target + self.vector.EMBEDDING_MODEL, self.vector._embedding_dim = target.model_id, target.dimension + self.vector._model = object() + + def reranking(tier, name, **kwargs): + self.assertFalse(self.conn.in_transaction) + self.loads.append(("reranker", name)) + self.reranker._active_tier, self.reranker._model_name = tier, name + self.reranker._frozen_reranker_id = name + self.reranker._model = object() + self.reranker._model_certified = True + + self.vector.apply_frozen_embedding_target = embedding + self.reranker.apply_frozen_reranker = reranking + self.reranker.get_reranker = lambda model_name, **kwargs: reranking("edge", model_name, **kwargs) + self.modules["vector_search"], self.modules["reranker"] = self.vector, self.reranker + + def safe_import(name, globals=None, locals=None, fromlist=(), level=0): + if name in {"numpy", "torch", "sentence_transformers", "model2vec", "sqlite_vec"}: + raise AssertionError("Native/model imports are forbidden in safe bridge tests") + if name == "truememory": + return types.SimpleNamespace(vector_search=self.vector, reranker=self.reranker) + if name == "truememory.tier_switch": + return types.SimpleNamespace(serving=self.gate) + if name.startswith("truememory."): + short = name.removeprefix("truememory.") + if short in self.modules: + return self.modules[short] + raise AssertionError("Unexpected application import: " + name) + return builtins.__import__(name, globals, locals, fromlist, level) + + self.safe_import = safe_import + self.api = types.ModuleType("synthetic_runtime_bridge") + sys.modules[self.api.__name__] = self.api + self.api.__dict__["__builtins__"] = dict(vars(builtins), __import__=safe_import) + path = ROOT / "truememory/tier_switch/runtime.py" + exec(compile(path.read_text(encoding="utf-8"), str(path), "exec"), self.api.__dict__) + self.modules["tier_switch.runtime"] = self.api + self.api._database_identity = lambda conn: ("memory", conn) + + def select(self): + intent, plan = self.fixture.build() + return self.fixture.commit(intent, plan) + + def test_legacy_no_record_stays_lazy_and_bound_none_is_distinct(self) -> None: + self.assertIsNone(self.api.current_operation(self.conn)) + before = (self.conn.commits, self.conn.rollbacks) + with self.api.serving_operation(self.conn) as operation: + self.assertIsNone(operation.selection) + self.assertIs(self.api.current_operation(self.conn), operation) + self.assertEqual(operation.tables, ("vec_messages", "vec_messages_sep")) + self.assertEqual(operation.tier, "edge") + self.assertEqual(self.loads, []) + self.assertEqual((self.conn.commits, self.conn.rollbacks), before) + self.assertIsNone(self.api.current_operation(self.conn)) + + def test_selected_reconciles_exact_identity_before_body_and_ack(self) -> None: + selected = self.select() + with self.api.serving_operation(self.conn) as operation: + self.assertEqual(operation.selection, selected) + self.assertEqual(operation.tables, selected.tables) + self.assertEqual(operation.reranker_id, selected.reranker_id) + self.assertEqual(operation.tier, "custom") + self.assertFalse(self.conn.in_transaction) + self.assertEqual(self.api.runtime_acknowledgement(), self.api.selection_key(selected)) + self.assertEqual(self.loads, [("embedding", selected.target), ("reranker", selected.reranker_id)]) + with self.api.serving_operation(self.conn): + pass + self.assertEqual(len(self.loads), 2) + + def test_projection_rechecks_controls_after_readiness_modules(self) -> None: + selected = self.select() + cancelled = threading.Event() + original = self.api._modules + def modules(): + cancelled.set() + return original() + with patch.object(self.api, "_modules", side_effect=modules): + with self.assertRaises(self.gate.ServingLeaseCancelled): + self.api.apply_frozen_selection(selected, cancelled=cancelled) + self.assertEqual(self.loads, []) + self.assertIsNone(self.api.runtime_acknowledgement()) + + def test_legacy_explicit_override_forwards_admission_controls(self) -> None: + cancelled = threading.Event() + observed = [] + original = self.reranker.get_reranker + def loader(**kwargs): + observed.append(kwargs) + return original(**kwargs) + with patch.object(self.reranker, "get_reranker", side_effect=loader): + with self.api.serving_operation(self.conn, reranker_id="synthetic/deep", cancelled=cancelled): + pass + self.assertEqual(observed, [{"model_name": "synthetic/deep", "deadline": None, "cancelled": cancelled}]) + + def test_legacy_optional_reranker_failure_preserves_bound_retrieval(self) -> None: + for error in (OSError("synthetic unavailable"), ImportError("synthetic missing")): + with self.subTest(error=type(error).__name__): + with patch.object(self.reranker, "get_reranker", side_effect=error) as loader: + with self.api.serving_operation(self.conn, reranker_id="synthetic/deep") as operation: + self.assertEqual(operation.reranker_id, "synthetic/deep") + self.assertIs(self.api.current_operation(self.conn), operation) + self.assertIsNone(operation.selection) + self.assertIsNone(operation.policy) + loader.assert_called_once() + self.assertIsNone(self.api.current_operation(self.conn)) + self.assertFalse(self.conn.in_transaction) + + def test_legacy_reranker_identity_and_control_refusals_remain_hard(self) -> None: + for error in (self.api.TierRuntimeError("identity"), self.gate.ServingLeaseError("admission"), + self.gate.ServingLeaseTimeout("timeout"), self.gate.ServingLeaseCancelled("cancelled")): + with self.subTest(error=type(error).__name__): + with patch.object(self.reranker, "get_reranker", side_effect=error): + with self.assertRaises(type(error)): + with self.api.serving_operation(self.conn, reranker_id="synthetic/deep"): + self.fail("Typed refusal must prevent retrieval") + self.assertIsNone(self.api.current_operation(self.conn)) + + def test_borrowed_legacy_uncached_reranker_never_attempts_load(self) -> None: + self.conn.execute("BEGIN") + before = (self.conn.commits, self.conn.rollbacks) + with patch.object(self.reranker, "get_reranker", side_effect=OSError("synthetic unavailable")) as loader: + with self.assertRaises(self.api.TierRuntimeError): + with self.api.serving_operation(self.conn, reranker_id="synthetic/deep"): + self.fail("Borrowed SQL cannot load an uncached reranker") + loader.assert_not_called() + self.assertTrue(self.conn.in_transaction) + self.assertEqual((self.conn.commits, self.conn.rollbacks), before) + self.conn.rollback() + + def test_selected_reranker_preparation_failure_never_enters_body(self) -> None: + self.select() + with patch.object(self.reranker, "apply_frozen_reranker", side_effect=OSError("synthetic unavailable")): + with self.assertRaises(OSError): + with self.api.serving_operation(self.conn, reranker_id="synthetic/deep"): + self.fail("Selected certification is required before retrieval") + self.assertIsNone(self.api.current_operation(self.conn)) + self.assertIsNone(self.api.runtime_acknowledgement()) + + def test_nested_search_has_zero_selection_sql_and_reuses_context(self) -> None: + self.select() + queries = [] + self.conn.set_trace_callback(queries.append) + with self.api.serving_operation(self.conn) as outer: + before = len(queries) + with self.api.serving_operation(self.conn) as inner: + self.assertIs(inner, outer) + self.assertIs(self.api.current_operation(self.conn), outer) + self.assertEqual(len(queries), before) + + def test_outer_override_is_frozen_without_changing_journal(self) -> None: + selected = self.select() + with self.api.serving_operation(self.conn, reranker_id="synthetic/deep") as operation: + self.assertEqual(operation.reranker_id, "synthetic/deep") + self.assertEqual(operation.selection.reranker_id, selected.reranker_id) + self.assertEqual(self.api.runtime_acknowledgement(), self.api.selection_key(selected, "synthetic/deep")) + with self.api.serving_operation(self.conn) as nested: + self.assertIs(nested, operation) + with self.assertRaises(self.api.TierRuntimeError): + with self.api.serving_operation(self.conn, reranker_id="synthetic/other"): + self.fail("Nested override cannot change the admitted model") + self.assertEqual(self.fixture.api.read_activation_state(self.conn).selection, selected) + + def test_same_thread_projection_upgrade_refuses(self) -> None: + selected = self.select() + with self.api.serving_operation(self.conn): + with self.assertRaises(self.api.TierRuntimeError): + self.api.apply_frozen_selection(selected) + + def test_projection_reenters_existing_exclusive_guard(self) -> None: + selected = self.select() + with self.gate.exclusive_activation(): + self.api.apply_frozen_selection(selected) + self.assertEqual(self.api.runtime_acknowledgement(), self.api.selection_key(selected)) + + def test_loader_failure_clears_ack_and_preserves_exception(self) -> None: + selected = self.select() + self.api.apply_frozen_selection(selected) + changed = dataclasses.replace(selected, generation="f" * 32) + for module, name in ((self.vector, "apply_frozen_embedding_target"), (self.reranker, "apply_frozen_reranker")): + sentinel = Cancelled("synthetic") + self.api._acknowledged = self.api.selection_key(selected) + with patch.object(module, name, side_effect=sentinel): + with self.assertRaises(Cancelled) as raised: + self.api.apply_frozen_selection(changed) + self.assertIs(raised.exception, sentinel) + self.assertIsNone(self.api.runtime_acknowledgement()) + + def test_body_baseexception_clears_context_and_gate(self) -> None: + sentinel = Cancelled("synthetic") + with self.assertRaises(Cancelled) as raised: + with self.api.serving_operation(self.conn): + raise sentinel + self.assertIs(raised.exception, sentinel) + self.assertIsNone(self.api.current_operation(self.conn)) + with self.gate.exclusive_activation(timeout=1): + pass + + def test_bound_different_connection_refuses_even_legacy_none(self) -> None: + other = sqlite3.connect(":memory:") + self.addCleanup(other.close) + with self.api.serving_operation(self.conn): + with self.assertRaises(self.api.TierRuntimeError): + self.api.current_operation(other) + with self.assertRaises(self.api.TierRuntimeError): + with self.api.serving_operation(other): + self.fail("Different connection cannot inherit the operation") + + def test_borrowed_legacy_is_nonblocking_and_never_ends_sql(self) -> None: + self.conn.execute("BEGIN") + before = (self.conn.commits, self.conn.rollbacks) + with self.api.serving_operation(self.conn) as operation: + self.assertTrue(operation.borrowed) + with self.assertRaises(self.api.TierRuntimeError): + self.api.require_model_load_allowed() + self.assertTrue(self.conn.in_transaction) + self.assertEqual((self.conn.commits, self.conn.rollbacks), before) + self.assertEqual(self.loads, []) + self.conn.rollback() + + def test_borrowed_selected_requires_existing_ready_runtime(self) -> None: + selected = self.select() + self.conn.execute("BEGIN") + before = (self.conn.commits, self.conn.rollbacks) + with self.assertRaises(self.api.TierRuntimeError): + with self.api.serving_operation(self.conn): + self.fail("Reconciliation cannot load in a borrowed transaction") + self.assertEqual(self.loads, []) + self.assertEqual((self.conn.commits, self.conn.rollbacks), before) + self.conn.rollback() + self.api.apply_frozen_selection(selected) + self.conn.execute("BEGIN") + before = (self.conn.commits, self.conn.rollbacks) + with self.api.serving_operation(self.conn): + pass + self.assertTrue(self.conn.in_transaction) + self.assertEqual((self.conn.commits, self.conn.rollbacks), before) + self.conn.rollback() + + def test_selected_admission_never_lazy_loads_inside_body(self) -> None: + self.select() + with self.api.serving_operation(self.conn): + with self.assertRaises(self.api.TierRuntimeError): + self.api.require_model_load_allowed() + + def test_child_reservation_outlives_parent_and_clears_context(self) -> None: + with self.api.serving_operation(self.conn) as parent: + child = parent.fork_child() + with child.join() as operation: + self.assertIs(self.api.current_operation(self.conn), operation) + self.assertEqual(operation.tables, parent.tables) + self.assertIsNone(self.api.current_operation(self.conn)) + with self.assertRaises(self.gate.ServingLeaseError): + with child.join(): + self.fail("Consumed child cannot join twice") + + def test_child_different_database_refuses_and_releases_admitted_pin(self) -> None: + other = sqlite3.connect(":memory:") + self.addCleanup(other.close) + with self.api.serving_operation(self.conn) as parent: + child = parent.fork_child() + with self.assertRaises(self.api.TierRuntimeError): + with child.join(other): + self.fail("Different database cannot join") + with self.gate.exclusive_activation(timeout=1): + pass + + def test_child_explicit_matching_database_binds_own_clean_connection(self) -> None: + other = sqlite3.connect(":memory:") + self.addCleanup(other.close) + self.api._database_identity = lambda conn: ("file", "synthetic-same-db") + with self.api.serving_operation(self.conn) as parent: + child = parent.fork_child() + with child.join(other) as operation: + self.assertIs(self.api.current_operation(other), operation) + with self.assertRaises(self.api.TierRuntimeError): + self.api.current_operation(self.conn) + + def test_forked_process_ack_is_not_ready(self) -> None: + selected = self.select() + self.api.apply_frozen_selection(selected) + with patch.object(self.api.os, "getpid", return_value=self.api._pid + 1): + self.assertIsNone(self.api.runtime_acknowledgement()) + + def test_nonblocking_admission_attempts_mutex_once(self) -> None: + gate = self.gate.ServingGate() + lock = types.SimpleNamespace(acquire=lambda **kwargs: False) + with patch.object(gate, "_condition", lock): + with self.assertRaises(self.gate.ServingLeaseError): + with gate.operation_lease(("synthetic",), blocking=False): + self.fail("Unavailable mutex cannot admit") + with self.assertRaises(self.gate.ServingLeaseError): + with gate.operation_lease(("synthetic",), blocking=0): + self.fail("Only exact bool is accepted") + + def test_nonblocking_conflict_never_condition_waits(self) -> None: + gate = self.gate.ServingGate() + ticket = object() + gate._waiting_writers.append(ticket) + with patch.object(gate._condition, "wait", side_effect=AssertionError("Admission cannot wait")): + with self.assertRaises(self.gate.ServingLeaseError): + with gate.operation_lease(("synthetic",), blocking=False): + self.fail("Queued writer must block unrelated reader") + gate._waiting_writers.clear() + self.assertFalse(gate._readers) + + def test_nonblocking_nested_read_inherits_cancel_and_identity(self) -> None: + gate = self.gate.ServingGate() + cancelled = threading.Event() + with gate.operation_lease(("synthetic",), cancelled=cancelled): + with gate.operation_lease(("synthetic",), blocking=False): + pass + with self.assertRaises(self.gate.ServingLeaseError): + with gate.operation_lease(("different",), blocking=False): + self.fail("Nested identity cannot change") + cancelled.set() + with self.assertRaises(self.gate.ServingLeaseCancelled): + with gate.operation_lease(("synthetic",), blocking=False): + self.fail("Inherited cancellation precedes admission") + + def test_engine_and_hybrid_outer_boundaries_are_present(self) -> None: + engine = ast.parse((ROOT / "truememory/engine.py").read_text(encoding="utf-8")) + klass = next(node for node in engine.body if isinstance(node, ast.ClassDef) and node.name == "TrueMemoryEngine") + for name in ("search", "search_agentic", "search_vectors_raw", "add", "update", "delete", "delete_all"): + node = next(node for node in klass.body if isinstance(node, ast.FunctionDef) and node.name == name) + self.assertIn("engine_operation", [ast.unparse(value) for value in node.decorator_list]) + hybrid = ast.parse((ROOT / "truememory/hybrid.py").read_text(encoding="utf-8")) + function = next(node for node in hybrid.body if isinstance(node, ast.FunctionDef) and node.name == "search_hybrid") + self.assertIn("database_operation", [ast.unparse(value) for value in function.decorator_list]) + + def test_cancellation_during_final_journal_read_refuses_body(self) -> None: + self.select() + event = threading.Event() + original = self.api._read_selection + calls = [] + def read(conn): + value = original(conn) + calls.append(value) + if len(calls) == 2: + event.set() + return value + with patch.object(self.api, "_read_selection", side_effect=read): + with self.assertRaises(self.gate.ServingLeaseCancelled): + with self.api.serving_operation(self.conn, cancelled=event): + self.fail("Cancellation after SQL must prevent body admission") + self.assertIsNone(self.api.current_operation(self.conn)) + with self.gate.exclusive_activation(timeout=1): + pass + + def test_deadline_consumed_by_final_journal_read_refuses_body(self) -> None: + self.select() + clock = [1000.0] + original = self.api._read_selection + calls = [] + def read(conn): + value = original(conn) + calls.append(value) + if len(calls) == 2: + clock[0] = 1002.0 + return value + with patch.object(self.api.time, "monotonic", side_effect=lambda: clock[0]), patch.object(self.api, "_read_selection", side_effect=read): + with self.assertRaises(self.gate.ServingLeaseTimeout): + with self.api.serving_operation(self.conn, deadline=1001.0): + self.fail("Expired deadline after SQL must prevent body admission") + + def test_child_rechecks_inherited_event_after_new_connection_read(self) -> None: + other = sqlite3.connect(":memory:") + self.addCleanup(other.close) + self.api._database_identity = lambda conn: ("file", "synthetic-same-db") + event = threading.Event() + with self.api.serving_operation(self.conn, cancelled=event) as operation: + child = operation.fork_child() + original = self.api._read_selection + def read(conn): + value = original(conn) + event.set() + return value + with patch.object(self.api, "_read_selection", side_effect=read): + with self.assertRaises(self.gate.ServingLeaseCancelled): + with child.join(other): + self.fail("Child must enforce inherited cancellation after SQL") + self.assertIsNone(self.api.current_operation(other)) + with self.gate.exclusive_activation(timeout=1): + pass + + def test_nested_reservation_inherits_stricter_nested_control(self) -> None: + event = threading.Event() + with self.api.serving_operation(self.conn): + with self.api.serving_operation(self.conn, cancelled=event) as operation: + child = operation.fork_child() + event.set() + with self.assertRaises(self.gate.ServingLeaseCancelled): + with child.join(): + self.fail("Nested control must reach the forked child") + child.close() + + def test_actual_selected_engine_init_reconciles_before_validation_and_keeps_schedule(self) -> None: + selected = self.select() + tree = ast.parse((ROOT / "truememory/engine.py").read_text(encoding="utf-8")) + source = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == "TrueMemoryEngine") + source.body = [node for node in source.body if isinstance(node, ast.FunctionDef) and node.name in { + "_ensure_connection", "_initialize_connection", "_open_connection_handle", "close", + }] + calls = [] + def validate(conn): + self.assertEqual(self.api.current_operation(conn).selection, selected) + self.assertEqual(self.api.runtime_acknowledgement(), self.api.selection_key(selected)) + calls.append("validate") + def imports(name, globals=None, locals=None, fromlist=(), level=0): + if name == "sqlite_vec": + return types.SimpleNamespace(load=lambda conn: calls.append("extension")) + return self.safe_import(name, globals, locals, fromlist, level) + namespace = { + "__builtins__": dict(vars(builtins), __import__=imports), + "_HAS_VECTOR": True, "_HAS_HYBRID": True, "init_vec_table": validate, "sqlite3": sqlite3, + } + exec(compile(ast.fix_missing_locations(ast.Module(body=[source], type_ignores=[])), "synthetic-engine-init", "exec"), namespace) + engine = namespace["TrueMemoryEngine"]() + engine.conn = self.conn + self.conn.enable_load_extension = lambda enabled: None + engine._write_lock = threading.Lock() + engine._init_lock = threading.Lock() + engine._runtime_initialized = False + engine._open_connection_handle = lambda: calls.append("plain-open") + engine._purge_legacy_entity_profile_summaries = lambda: calls.append("purge") + engine._maybe_startup_consolidate = lambda: calls.append("startup-schedule") + engine._maybe_auto_consolidate = lambda: calls.append("cached-schedule") + engine._ensure_connection() + self.assertEqual(calls, ["plain-open", "extension", "validate", "purge", "startup-schedule"]) + self.assertTrue(engine._has_vectors) + self.assertTrue(engine._has_hybrid) + engine._ensure_connection() + self.assertEqual(calls[-2:], ["plain-open", "cached-schedule"]) + replacement = sqlite3.connect(":memory:", factory=JOURNAL["JournalConnection"]) + self.addCleanup(replacement.close) + self.conn.backup(replacement) + replacement.enable_load_extension = lambda enabled: None + namespace["create_db"] = lambda path: replacement + engine.db_path = Path(":memory:") + engine._open_connection_handle = types.MethodType(namespace["TrueMemoryEngine"]._open_connection_handle, engine) + self.conn.close() + calls.clear() + engine._ensure_connection() + self.assertEqual(calls, ["extension", "validate", "purge", "startup-schedule"]) + self.assertIs(engine._runtime_vector_connection, replacement) + self.assertTrue(engine._runtime_initialized) + self.assertTrue(engine._has_vectors) + self.assertTrue(engine.ready) + engine.close() + self.assertIsNone(engine._runtime_vector_connection) + self.assertIsNone(engine._runtime_vector_generation) + self.assertFalse(engine._runtime_initialized) + + + def test_ready_legacy_borrowed_connection_does_not_reinitialize(self) -> None: + tree = ast.parse((ROOT / "truememory/engine.py").read_text(encoding="utf-8")) + source = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == "TrueMemoryEngine") + source.body = [node for node in source.body if isinstance(node, ast.FunctionDef) and node.name == "_initialize_connection"] + namespace = {"__builtins__": dict(vars(builtins), __import__=self.safe_import)} + exec(compile(ast.fix_missing_locations(ast.Module(body=[source], type_ignores=[])), "synthetic-legacy-ready", "exec"), namespace) + engine = namespace["TrueMemoryEngine"]() + engine.conn, engine.ready, engine._runtime_initialized = self.conn, True, False + engine._init_lock = threading.Lock() + engine._write_lock = threading.Lock() + self.conn.execute("INSERT INTO messages(id,content) VALUES(901,'synthetic borrowed legacy')") + before = self.conn.commits, self.conn.rollbacks + with self.api.serving_operation(self.conn): + engine._initialize_connection(_suppress_maintenance=True) + self.assertTrue(self.conn.in_transaction) + self.assertEqual((self.conn.commits, self.conn.rollbacks), before) + engine.ready = False + with self.assertRaises(self.api.TierRuntimeError): + engine._initialize_connection(_suppress_maintenance=True) + + def test_fresh_or_closed_handle_clears_legacy_ready_before_initialization(self) -> None: + tree = ast.parse((ROOT / "truememory/engine.py").read_text(encoding="utf-8")) + source = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == "TrueMemoryEngine") + source.body = [node for node in source.body if isinstance(node, ast.FunctionDef) and node.name == "_open_connection_handle"] + replacement = sqlite3.connect(":memory:") + self.addCleanup(replacement.close) + namespace = {"sqlite3": sqlite3, "create_db": lambda path: replacement, + "__builtins__": dict(vars(builtins), __import__=self.safe_import)} + exec(compile(ast.fix_missing_locations(ast.Module(body=[source], type_ignores=[])), "synthetic-legacy-reopen", "exec"), namespace) + engine = namespace["TrueMemoryEngine"]() + engine.db_path, engine._init_lock = Path(":memory:"), threading.Lock() + engine._write_lock = threading.Lock() + for prior in (None, self.conn): + if prior is not None: + prior.close() + engine.conn, engine.ready, engine._runtime_initialized = prior, True, True + engine._open_connection_handle() + self.assertIs(engine.conn, replacement) + self.assertFalse(engine.ready) + self.assertFalse(engine._runtime_initialized) + + def test_managed_writer_transaction_waits_and_is_not_caller_borrowed(self) -> None: + conn = sqlite3.connect(":memory:", check_same_thread=False) + self.addCleanup(conn.close) + conn.execute("CREATE TABLE synthetic(value TEXT)") + class Engine: + pass + engine = Engine() + engine.conn = conn + waiting, release, owned, entered = (threading.Event() for _ in range(4)) + failures = [] + api = self.api + class ObservedLock(api.ConnectionWriteLock): + def acquire(self, blocking=True, timeout=-1): + if blocking and self.owns_other_transaction(conn): + waiting.set() + return super().acquire(blocking, timeout) + lock = ObservedLock(engine) + def writer(): + try: + with lock: + conn.execute("INSERT INTO synthetic VALUES('owned')") + owned.set() + if not release.wait(2): + raise AssertionError("Synthetic writer release timed out") + conn.commit() + except BaseException as exc: + failures.append(exc) + def reader(): + try: + with api._connection_read(conn, lock, api.time.monotonic() + 2, None): + self.assertFalse(conn.in_transaction) + self.assertEqual(conn.execute("SELECT value FROM synthetic").fetchall(), [("owned",)]) + entered.set() + except BaseException as exc: + failures.append(exc) + owner = threading.Thread(target=writer) + contender = threading.Thread(target=reader) + owner.start() + try: + self.assertTrue(owned.wait(2)) + contender.start() + self.assertTrue(waiting.wait(2)) + self.assertFalse(entered.is_set()) + finally: + release.set() + owner.join(2) + if contender.ident is not None: + contender.join(2) + self.assertFalse(owner.is_alive()) + self.assertFalse(contender.is_alive()) + self.assertEqual(failures, []) + self.assertTrue(entered.is_set()) + + def test_managed_lock_does_not_adopt_preexisting_caller_transaction(self) -> None: + conn = sqlite3.connect(":memory:", check_same_thread=False) + self.addCleanup(conn.close) + conn.execute("CREATE TABLE synthetic(value TEXT)") + conn.execute("INSERT INTO synthetic VALUES('caller')") + class Engine: + pass + engine = Engine() + engine.conn = conn + lock = self.api.ConnectionWriteLock(engine) + owned, release = threading.Event(), threading.Event() + failures = [] + def holder(): + try: + with lock: + owned.set() + if not release.wait(2): + raise AssertionError("Synthetic holder release timed out") + except BaseException as exc: + failures.append(exc) + thread = threading.Thread(target=holder) + thread.start() + try: + self.assertTrue(owned.wait(2)) + with self.assertRaisesRegex(self.api.TierRuntimeError, "Borrowed connection admission is busy"): + with self.api._connection_read(conn, lock, None, None): + self.fail("Caller SQL must not wait for an unrelated lock holder") + self.assertTrue(conn.in_transaction) + self.assertEqual(conn.execute("SELECT value FROM synthetic").fetchall(), [("caller",)]) + finally: + release.set() + thread.join(2) + self.assertFalse(thread.is_alive()) + self.assertEqual(failures, []) + conn.rollback() + self.assertEqual(conn.execute("SELECT value FROM synthetic").fetchall(), []) + + def test_connection_probe_error_preserves_handle_under_both_locks(self) -> None: + tree = ast.parse((ROOT / "truememory/engine.py").read_text(encoding="utf-8")) + source = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == "TrueMemoryEngine") + source.body = [node for node in source.body if isinstance(node, ast.FunctionDef) + and node.name == "_open_connection_handle"] + namespace = {"sqlite3": sqlite3, + "__builtins__": dict(vars(builtins), __import__=self.safe_import)} + exec(compile(ast.fix_missing_locations(ast.Module(body=[source], type_ignores=[])), + "synthetic-probe-failure", "exec"), namespace) + engine = namespace["TrueMemoryEngine"]() + engine._init_lock, engine._write_lock = threading.Lock(), threading.Lock() + owner = self + class Connection: + in_transaction = False + def execute(self, sql): + owner.assertTrue(engine._init_lock.locked()) + owner.assertTrue(engine._write_lock.locked()) + raise sqlite3.OperationalError("synthetic transient probe failure") + def close(self): + owner.fail("A transient SQL error must not close a shared connection") + engine.conn = Connection() + original = engine.conn + with self.assertRaisesRegex(sqlite3.OperationalError, "synthetic transient"): + engine._open_connection_handle() + self.assertIs(engine.conn, original) + self.assertFalse(engine._init_lock.locked()) + self.assertFalse(engine._write_lock.locked()) + + + def lifecycle_engine( + self, method: str, conn: sqlite3.Connection | None = None, + create_db: Callable[[Path], sqlite3.Connection] | None = None, + ) -> object: + tree = ast.parse((ROOT / "truememory/engine.py").read_text(encoding="utf-8")) + source = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == "TrueMemoryEngine") + source.body = [node for node in source.body if isinstance(node, ast.FunctionDef) and node.name == method] + namespace = {"sqlite3": sqlite3, "create_db": create_db, + "__builtins__": dict(vars(builtins), __import__=self.safe_import)} + exec(compile(ast.fix_missing_locations(ast.Module(body=[source], type_ignores=[])), + "synthetic-engine-lifecycle", "exec"), namespace) + engine = namespace["TrueMemoryEngine"]() + engine.conn = self.conn if conn is None else conn + engine.db_path = Path(":memory:") + engine._init_lock = threading.Lock() + engine._write_lock = self.api.ConnectionWriteLock(engine) + engine.ready = engine._has_vectors = engine._runtime_initialized = True + engine._runtime_vector_generation = "synthetic-initialized" + engine._runtime_vector_connection = engine.conn + return engine + + @contextmanager + def held_lifecycle_lock(self, lock: object) -> Iterator[None]: + entered, release = threading.Event(), threading.Event() + failures = [] + + def holder() -> None: + try: + with lock: + entered.set() + if not release.wait(2): + raise AssertionError("Synthetic lifecycle holder release timed out") + except BaseException as exc: + failures.append(exc) + + thread = threading.Thread(target=holder) + thread.start() + try: + self.assertTrue(entered.wait(2)) + yield + finally: + release.set() + thread.join(2) + self.assertFalse(thread.is_alive()) + self.assertEqual(failures, []) + + def test_actual_open_borrowed_busy_preserves_caller_sql_at_each_lock(self) -> None: + engine = self.lifecycle_engine("_open_connection_handle") + self.conn.execute("INSERT INTO messages(id,content) VALUES(901,'synthetic caller SQL')") + before = self.conn.commits, self.conn.rollbacks + queries = [] + self.conn.set_trace_callback(queries.append) + for lock in (engine._init_lock, engine._write_lock): + with self.subTest(lock=type(lock).__name__): + queries.clear() + with self.held_lifecycle_lock(lock): + with self.assertRaisesRegex(self.api.TierRuntimeError, "Borrowed connection admission is busy"): + engine._open_connection_handle() + self.assertEqual(queries, []) + self.assertIs(engine.conn, self.conn) + self.assertTrue(self.conn.in_transaction) + self.assertEqual((self.conn.commits, self.conn.rollbacks), before) + self.assertEqual(self.conn.execute("SELECT content FROM messages WHERE id=901").fetchone()[0], + "synthetic caller SQL") + self.assertFalse(engine._init_lock.locked()) + self.assertFalse(engine._write_lock.locked()) + self.conn.rollback() + self.assertEqual(self.conn.execute("SELECT count(*) FROM messages WHERE id=901").fetchone()[0], 0) + + def test_actual_open_clean_ioerr_reconnects_and_invalidates_runtime(self) -> None: + coded = sqlite3.OperationalError("synthetic extended I/O error") + coded.sqlite_errorcode = getattr(sqlite3, "SQLITE_IOERR", 10) | (7 << 8) + for error in (sqlite3.OperationalError("disk I/O error"), coded): + with self.subTest(error=str(error)): + original = sqlite3.connect(":memory:", factory=JOURNAL["JournalConnection"]) + replacement = sqlite3.connect(":memory:") + self.addCleanup(original.close) + self.addCleanup(replacement.close) + original.armed, original.fault_contains, original.fault = True, "PRAGMA schema_version", error + opened = [] + + def create(path: Path) -> sqlite3.Connection: + self.assertEqual(path, Path(":memory:")) + opened.append(path) + return replacement + + engine = self.lifecycle_engine("_open_connection_handle", original, create) + with patch.object(original, "close", wraps=original.close) as close: + engine._open_connection_handle() + close.assert_called_once_with() + self.assertEqual(opened, [Path(":memory:")]) + self.assertIs(engine.conn, replacement) + self.assertFalse(engine.ready) + self.assertFalse(engine._has_vectors) + self.assertFalse(engine._runtime_initialized) + self.assertIsNone(engine._runtime_vector_generation) + self.assertIsNone(engine._runtime_vector_connection) + self.assertFalse(engine._init_lock.locked()) + self.assertFalse(engine._write_lock.locked()) + + def test_actual_open_borrowed_ioerr_preserves_handle_and_caller_sql(self) -> None: + coded = sqlite3.OperationalError("synthetic extended I/O error") + coded.sqlite_errorcode = getattr(sqlite3, "SQLITE_IOERR", 10) | (7 << 8) + for error in (sqlite3.OperationalError("disk I/O error"), coded): + with self.subTest(error=str(error)): + engine = self.lifecycle_engine("_open_connection_handle") + self.conn.execute("INSERT INTO messages(id,content) VALUES(901,'synthetic caller SQL')") + before = self.conn.commits, self.conn.rollbacks + self.conn.armed, self.conn.fault_contains, self.conn.fault = True, "PRAGMA schema_version", error + with patch.object(self.conn, "close", wraps=self.conn.close) as close: + with self.assertRaises(sqlite3.OperationalError) as raised: + engine._open_connection_handle() + close.assert_not_called() + self.assertIs(raised.exception, error) + self.assertIs(engine.conn, self.conn) + self.assertTrue(self.conn.in_transaction) + self.assertEqual((self.conn.commits, self.conn.rollbacks), before) + self.assertTrue(engine.ready) + self.assertTrue(engine._has_vectors) + self.assertTrue(engine._runtime_initialized) + self.assertEqual(engine._runtime_vector_generation, "synthetic-initialized") + self.assertIs(engine._runtime_vector_connection, self.conn) + self.assertFalse(engine._init_lock.locked()) + self.assertFalse(engine._write_lock.locked()) + self.assertEqual(self.conn.execute("SELECT content FROM messages WHERE id=901").fetchone()[0], + "synthetic caller SQL") + self.conn.armed = False + self.conn.rollback() + self.assertEqual(self.conn.execute("SELECT count(*) FROM messages WHERE id=901").fetchone()[0], 0) + + def test_actual_open_extended_ioerr_without_sqlite_constant(self) -> None: + class SQLiteWithoutIOErr: + missing_constant_reads = 0 + + def __getattr__(self, name: str) -> object: + if name == "SQLITE_IOERR": + self.missing_constant_reads += 1 + raise AttributeError(name) + return getattr(sqlite3, name) + + for borrowed in (False, True): + with self.subTest(borrowed=borrowed): + sqlite_api = SQLiteWithoutIOErr() + self.assertIs(sqlite_api.connect, sqlite3.connect) + self.assertIs(sqlite_api.OperationalError, sqlite3.OperationalError) + original = sqlite3.connect(":memory:", factory=JOURNAL["JournalConnection"]) + replacement = sqlite3.connect(":memory:") + self.addCleanup(original.close) + self.addCleanup(replacement.close) + original.execute("CREATE TABLE synthetic(value TEXT)") + if borrowed: + original.execute("INSERT INTO synthetic VALUES('synthetic caller SQL')") + before = original.commits, original.rollbacks + error = sqlite3.OperationalError("synthetic extended I/O error") + error.sqlite_errorcode = 10 | (7 << 8) + original.armed, original.fault_contains, original.fault = True, "PRAGMA schema_version", error + opened = [] + + def create(path: Path) -> sqlite3.Connection: + self.assertEqual(path, Path(":memory:")) + opened.append(path) + return replacement + + engine = self.lifecycle_engine("_open_connection_handle", original, create) + engine._open_connection_handle.__globals__["sqlite3"] = sqlite_api + with patch.object(original, "close", wraps=original.close) as close: + if borrowed: + with self.assertRaises(sqlite3.OperationalError) as raised: + engine._open_connection_handle() + self.assertIs(raised.exception, error) + close.assert_not_called() + self.assertEqual(opened, []) + self.assertIs(engine.conn, original) + self.assertTrue(original.in_transaction) + self.assertTrue(engine.ready) + self.assertTrue(engine._has_vectors) + self.assertTrue(engine._runtime_initialized) + self.assertEqual(engine._runtime_vector_generation, "synthetic-initialized") + self.assertIs(engine._runtime_vector_connection, original) + self.assertEqual(original.execute("SELECT value FROM synthetic").fetchall(), + [("synthetic caller SQL",)]) + else: + engine._open_connection_handle() + close.assert_called_once_with() + self.assertEqual(opened, [Path(":memory:")]) + self.assertIs(engine.conn, replacement) + self.assertFalse(engine.ready) + self.assertFalse(engine._has_vectors) + self.assertFalse(engine._runtime_initialized) + self.assertIsNone(engine._runtime_vector_generation) + self.assertIsNone(engine._runtime_vector_connection) + self.assertEqual(sqlite_api.missing_constant_reads, 1) + self.assertEqual((original.commits, original.rollbacks), before) + self.assertFalse(engine._init_lock.locked()) + self.assertFalse(engine._write_lock.locked()) + if borrowed: + original.armed = False + original.rollback() + self.assertEqual(original.execute("SELECT count(*) FROM synthetic").fetchone()[0], 0) + + def test_actual_selected_ready_borrowed_init_ignores_competing_writer(self) -> None: + selected = self.select() + self.api.apply_frozen_selection(selected) + engine = self.lifecycle_engine("_initialize_connection") + engine._runtime_vector_generation = selected.generation + self.conn.execute("INSERT INTO messages(id,content) VALUES(901,'synthetic caller SQL')") + before = self.conn.commits, self.conn.rollbacks + loads = list(self.loads) + queries = [] + self.conn.set_trace_callback(queries.append) + with self.api.serving_operation(self.conn, connection_lock=engine._write_lock) as operation: + self.assertTrue(operation.borrowed) + with self.held_lifecycle_lock(engine._write_lock): + queries.clear() + with patch.object(self.api, "_connection_read", side_effect=AssertionError("Ready init must not acquire SQL locks")), \ + patch.object(self.api, "apply_frozen_selection", side_effect=AssertionError("Ready init must not reload models")): + engine._initialize_connection(_suppress_maintenance=True) + self.assertTrue(engine._write_lock.locked()) + self.assertEqual(queries, []) + self.assertEqual(self.loads, loads) + self.assertTrue(self.conn.in_transaction) + self.assertEqual((self.conn.commits, self.conn.rollbacks), before) + self.assertIs(self.api.current_operation(self.conn), operation) + self.assertTrue(engine.ready) + self.assertEqual(self.conn.execute("SELECT content FROM messages WHERE id=901").fetchone()[0], + "synthetic caller SQL") + self.conn.rollback() + self.assertEqual(self.conn.execute("SELECT count(*) FROM messages WHERE id=901").fetchone()[0], 0) + + +class TestRuntimeModels(unittest.TestCase): + def setUp(self) -> None: + self.fixture = PREPARED["TestPreparedTargets"]() + self.addCleanup(self.fixture.doCleanups) + self.fixture.setUp() + self.vector = self.fixture.vector + self.vector._frozen_embedding_target = None + self.gate = GATE["load"]() + self.operation = None + self.runtime = types.SimpleNamespace( + require_model_load_allowed=lambda: None, current_runtime_operation=lambda: self.operation, + TierRuntimeError=RuntimeError, + ) + package = types.ModuleType("truememory.tier_switch") + package.__path__ = [] + modules = patch.dict(sys.modules, { + "truememory.tier_switch": package, + "truememory.tier_switch.serving": self.gate, + "truememory.tier_switch.runtime": self.runtime, + }) + modules.start() + self.addCleanup(modules.stop) + + def test_actual_local_factory_releases_old_slot_before_constructor(self) -> None: + class Model: + pass + self.vector._model = Model() + old = weakref.ref(self.vector._model) + def check(): + gc.collect() + self.assertIsNone(old()) + self.assertIsNone(self.vector._model) + self.fixture.build_hook = check + self.vector.apply_frozen_embedding_target(self.fixture.target) + self.assertEqual(self.vector.EMBEDDING_MODEL, "qwen3_256") + self.assertEqual(self.vector._embedding_dim, 256) + self.assertEqual(len(self.fixture.calls), 1) + self.assertEqual(self.fixture.encoded[0][1], [self.fixture.target_module.TARGET_PROBE_TEXT]) + self.assertIsNotNone(self.vector._model) + + def test_failed_constructor_keeps_exact_target_and_empty_slot(self) -> None: + sentinel = Cancelled("synthetic") + def fail(): + raise sentinel + self.fixture.build_hook = fail + with self.assertRaises(Cancelled) as raised: + self.vector.apply_frozen_embedding_target(self.fixture.target) + self.assertIs(raised.exception, sentinel) + self.assertIsNone(self.vector._model) + self.assertEqual(self.vector._frozen_embedding_target, self.fixture.target) + self.assertFalse(self.vector._prepared_target_slot.locked()) + + def test_wrong_width_and_busy_prepared_slot_never_publish_active_model(self) -> None: + self.fixture.width = 257 + with self.assertRaises(self.fixture.target_module.EmbeddingTargetError): + self.vector.apply_frozen_embedding_target(self.fixture.target) + self.assertIsNone(self.vector._model) + self.fixture.width = None + self.vector._prepared_target_slot.acquire() + try: + before = len(self.fixture.calls) + with self.assertRaises(RuntimeError): + self.vector.apply_frozen_embedding_target(self.fixture.target) + self.assertEqual(len(self.fixture.calls), before) + finally: + self.vector._prepared_target_slot.release() + + def test_base_pro_identity_reuse_has_zero_second_native_work(self) -> None: + self.vector.apply_frozen_embedding_target(self.fixture.target) + original = self.vector._model + before = (len(self.fixture.calls), len(self.fixture.encoded), self.vector._model_generation) + pro = self.fixture.Target.capture("pro") + self.vector.apply_frozen_embedding_target(pro) + self.assertIs(self.vector._model, original) + self.assertEqual(self.vector._frozen_embedding_target, pro) + self.assertEqual((len(self.fixture.calls), len(self.fixture.encoded), self.vector._model_generation), before) + + def test_deadline_consumed_by_state_lock_prevents_constructor(self) -> None: + clock = [1000.0] + class Lock: + def acquire(self, **kwargs): + clock[0] = 1002.0 + return True + def release(self): + pass + with patch.object(self.vector.time, "monotonic", side_effect=lambda: clock[0]), patch.object(self.vector, "_lock", Lock()): + with self.assertRaises(TimeoutError): + self.vector.apply_frozen_embedding_target(self.fixture.target, timeout=1) + self.assertEqual(self.fixture.calls, []) + + def test_setter_and_unload_cannot_mutate_admitted_model(self) -> None: + self.vector.apply_frozen_embedding_target(self.fixture.target) + model = self.vector._model + with self.gate.operation_lease(("synthetic",)): + for action in (lambda: self.vector.set_embedding_model("edge"), self.vector.unload_model): + with self.assertRaises(self.gate.ServingLeaseError): + action() + self.assertIs(self.vector._model, model) + + def test_shared_builtin_retains_normal_fast_protocol_custom_uses_target_protocol(self) -> None: + self.fixture.client.use_model_server = lambda: True + self.fixture.client.np.integer = int + self.fixture.client.PROTOCOL_VERSION = 1 + requests = [] + def request(value, timeout=None): + requests.append(value) + return self.fixture.server.handle_request(value) + self.fixture.client._request_with_autostart = request + self.vector.apply_frozen_embedding_target(self.fixture.target) + self.vector._model.encode(["synthetic"], batch_size=2, show_progress_bar=False) + self.assertEqual([entry["op"] for entry in requests], ["prepare_embed_target_v1", "embed_batched"]) + self.assertEqual(requests[-1]["tier"], "qwen3_256") + custom = self.fixture.Target.capture("custom") + requests.clear() + self.vector.apply_frozen_embedding_target(custom) + self.vector._model.encode(["synthetic"], batch_size=2, show_progress_bar=False) + self.assertEqual([entry["op"] for entry in requests], ["prepare_embed_target_v1", "embed_target_v1"]) + self.assertEqual(requests[-1]["target"], custom.to_wire()) + + def load_reranker(self): + return PREPARED["load"]("reranker", definitions=True, namespace={ + "threading": threading, "contextmanager": contextmanager, + "_active_tier": "edge", "_frozen_reranker_id": None, + "_model_certified": False, + "_model": None, "_model_name": "synthetic/old", "_lock": threading.Lock(), + "_TIER_RERANKERS": {"edge": "synthetic/default"}, + }) + + def test_reranker_actual_constructor_releases_old_reference_and_honors_exact_name(self) -> None: + reranker = self.load_reranker() + class Model: + pass + reranker._model = Model() + old = weakref.ref(reranker._model) + def build(name, **kwargs): + gc.collect() + self.assertIsNone(old()) + self.assertEqual(name, "synthetic/deep") + return Model() + with patch.dict(sys.modules, {"sentence_transformers": types.SimpleNamespace(CrossEncoder=build)}): + result = reranker.get_reranker(model_name="synthetic/deep", device="cpu") + self.assertIs(reranker._model, result) + self.assertEqual(reranker._model_name, "synthetic/deep") + + def test_same_name_uncertified_proxy_is_replaced_without_daemon_preflight(self) -> None: + reranker = self.load_reranker() + reranker._model, reranker._model_name = object(), "synthetic/deep" + old = reranker._model + self.fixture.client.use_model_server = lambda: True + with patch.object(self.fixture.client, "_request_with_autostart", side_effect=AssertionError("Unexpected preflight")): + result = reranker.get_reranker("synthetic/deep", certified=True) + self.assertIsNot(result, old) + self.assertIsInstance(result, self.fixture.client.CertifiedRerankerProxy) + self.assertTrue(reranker._model_certified) + self.assertIs(reranker.get_reranker("synthetic/deep", certified=True), result) + + def test_reranker_setter_unload_and_nested_override_refuse_during_read(self) -> None: + reranker = self.load_reranker() + reranker._model = object() + self.operation = types.SimpleNamespace(reranker_id="synthetic/default", selection=None) + with self.gate.operation_lease(("synthetic",)): + for action in (lambda: reranker.set_active_tier("pro"), reranker.unload_reranker, + lambda: reranker.get_reranker(model_name="synthetic/other")): + with self.assertRaises(RuntimeError): + action() + + +@unittest.skipUnless(os.environ.get("TRUEMEMORY_TEST_NATIVE_VEC") == "1", "GPUBox-only native sqlite-vec composition") +class TestNativeRuntimeComposition(unittest.TestCase): + def test_selected_engine_crud_search_and_reopen_use_one_pair(self) -> None: + import numpy as np + import sqlite_vec + from truememory import engine as engine_module, model_client, reranker, storage, vector_search + from truememory.embedding_target import EmbeddingTarget + from truememory.tier_switch import activation, runtime + + target = EmbeddingTarget.capture("base") + selection = activation.TierSelection( + generation="1" * 32, intent_id="2" * 32, target=target, + reranker_id="synthetic/reranker", tables=target.tables, job_id="3" * 32, + tracker="canonical-v1", source_epoch="synthetic-epoch", source_schema_signature="4" * 64, + manifest_generation="5" * 32, manifest_hash="6" * 64, receipt_hash="7" * 64, + pair_digest="8" * 64, schema="9" * 64, vector_count=0, cursor=None, + ) + intent = activation.ActivationIntent( + intent_id=selection.intent_id, expected_generation=None, target=target, + reranker_id=selection.reranker_id, job_id=selection.job_id, tracker=selection.tracker, + source_epoch=selection.source_epoch, source_schema_signature=selection.source_schema_signature, + state="db_selected", + ) + handles, encodes, projections = [], [], [] + engine = engine_module.TrueMemoryEngine(":memory:") + self.addCleanup(engine.close) + + def create(path): + self.assertEqual(str(path), ":memory:") + conn = storage.create_db(":memory:") + handles.append(conn) + conn.enable_load_extension(True) + sqlite_vec.load(conn) + conn.enable_load_extension(False) + vector_search.init_prepared_target_tables(conn, target) + for table in ("vec_messages_edge", "vec_messages_sep_edge"): + conn.execute(f"CREATE VIRTUAL TABLE {table} USING vec0(embedding float[256] distance_metric=cosine)") + conn.execute("INSERT INTO vector_cache_registry(tier_group,vec_table,sep_table,model_name,embedding_dim) VALUES (?,?,?,?,?)", + (target.tier_group, *target.tables, target.model_id, target.dimension)) + # This controlled fixture seeds an already selected journal; certification + # faults are covered by the separate activation-journal suite. + activation._put(conn, activation._SELECTED, activation._dump(selection)) + activation._put(conn, activation._INTENT, activation._dump(intent)) + activation._put(conn, "embed_model", target.model_id) + activation._put(conn, "embed_dim", str(target.dimension)) + conn.commit() + return conn + + class Encoder: + device = "cpu" + def encode(self, texts, **kwargs): + operation = runtime.current_operation(engine.conn) + if operation is None or operation.selection != selection or operation.tables != target.tables: + raise AssertionError("Encode escaped selected serving operation") + encodes.append(operation) + result = np.zeros((len(texts), target.dimension), dtype=np.float32) + result[:, 0] = 1.0 + return result + + def embedding(frozen, **kwargs): + self.assertEqual(frozen, target) + self.assertFalse(engine.conn.in_transaction) + projections.append(frozen) + return Encoder() + + class CrossEncoder: + device = "cpu" + def __init__(self, name, **kwargs): + if name != selection.reranker_id: + raise AssertionError("Unexpected synthetic reranker identity") + def predict(self, pairs, **kwargs): + return np.ones(len(pairs), dtype=np.float32) + + with ExitStack() as stack: + stack.enter_context(patch.multiple(vector_search, _model=None, _frozen_embedding_target=None, + EMBEDDING_MODEL="model2vec", _embedding_dim=256, _model_generation=0, + _runtime_policy_tier=None)) + stack.enter_context(patch.multiple(reranker, _model=None, _model_name="synthetic/old", _active_tier="edge", + _frozen_reranker_id=None, _model_certified=False)) + stack.enter_context(patch.multiple(runtime, _local=threading.local(), _acknowledged=None)) + stack.enter_context(patch.object(engine_module, "create_db", side_effect=create)) + stack.enter_context(patch.object(vector_search, "_load_frozen_embedding_target", side_effect=embedding)) + stack.enter_context(patch.object(model_client, "use_model_server", return_value=False)) + absent = object() + previous_transformers = sys.modules.get("sentence_transformers", absent) + + def restore_transformers() -> None: + if previous_transformers is absent: + sys.modules.pop("sentence_transformers", None) + else: + sys.modules["sentence_transformers"] = previous_transformers + + # A whole-registry mock would evict torch first imported by device + # detection here, leaving live native objects unsafe to re-import. + stack.callback(restore_transformers) + sys.modules["sentence_transformers"] = types.SimpleNamespace(CrossEncoder=CrossEncoder) + stack.enter_context(patch.object(engine, "_maybe_auto_consolidate", return_value=None)) + stack.enter_context(patch.object(engine, "_maybe_startup_consolidate", return_value=None)) + for flag in ("_has_personality", "_has_style_vec", "_has_temporal", "_has_salience", "_has_consolidation", + "_has_predictive", "_has_hyde", "_has_clustering", "_has_reranker"): + setattr(engine, flag, False) + first = engine.add("synthetic cobalt record") + self.assertTrue(engine.ready) + self.assertEqual(projections, [target]) + for table in target.tables: + self.assertEqual(engine.conn.execute(f"SELECT rowid FROM {table}").fetchall(), [(first["id"],)]) + self.assertEqual(engine.conn.execute("SELECT count(*) FROM vec_messages_edge").fetchone()[0], 0) + self.assertEqual(engine.update(first["id"], "synthetic cobalt corrected")["content"], "synthetic cobalt corrected") + self.assertEqual(engine.search_vectors_raw("synthetic cobalt")[0]["id"], first["id"]) + before = len(encodes) + result = engine.search_agentic("synthetic cobalt", limit=1, max_rounds=1, + use_hyde=False, use_reranker=False, use_clustering=False) + self.assertEqual(result[0]["id"], first["id"]) + self.assertGreater(len(encodes), before) + self.assertEqual(len({id(op) for op in encodes[before:]}), 1) + self.assertTrue(engine.delete(first["id"])) + for table in target.tables: + self.assertEqual(engine.conn.execute(f"SELECT count(*) FROM {table}").fetchone()[0], 0) + engine.add("synthetic second record") + self.assertTrue(engine.delete_all()) + self.assertEqual(engine.conn.execute("SELECT count(*) FROM messages").fetchone()[0], 0) + original = engine.conn + original.close() + engine.add("synthetic reopened record") + self.assertIsNot(engine.conn, original) + self.assertTrue(engine.ready) + self.assertIs(engine._runtime_vector_connection, engine.conn) + self.assertEqual(projections, [target]) + self.assertEqual(len(handles), 2) + self.assertIsNone(runtime.current_operation(engine.conn)) + native_modules = {name: id(module) for name, module in sys.modules.items() + if name == "torch" or name.startswith("torch.")} + self.assertEqual({name: id(sys.modules.get(name)) for name in native_modules}, native_modules) + self.assertIs(sys.modules.get("sentence_transformers", absent), previous_transformers) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-tier-serving-gate-795.py b/tests/test-tier-serving-gate-795.py new file mode 100644 index 00000000..70951a9e --- /dev/null +++ b/tests/test-tier-serving-gate-795.py @@ -0,0 +1,572 @@ +"""Serving admission through stdlib events and controlled condition wait points.""" + +from __future__ import annotations + +import importlib.util +import gc +import math +import sys +import threading +import types +import unittest +import weakref +from pathlib import Path +from unittest.mock import patch + +SOURCE = Path(__file__).resolve().parents[1] / "truememory/tier_switch/serving.py" +KEY = ("synthetic-database", "synthetic-generation") + + +def load() -> types.ModuleType: + spec = importlib.util.spec_from_file_location("synthetic_serving_gate", SOURCE) + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +class Cancelled(BaseException): + pass + + +class TestServingGate(unittest.TestCase): + def setUp(self) -> None: + self.api = load() + self.gate = self.api.ServingGate() + self.errors = [] + self.threads = [] + self.releases = [] + self.waiting = {} + self.wait_arguments = [] + original = self.gate._condition.wait + + def observed_wait(timeout=None): + name = threading.current_thread().name + self.wait_arguments.append((name, timeout)) + if name in self.waiting: + self.waiting[name].set() + return original(timeout) + + self.gate._condition.wait = observed_wait + self.addCleanup(self.cleanup_threads) + + def cleanup_threads(self) -> None: + for event in self.releases: + event.set() + for thread in self.threads: + thread.join(timeout=3) + self.assertFalse(any(thread.is_alive() for thread in self.threads), "Synthetic thread did not finish") + + def event(self) -> threading.Event: + event = threading.Event() + self.releases.append(event) + return event + + def start(self, name, work): + done = threading.Event() + self.waiting[name] = threading.Event() + def run(): + try: + work() + except BaseException as error: + self.errors.append(error) + finally: + done.set() + thread = threading.Thread(target=run, name=name, daemon=True) + self.threads.append(thread) + thread.start() + return done + + def reached(self, event: threading.Event) -> None: + self.assertTrue(event.wait(timeout=3), "Synthetic event was not reached") + + def writer(self, entered, release=None, **kwargs): + def work(): + with self.gate.exclusive_activation(**kwargs): + entered.set() + if release is not None: + self.reached(release) + return work + + def assert_idle(self) -> None: + with self.gate.exclusive_activation(timeout=1): + self.assertFalse(self.gate._readers) + self.assertFalse(self.gate._children) + self.assertFalse(self.gate._waiting_writers) + self.assertFalse(self.gate._writers) + self.assertEqual(self.errors, []) + + def test_concurrent_readers_and_same_thread_nested_calls(self) -> None: + child_entered, child_release = threading.Event(), self.event() + def reader(): + with self.gate.operation_lease(KEY) as lease: + self.assertEqual(lease.selection_key, KEY) + child_entered.set() + self.reached(child_release) + with self.gate.operation_lease(KEY) as outer: + done = self.start("reader", reader) + self.reached(child_entered) + with self.gate.operation_lease(KEY) as inner: + self.assertEqual(outer.selection_key, inner.selection_key) + self.assertIsNot(inner, outer) + child_release.set() + self.reached(done) + self.assert_idle() + + def test_waiting_writer_blocks_unrelated_reader_but_not_nested_reader(self) -> None: + writer_entered, writer_release, reader_entered = threading.Event(), self.event(), threading.Event() + with self.gate.operation_lease(KEY): + writer_done = self.start("writer", self.writer(writer_entered, writer_release)) + self.reached(self.waiting["writer"]) + def reader(): + with self.gate.operation_lease(KEY): + reader_entered.set() + reader_done = self.start("late-reader", reader) + self.reached(self.waiting["late-reader"]) + with self.gate.operation_lease(KEY): + self.assertFalse(writer_entered.is_set()) + self.assertFalse(reader_entered.is_set()) + self.reached(writer_entered) + self.assertFalse(reader_entered.is_set()) + writer_release.set() + self.reached(writer_done) + self.reached(reader_done) + self.assert_idle() + + def test_reserved_child_outlives_parent_and_joins_ahead_of_waiting_writer(self) -> None: + writer_entered, child_entered, child_release = threading.Event(), threading.Event(), self.event() + with self.gate.operation_lease(KEY) as lease: + child = lease.fork_child() + writer_done = self.start("writer", self.writer(writer_entered)) + self.reached(self.waiting["writer"]) + def task(): + with child.join() as joined: + self.assertEqual(joined.selection_key, KEY) + child_entered.set() + self.reached(child_release) + child_done = self.start("child", task) + self.reached(child_entered) + self.assertFalse(writer_entered.is_set()) + child_release.set() + self.reached(child_done) + self.reached(writer_done) + with self.assertRaises(self.api.ServingLeaseError): + with child.join(): + self.fail("Consumed reservation was reused") + child.close() + self.assert_idle() + + def test_parent_can_wait_for_parallel_children_with_a_writer_queued(self) -> None: + writer_entered = threading.Event() + with self.gate.operation_lease(KEY) as lease: + children = [lease.fork_child() for _ in range(3)] + writer_done = self.start("writer", self.writer(writer_entered)) + self.reached(self.waiting["writer"]) + child_events = [] + for index, child in enumerate(children): + def task(reservation=child): + with reservation.join() as joined: + with self.gate.operation_lease(KEY): + self.assertEqual(joined.selection_key, KEY) + child_events.append(self.start(f"child-{index}", task)) + for done in child_events: + self.reached(done) + self.assertFalse(writer_entered.is_set()) + self.reached(writer_done) + self.assert_idle() + + def test_unused_reservation_close_unpins_writer_and_is_idempotent(self) -> None: + entered = threading.Event() + with self.gate.operation_lease(KEY) as lease: + child = lease.fork_child() + done = self.start("writer", self.writer(entered)) + self.reached(self.waiting["writer"]) + child.close() + child.close() + self.reached(done) + self.assertTrue(entered.is_set()) + with self.assertRaises(self.api.ServingLeaseError): + with child.join(): + self.fail("Closed reservation was joined") + self.assert_idle() + + def test_active_join_cannot_be_closed_or_joined_twice(self) -> None: + entered, release = threading.Event(), self.event() + with self.gate.operation_lease(KEY) as lease: + child = lease.fork_child() + def task(): + with child.join(): + entered.set() + self.reached(release) + done = self.start("child", task) + self.reached(entered) + with self.assertRaises(self.api.ServingLeaseError): + child.close() + with self.assertRaises(self.api.ServingLeaseError): + with child.join(): + self.fail("Concurrent duplicate join was admitted") + self.assertEqual(len(self.gate._readers), 1) + release.set() + self.reached(done) + self.assert_idle() + + def test_child_can_reserve_grandchild_without_extending_writer_privilege(self) -> None: + with self.gate.operation_lease(KEY) as lease: + child = lease.fork_child() + with child.join() as joined: + grandchild = joined.fork_child() + with grandchild.join() as final: + self.assertEqual(final.selection_key, KEY) + self.assert_idle() + + def test_read_upgrade_refuses_without_waiting_or_poisoning_queue(self) -> None: + with self.gate.operation_lease(KEY): + with self.assertRaises(self.api.ServingLeaseError): + with self.gate.exclusive_activation(): + self.fail("Read was upgraded") + self.assertFalse(self.gate._waiting_writers) + self.assert_idle() + + def test_writer_reentry_and_sync_reader_are_safe_but_not_exportable(self) -> None: + with self.gate.exclusive_activation(): + with self.gate.exclusive_activation(): + with self.gate.operation_lease(KEY) as lease: + with self.gate.exclusive_activation(): + self.assertEqual(lease.selection_key, KEY) + with self.assertRaises(self.api.ServingLeaseError): + lease.fork_child() + self.assert_idle() + + def test_writer_owned_read_still_pins_after_outer_writer_exits_first(self) -> None: + writer = self.gate.exclusive_activation() + writer.__enter__() + reader = self.gate.operation_lease(KEY) + lease = reader.__enter__() + writer.__exit__(None, None, None) + entered = threading.Event() + done = self.start("next-writer", self.writer(entered)) + self.reached(self.waiting["next-writer"]) + with self.gate.operation_lease(KEY) as nested: + with self.assertRaises(self.api.ServingLeaseError): + nested.fork_child() + self.assertFalse(entered.is_set()) + reader.__exit__(None, None, None) + self.reached(done) + with self.assertRaises(self.api.ServingLeaseError): + lease.fork_child() + self.assert_idle() + + def test_writer_fifo_and_cancelled_writer_wakes_unrelated_readers(self) -> None: + order, first_release = [], self.event() + def first(): + with self.gate.exclusive_activation(): + order.append("first") + self.reached(first_release) + def second(): + with self.gate.exclusive_activation(): + order.append("second") + with self.gate.operation_lease(KEY): + first_done = self.start("first-writer", first) + self.reached(self.waiting["first-writer"]) + second_done = self.start("second-writer", second) + self.reached(self.waiting["second-writer"]) + # The first writer must remain ahead until its release event. + self.reached(self.waiting["second-writer"]) + first_release.set() + self.reached(first_done) + self.reached(second_done) + self.assertEqual(order, ["first", "second"]) + self.assert_idle() + + def test_cancellation_while_blocker_is_held_uses_bounded_condition_wait(self) -> None: + cancel, done, reader_entered = threading.Event(), None, threading.Event() + outcomes = [] + def waiting_writer(): + try: + with self.gate.exclusive_activation(cancelled=cancel): + self.fail("Cancelled writer admitted") + except self.api.ServingLeaseCancelled: + outcomes.append("cancelled") + with self.gate.operation_lease(KEY): + done = self.start("cancel-writer", waiting_writer) + self.reached(self.waiting["cancel-writer"]) + def late_reader(): + with self.gate.operation_lease(KEY): + reader_entered.set() + late_done = self.start("reader-after-cancel", late_reader) + self.reached(self.waiting["reader-after-cancel"]) + cancel.set() + self.reached(done) + self.reached(late_done) + self.assertTrue(reader_entered.is_set()) + self.assertEqual(outcomes, ["cancelled"]) + waits = [timeout for name, timeout in self.wait_arguments if name == "cancel-writer"] + self.assertTrue(waits) + self.assertTrue(all(0 < timeout <= 0.05 for timeout in waits)) + self.assert_idle() + + def test_expired_admission_and_inherited_child_cancellation_release_resources(self) -> None: + cancel = threading.Event() + with self.gate.operation_lease(KEY, cancelled=cancel) as lease: + child = lease.fork_child() + cancel.set() + with self.assertRaises(self.api.ServingLeaseCancelled): + with child.join(): + self.fail("Child escaped inherited cancellation") + self.assertIn(child, self.gate._children) + child.close() + self.assertFalse(self.gate._children) + with patch.object(self.api.time, "monotonic", return_value=100.0): + for context in (self.gate.operation_lease(KEY, deadline=100.0), + self.gate.exclusive_activation(deadline=100.0)): + with self.assertRaises(self.api.ServingLeaseTimeout): + with context: + self.fail("Expired admission ran") + self.assert_idle() + + def test_deadline_consumed_during_wait_refuses_and_removes_writer_ticket(self) -> None: + clock = [100.0] + with self.gate.operation_lease(KEY): + def timed_wait(timeout=None): + clock[0] = 102.0 + # A separate thread is needed to avoid the explicit upgrade guard. + outcomes = [] + def waiter(): + try: + with self.gate.exclusive_activation(deadline=101.0): + self.fail("Expired queued writer ran") + except self.api.ServingLeaseTimeout: + outcomes.append("expired") + with patch.object(self.api.time, "monotonic", side_effect=lambda: clock[0]), patch.object(self.gate._condition, "wait", timed_wait): + done = self.start("deadline-writer", waiter) + self.reached(done) + self.assertEqual(outcomes, ["expired"]) + self.assertFalse(self.gate._waiting_writers) + self.assert_idle() + + def test_initial_bookkeeping_lock_timeout_does_not_wait_again_for_cleanup(self) -> None: + class RefusingCondition: + def __init__(self): + self.calls = [] + def acquire(self, **kwargs): + self.calls.append(kwargs) + clock[0] = 102.0 + return False + def release(self): + raise AssertionError("Unacquired lock released") + clock = [100.0] + condition = RefusingCondition() + for kind in ("reader", "writer"): + gate = self.api.ServingGate() + gate._condition = condition + clock[0] = 100.0 + with patch.object(self.api.time, "monotonic", side_effect=lambda: clock[0]): + context = gate.operation_lease(KEY, deadline=101.0) if kind == "reader" else gate.exclusive_activation(deadline=101.0) + with self.assertRaises(self.api.ServingLeaseTimeout): + with context: + self.fail("Timed-out bookkeeping admission ran") + self.assertFalse(gate._readers) + self.assertFalse(gate._writers) + self.assertEqual(len(condition.calls), 2) + self.assertTrue(all("timeout" in call for call in condition.calls)) + + def test_inherited_controls_apply_before_mutex_for_nested_read_fork_and_join(self) -> None: + for operation in ("nested", "fork", "join"): + for mode in ("deadline", "cancelled"): + with self.subTest(operation=operation, mode=mode): + gate = self.api.ServingGate() + clock, cancelled = [100.0], threading.Event() + options = {"deadline": 101.0} if mode == "deadline" else {"cancelled": cancelled} + with patch.object(self.api.time, "monotonic", side_effect=lambda: clock[0]): + parent = gate.operation_lease(KEY, **options) + lease = parent.__enter__() + child = lease.fork_child() if operation == "join" else None + if child is not None: + parent.__exit__(None, None, None) + real_condition = gate._condition + arguments = [] + class RefusingCondition: + def acquire(self, **kwargs): + arguments.append(kwargs) + clock[0] = 102.0 + cancelled.set() + return False + def release(self): + raise AssertionError("Unacquired inherited lock released") + gate._condition = RefusingCondition() + expected = self.api.ServingLeaseTimeout if mode == "deadline" else self.api.ServingLeaseCancelled + try: + with self.assertRaises(expected): + if operation == "fork": + lease.fork_child() + else: + context = child.join() if child is not None else gate.operation_lease(KEY) + with context: + self.fail("Inherited admission bound was ignored") + finally: + gate._condition = real_condition + self.assertEqual(len(arguments), 1) + self.assertIn("timeout", arguments[0]) + if mode == "cancelled": + self.assertLessEqual(arguments[0]["timeout"], 0.05) + clock[0] = 100.0 + cancelled.clear() + if child is not None: + self.assertIn(child, gate._children) + with child.join() as joined: + self.assertEqual(joined.selection_key, KEY) + else: + if operation == "fork": + retry = lease.fork_child() + retry.close() + else: + with gate.operation_lease(KEY): + pass + parent.__exit__(None, None, None) + with gate.exclusive_activation(): + self.assertFalse(gate._children) + self.assertFalse(gate._readers) + + def test_cancelled_unadmitted_child_can_be_closed_without_joining(self) -> None: + cancel = threading.Event() + with self.gate.operation_lease(KEY) as lease: + child = lease.fork_child() + cancel.set() + with self.assertRaises(self.api.ServingLeaseCancelled): + with child.join(cancelled=cancel): + self.fail("Cancelled child entered") + self.assertIn(child, self.gate._children) + child.close() + self.assert_idle() + + def test_retired_authenticity_registry_does_not_retain_closed_children(self) -> None: + references = [] + with self.gate.operation_lease(KEY) as lease: + for _ in range(32): + child = lease.fork_child() + references.append(weakref.ref(child)) + child.close() + del child + gc.collect() + self.assertTrue(all(reference() is None for reference in references)) + self.assertEqual(len(self.gate._retired_children), 0) + self.assert_idle() + + def test_child_join_failure_and_body_baseexception_release_exactly_once(self) -> None: + sentinel = Cancelled("synthetic interruption") + with self.gate.operation_lease(KEY) as lease: + child = lease.fork_child() + with self.assertRaises(Cancelled) as caught: + with child.join(): + raise sentinel + self.assertIs(caught.exception, sentinel) + self.assertFalse(self.gate._children) + for context in (self.gate.operation_lease(KEY), self.gate.exclusive_activation()): + with self.assertRaises(Cancelled) as caught: + with context: + raise sentinel + self.assertIs(caught.exception, sentinel) + self.assert_idle() + + def test_wait_baseexception_unregisters_writer_and_notifies_readers(self) -> None: + sentinel = Cancelled("synthetic wait interruption") + outcomes = [] + with self.gate.operation_lease(KEY): + with patch.object(self.gate._condition, "wait", side_effect=sentinel): + def waiter(): + try: + with self.gate.exclusive_activation(): + self.fail("Writer should not enter") + except Cancelled as error: + outcomes.append(error) + done = self.start("interrupted-writer", waiter) + self.reached(done) + self.assertEqual(outcomes, [sentinel]) + self.assertFalse(self.gate._waiting_writers) + self.assert_idle() + + def test_foreign_thread_cannot_fork_or_inspect_parent_lease(self) -> None: + outcomes = [] + with self.gate.operation_lease(KEY) as lease: + def wrong_thread(): + for call in (lease.fork_child, lambda: lease.selection_key): + try: + call() + except self.api.ServingLeaseError: + outcomes.append("refused") + done = self.start("foreign-reader", wrong_thread) + self.reached(done) + self.assertEqual(outcomes, ["refused", "refused"]) + self.assert_idle() + + def test_forged_foreign_stale_and_process_inherited_tokens_refuse(self) -> None: + for kind in (self.api.OperationLease, self.api.ChildReservation): + with self.assertRaises(self.api.ServingLeaseError): + kind() + forged = object.__new__(self.api.ChildReservation) + with self.assertRaises(self.api.ServingLeaseError): + forged.close() + object.__setattr__(forged, "_gate", self.gate) + object.__setattr__(forged, "_closed", True) + with self.assertRaises(self.api.ServingLeaseError): + forged.close() + with self.gate.operation_lease(KEY) as lease: + child = lease.fork_child() + self.assertNotIn(KEY[0], repr(child)) + foreign = self.api.ServingGate() + with self.assertRaises(self.api.ServingLeaseError): + foreign._close_child(child) + self.gate._condition.acquire() + try: + with patch.object(self.api.os, "getpid", return_value=self.gate._pid + 1): + with self.assertRaises(self.api.ServingLeaseError): + child.close() + with self.assertRaises(self.api.ServingLeaseError): + with self.gate.operation_lease(KEY): + self.fail("Inherited explicit gate admitted") + finally: + self.gate._condition.release() + child.close() + with self.assertRaises(self.api.ServingLeaseError): + lease.fork_child() + self.assert_idle() + + def test_module_gate_reset_rejects_inherited_tokens_and_allows_fresh_operations(self) -> None: + old = self.api._DEFAULT_GATE + with self.api.operation_lease(KEY) as lease: + child = lease.fork_child() + with patch.object(self.api.os, "getpid", return_value=old._pid + 1): + self.api._after_fork() + self.assertIsNot(self.api._DEFAULT_GATE, old) + with self.api.operation_lease(KEY) as fresh: + self.assertEqual(fresh.selection_key, KEY) + with self.assertRaises(self.api.ServingLeaseError): + with child.join(): + self.fail("Inherited reservation joined") + child.close() + + def test_keys_are_inert_bounded_exact_types_and_never_disclosed(self) -> None: + class Hostile(str): + def __eq__(self, other): + raise AssertionError("User comparator invoked") + def __len__(self): + raise AssertionError("User length invoked") + for key in (["synthetic"], (), ("",), ("x" * 513,), ("x",) * 9, (Hostile("secret-key"),), (True,)): + with self.assertRaises(self.api.ServingLeaseError) as caught: + with self.gate.operation_lease(key): + self.fail("Invalid key admitted") + self.assertNotIn("secret-key", str(caught.exception)) + with self.gate.operation_lease(KEY): + with self.assertRaises(self.api.ServingLeaseError) as caught: + with self.gate.operation_lease(("different-private-key",)): + self.fail("Selection mixed within operation") + self.assertNotIn("private", str(caught.exception)) + for value in (True, math.inf, math.nan, "1", 10**1000, -1): + with self.assertRaises(self.api.ServingLeaseError): + with self.gate.operation_lease(KEY, timeout=value): + self.fail("Invalid timeout admitted") + self.assert_idle() + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-tier-serving-receipt-795.py b/tests/test-tier-serving-receipt-795.py new file mode 100644 index 00000000..ec5ea748 --- /dev/null +++ b/tests/test-tier-serving-receipt-795.py @@ -0,0 +1,480 @@ +"""Selected response receipts through real control flow and stdlib model doubles.""" +from __future__ import annotations + +import dataclasses +from pathlib import Path +import runpy +import sys +import threading +import types +import unittest +from unittest.mock import Mock, patch + + +BASE = runpy.run_path(str(Path(__file__).with_name("test-prepared-target-795.py"))) +Array = BASE["Array"] + + +class FirstBusyLock: + """One nonblocking contention observation, followed by a real unlocked lock.""" + + def __init__(self) -> None: + self.lock = threading.Lock() + self.first = True + + def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: + if not blocking and self.first: + self.first = False + return False + return self.lock.acquire(blocking, timeout) + + def release(self) -> None: + self.lock.release() + + def locked(self) -> bool: + return self.lock.locked() + + +class TestServingReceipts(unittest.TestCase): + def setUp(self) -> None: + self.fixture = BASE["TestPreparedTargets"]() + self.fixture.setUp() + self.addCleanup(self.fixture.doCleanups) + self.client = self.fixture.client + self.client.PROTOCOL_VERSION = 1 + self.client.np.integer = int + self.server = self.fixture.server + self.ms = self.fixture.ms + self.Target = self.fixture.Target + self.edge = self.Target.capture("edge") + self.base = self.Target.capture("base") + self.calls: list[tuple[str, str, list, int]] = [] + self.builds: list[tuple[str, str | None]] = [] + self.width = 256 + self.before_encode = lambda: None + self.before_predict = lambda: None + self.build_hook = lambda: None + self.embed_oom = False + self.rerank_oom = False + self.cpu_error = False + owner = self + + class Model: + def __init__(self, identity: str, device: str | None = None) -> None: + self.identity, self.device = identity, device + + def encode(self, texts: list[str], **kwargs: object) -> Array: + owner.before_encode() + owner.calls.append(("embed", self.identity, list(texts), kwargs["batch_size"])) + if owner.cpu_error and owner.server._fast_lock.locked(): + owner.cpu_error = False + raise RuntimeError("synthetic transient CPU failure") + if owner.embed_oom: + owner.embed_oom = False + raise RuntimeError("MPS backend out of memory") + return Array([[float(text[1:])] * owner.width for text in texts], (len(texts), owner.width)) + + def to(self, device: str) -> None: + self.device = device + + class Reranker: + def __init__(self, name: str, device: str | None = None) -> None: + self.name, self.device = name, device + owner.builds.append((name, device)) + + def predict(self, pairs: list, **kwargs: object) -> Array: + owner.before_predict() + owner.calls.append(("rerank", self.name, list(pairs), kwargs["batch_size"])) + if owner.rerank_oom: + owner.rerank_oom = False + raise RuntimeError("MPS backend out of memory") + return Array([[float(pair[1][1:])] for pair in pairs], (len(pairs), 1)) + + self.Model = Model + + def build(identity: str, device: str | None) -> Model: + self.assertTrue(self.server._inference_lock.locked() or self.server._fast_lock.locked()) + self.build_hook() + self.builds.append((identity, device)) + return Model(identity, device) + + self.server._build_embed_model = build + self.fixture.mps.auto_detect_device = lambda: "cpu" + self.fixture.mps.is_mps_oom = lambda exc: "MPS backend out of memory" in str(exc) + modules = patch.dict(sys.modules, { + "sentence_transformers": types.SimpleNamespace(CrossEncoder=Reranker), + "truememory.reranker": types.SimpleNamespace(get_current_reranker_name=lambda: "synthetic/default"), + }) + modules.start() + self.addCleanup(modules.stop) + + def embed(self, count: int = 1, *, target=None, **fields: object) -> dict: + target = self.edge if target is None else target + return self.server.handle_request({ + "op": "embed", "tier": target.model_id, + "expected_target": target.to_wire(), "texts": [f"s{i}" for i in range(count)], **fields, + }) + + def rerank(self, count: int = 1, **fields: object) -> dict: + return self.server.handle_request({ + "op": "rerank", "model_name": "synthetic/reranker", "expected_model_name": "synthetic/reranker", + "pairs": [["query", f"s{i}"] for i in range(count)], **fields, + }) + + def assert_released(self) -> None: + self.assertFalse(self.server._inference_lock.locked()) + self.assertFalse(self.server._fast_lock.locked()) + self.assertFalse(self.server._lock.locked()) + self.assertEqual(self.server._inflight, 0) + + def test_constructors_are_lazy_and_embedding_is_a_remote_proxy(self) -> None: + self.client._request_with_autostart = Mock(side_effect=AssertionError("unexpected request")) + proxy = self.client.CertifiedEmbeddingProxy(self.edge) + self.assertIsInstance(proxy, self.client.EmbeddingProxy) + self.client.CertifiedRerankerProxy("synthetic/reranker") + self.client._request_with_autostart.assert_not_called() + self.assertEqual(self.builds, []) + + def test_certified_client_uses_existing_operation_batch_and_deadline(self) -> None: + calls = [] + + def send(request: dict, timeout=None) -> dict: + calls.append((request, timeout)) + return self.server.handle_request(request) + + self.client._request_with_autostart = send + proxy = self.client.CertifiedEmbeddingProxy(self.edge) + output = proxy.encode(["s0", "s1", "s2"], timeout=7, batch_size=2, show_progress_bar=False) + self.assertEqual(output.rows, [[float(i)] * 256 for i in range(3)]) + self.assertEqual(calls[0][0]["op"], "embed_batched") + self.assertEqual(calls[0][0]["tier"], "model2vec") + self.assertEqual(calls[0][0]["expected_target"], self.edge.to_wire()) + self.assertEqual(calls[0][1], 7) + self.assertEqual([len(call[2]) for call in self.calls], [2, 1]) + self.assert_released() + + def test_each_response_rejects_same_width_identity_drift_after_success(self) -> None: + proxy = self.client.CertifiedEmbeddingProxy(self.edge) + output = proxy.encode("s0") + self.assertEqual(output.shape, (1, 256)) + for protocol in (1, None): + for receipt in (None, self.base.to_wire()): + response = {"ok": True, "vectors": output, "target": receipt} + if protocol is not None: + response["protocol"] = protocol + self.client._request_with_autostart = Mock(return_value=response) + with self.subTest(protocol=protocol, receipt=receipt), self.assertRaises(self.client.ProtocolMismatchError): + proxy.encode("s0") + self.client._request_with_autostart.assert_called_once() + + def test_embedding_receipt_protocol_and_descriptor_types_are_strict(self) -> None: + response = self.embed() + mutations = [ + {"protocol": value} for value in (None, True, 1.0, "1", 2) + ] + [{"target": {**self.edge.to_wire(), **change}} for change in ( + {"version": True}, {"version": 2}, {"dimension": True}, {"extra": 1}, {"tier": "base"}, + )] + for mutation in mutations: + self.client._request_with_autostart = Mock(return_value={**response, **mutation}) + with self.subTest(mutation=mutation), self.assertRaises(self.client.ProtocolMismatchError): + self.client.CertifiedEmbeddingProxy(self.edge).encode("s0") + self.client._request_with_autostart = Mock(return_value={**response, "vectors": Array([[1.0]], (1, 1))}) + with self.assertRaises(self.fixture.target_module.EmbeddingTargetError): + self.client.CertifiedEmbeddingProxy(self.edge).encode("s0") + + def test_custom_and_noncanonical_requests_refuse_before_native(self) -> None: + custom = self.Target.capture("custom") + with self.assertRaises(ValueError): + self.client.CertifiedEmbeddingProxy(custom) + for fields in ({"tier": "edge"}, {"tier": "qwen3_256"}, {"expected_target": custom.to_wire()}, + {"expected_target": None}, {"texts": [object()]}): + with self.subTest(fields=fields), self.assertRaises(ValueError): + self.embed(**fields) + self.assertEqual(self.builds, []) + self.assertEqual(self.calls, []) + self.assert_released() + + def test_direct_main_fast_receipt_preserves_sustained_bookkeeping(self) -> None: + before = self.fixture.globals() + response = self.embed() + self.assertEqual(response["target"], self.edge.to_wire()) + self.assertEqual(response["protocol"], 1) + self.assertEqual(self.server._embed_timestamps, []) + self.assertEqual(self.fixture.globals(), before) + self.assertEqual(len(self.calls), 1) + self.assert_released() + + def test_cpu_fast_receipt_and_cache_reuse_while_main_owner_busy(self) -> None: + self.embed() + self.server._inference_lock.acquire() + try: + first = self.embed() + second = self.embed() + finally: + self.server._inference_lock.release() + self.assertEqual(first["target"], self.edge.to_wire()) + self.assertEqual(second["target"], self.edge.to_wire()) + self.assertEqual(self.builds, [("model2vec", "cpu"), ("model2vec", "cpu")]) + self.assertEqual(self.server._embed_timestamps, []) + self.assert_released() + + def test_cold_fast_decline_preserves_main_fallback_and_receipt(self) -> None: + self.server._inference_lock = FirstBusyLock() + response = self.embed() + self.assertEqual(response["target"], self.edge.to_wire()) + self.assertEqual(len(self.server._embed_timestamps), 1) + self.assertEqual(len(self.calls), 1) + self.assert_released() + + def test_ordinary_cpu_conversion_keeps_its_original_post_owner_placement(self) -> None: + self.embed() + checked = self.ms._checked_batch + ownership = [] + + def observe(*args): + ownership.append(self.server._fast_lock.locked()) + return checked(*args) + + self.ms._checked_batch = observe + self.server._inference_lock.acquire() + try: + ordinary = self.server.handle_request({"op": "embed", "tier": "model2vec", "texts": ["s0"]}) + selected = self.embed() + finally: + self.server._inference_lock.release() + self.assertEqual(ownership, [False, True]) + self.assertEqual(ordinary["vectors"].rows, selected["vectors"].rows) + self.assertNotIn("target", ordinary) + self.assertEqual(selected["target"], self.edge.to_wire()) + self.assert_released() + + def test_transient_cpu_fast_failure_keeps_verified_main_fallback(self) -> None: + self.embed() + self.server._inference_lock = FirstBusyLock() + self.cpu_error = True + response = self.embed() + self.assertEqual(response["target"], self.edge.to_wire()) + self.assertEqual(len(self.calls), 3) + self.assertEqual(len(self.server._embed_timestamps), 1) + self.assert_released() + + def test_generation_changes_during_cpu_construction_do_not_relabel_output(self) -> None: + self.embed() + original_generation = self.server._embed_state.generation + + def advance() -> None: + if self.server._fast_lock.locked(): + with self.server._lock: + self.server._publish_embed_state(self.ms._EmbedState(self.Model("qwen3_256"), "qwen3_256", "qwen3_256")) + self.server._publish_embed_state(self.ms._EmbedState(self.Model("model2vec"), "model2vec", "model2vec")) + + self.build_hook = advance + self.server._inference_lock.acquire() + try: + response = self.embed() + finally: + self.server._inference_lock.release() + self.assertEqual(response["target"], self.edge.to_wire()) + self.assertIsNot(self.server._embed_state.generation, original_generation) + self.assertIsNone(self.server._fast_encoder) + self.assert_released() + + def test_effective_main_and_fast_identity_mismatch_never_encodes_or_falls_back(self) -> None: + for fast in (False, True): + self.server._embed_state = self.ms._EmbedState(self.Model("qwen3_256"), "model2vec", "qwen3_256") + if fast: + self.server._inference_lock.acquire() + try: + with self.subTest(fast=fast), self.assertRaises(ValueError): + self.embed() + finally: + if fast: + self.server._inference_lock.release() + self.assertEqual(self.calls, []) + self.assertEqual(self.server._embed_timestamps, []) + self.assert_released() + + def test_fast_cached_identity_mismatch_and_width_failure_never_fall_back(self) -> None: + self.embed() + self.server._inference_lock.acquire() + try: + self.embed() + self.server._fast_model_id = "qwen3_256" + with self.assertRaises(ValueError): + self.embed() + self.server._fast_model_id = "model2vec" + self.width = 1 + with self.assertRaises(ValueError): + self.embed() + finally: + self.server._inference_lock.release() + self.assertEqual(self.server._embed_timestamps, []) + self.assert_released() + + def test_empty_selected_embedding_requires_receipt_without_added_probe(self) -> None: + response = self.embed(0) + self.assertEqual(response["vectors"].shape, (0, 256)) + self.assertEqual(response["target"], self.edge.to_wire()) + self.assertEqual([call[2] for call in self.calls], [[]]) + self.client._request_with_autostart = Mock(return_value={"ok": True, "vectors": response["vectors"], "protocol": 1}) + with self.assertRaises(self.client.ProtocolMismatchError): + self.client.CertifiedEmbeddingProxy(self.edge).encode([]) + + def test_embedding_oom_fast_and_main_retry_keep_receipt_order_and_owner(self) -> None: + for count in (1, 5): + self.calls.clear() + self.embed_oom = True + response = self.embed(count, op="embed_batched", batch_size=2) + self.assertEqual(response["target"], self.edge.to_wire()) + self.assertEqual(response["vectors"].rows, [[float(i)] * 256 for i in range(count)]) + self.assertEqual([len(call[2]) for call in self.calls], [1, 1] if count == 1 else [2, 2, 2, 1]) + self.assertIn("embed", self.server._sticky_cpu) + self.assert_released() + + def test_every_main_slice_checks_width_and_never_returns_partial_output(self) -> None: + def change_width() -> None: + if self.calls: + self.width = 1 + + self.before_encode = change_width + with self.assertRaises(ValueError): + self.embed(5, op="embed_batched", batch_size=2) + self.assertEqual([len(call[2]) for call in self.calls], [2, 2]) + self.assert_released() + + def test_admission_and_deadline_refusals_have_no_receipt_or_fallback(self) -> None: + response = self.embed(deadline=1) + self.assertFalse(response["ok"]) + self.assertNotIn("target", response) + self.assertEqual(self.calls, []) + self.embed() + self.server._inference_lock.acquire() + self.server._check_process_memory = Mock(side_effect=self.ms._ProcessMemoryRefused("synthetic capacity")) + try: + response = self.embed() + finally: + self.server._inference_lock.release() + self.assertEqual(response["error_code"], "server_busy") + self.assertNotIn("target", response) + self.assertEqual(len(self.calls), 1) + self.assertEqual(self.server._embed_timestamps, []) + self.assert_released() + + def test_ordinary_clients_and_responses_remain_unattested(self) -> None: + self.client._request_with_autostart = Mock(return_value={"ok": True, "vectors": "legacy", "scores": "legacy"}) + self.assertEqual(self.client.EmbeddingProxy("edge").encode("s0"), "legacy") + self.assertNotIn("expected_target", self.client._request_with_autostart.call_args.args[0]) + self.assertEqual(self.client.RerankerProxy().predict([["q", "s0"]]), "legacy") + self.assertNotIn("expected_model_name", self.client._request_with_autostart.call_args.args[0]) + response = self.server.handle_request({"op": "embed", "tier": "model2vec", "texts": ["s0"]}) + self.assertEqual(set(response), {"ok", "vectors"}) + response = self.server.handle_request({"op": "rerank", "pairs": [["q", "s0"]]}) + self.assertEqual(set(response), {"ok", "scores"}) + + def test_reranker_every_response_certifies_explicit_name_and_bounded_order(self) -> None: + proxy = self.client.CertifiedRerankerProxy("synthetic/reranker") + response = proxy.predict([["q", f"s{i}"] for i in range(5)], timeout=3, batch_size=2) + self.assertEqual(response.rows, [[float(i)] for i in range(5)]) + self.assertEqual([len(call[2]) for call in self.calls], [2, 2, 1]) + self.assertEqual(self.builds, [("synthetic/reranker", "cpu")]) + for receipt in (None, {"version": 1, "model_name": "synthetic/default"}, + {"version": True, "model_name": "synthetic/reranker"}, + {"version": 1, "model_name": "synthetic/reranker", "extra": 1}): + self.client._request_with_autostart = Mock(return_value={"ok": True, "scores": response, "protocol": 1, "reranker": receipt}) + with self.subTest(receipt=receipt), self.assertRaises(self.client.ProtocolMismatchError): + proxy.predict([["q", "s0"]]) + self.client._request_with_autostart.assert_called_once() + self.assert_released() + + def test_reranker_receipt_requires_one_result_row_per_pair(self) -> None: + proxy = self.client.CertifiedRerankerProxy("synthetic/reranker") + pairs = [["q", "s0"], ["q", "s1"]] + receipt = {"version": 1, "model_name": "synthetic/reranker"} + for scores in (None, [0., 1.], Array([], ()), Array([[0.]], (1, 1)), + Array([[0.], [1.], [2.]], (3, 1))): + self.client._request_with_autostart = Mock(return_value={ + "ok": True, "scores": scores, "protocol": 1, "reranker": receipt, + }) + with self.subTest(shape=getattr(scores, "shape", None)), self.assertRaises(self.client.ProtocolMismatchError): + proxy.predict(iter(pairs)) + self.client._request_with_autostart.assert_called_once() + for shape in ((2,), (2, 1), (2, 2)): + scores = Array([[0.], [1.]], shape) + self.client._request_with_autostart = Mock(return_value={ + "ok": True, "scores": scores, "protocol": 1, "reranker": receipt, + }) + with self.subTest(shape=shape): + self.assertIs(proxy.predict(iter(pairs)), scores) + self.assert_released() + + def test_empty_reranker_and_missing_protocol_are_strict(self) -> None: + response = self.rerank(0) + self.assertEqual(response["scores"].shape, (0, 1)) + self.assertEqual(response["reranker"], {"version": 1, "model_name": "synthetic/reranker"}) + self.assertEqual([call[2] for call in self.calls], [[]]) + for value in (None, True, 1.0, "1", 2): + self.client._request_with_autostart = Mock(return_value={**response, "protocol": value}) + with self.subTest(protocol=value), self.assertRaises(self.client.ProtocolMismatchError): + self.client.CertifiedRerankerProxy("synthetic/reranker").predict([]) + + def test_reranker_explicit_identity_validation_and_effective_cache_mismatch(self) -> None: + for name in (None, "", True, "x" * 513, "synthetic/model/other", "bad\nname"): + with self.subTest(name=name), self.assertRaises(ValueError): + self.client.CertifiedRerankerProxy(name) + with self.subTest(name=name), self.assertRaises(ValueError): + self.rerank(expected_model_name=name) + with self.assertRaises(ValueError): + self.rerank(model_name=None) + self.assertEqual(self.builds, []) + real_get = self.server._get_reranker + + def wrong(name=None, deadline=None): + model = real_get(name, deadline) + self.server._reranker_name = "synthetic/different" + return model + + self.server._get_reranker = wrong + with self.assertRaises(ValueError): + self.rerank() + self.assertEqual(self.calls, []) + self.assert_released() + + def test_reranker_cpu_replacement_is_revalidated_before_retry(self) -> None: + self.rerank_oom = True + response = self.rerank(5, op="rerank_batched", batch_size=2) + self.assertEqual(response["reranker"], {"version": 1, "model_name": "synthetic/reranker"}) + self.assertEqual(response["scores"].rows, [[float(i)] for i in range(5)]) + self.assertEqual([len(call[2]) for call in self.calls], [2, 2, 2, 1]) + self.assertEqual([name for name, _device in self.builds], ["synthetic/reranker"] * 2) + real_get = self.server._get_reranker + + def wrong_retry(name=None, deadline=None): + replacing = self.server._reranker is None + model = real_get(name, deadline) + if replacing: + self.server._reranker_name = "synthetic/other" + return model + + self.server._get_reranker = wrong_retry + self.rerank_oom = True + before = len(self.calls) + with self.assertRaises(ValueError): + self.rerank() + self.assertEqual(len(self.calls), before + 1) + self.assert_released() + + def test_public_tier_receipt_does_not_collapse_base_and_pro(self) -> None: + response = self.embed(target=self.base) + model = self.server._embed_state.model + pro = dataclasses.replace(self.base, tier="pro") + second = self.embed(target=pro) + self.assertEqual(response["target"], self.base.to_wire()) + self.assertEqual(second["target"], pro.to_wire()) + self.assertIs(self.server._embed_state.model, model) + self.client._request_with_autostart = Mock(return_value=second) + with self.assertRaises(self.client.ProtocolMismatchError): + self.client.CertifiedEmbeddingProxy(self.base).encode("s0") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-tier-streamed-worker-756.py b/tests/test-tier-streamed-worker-756.py new file mode 100644 index 00000000..88a32e9c --- /dev/null +++ b/tests/test-tier-streamed-worker-756.py @@ -0,0 +1,646 @@ +"""Actual source checkpoints and worker flow with in-memory SQL and stub leases.""" + +import ast +import builtins +import gc +import math +import runpy +import sqlite3 +import struct +import sys +import types +import unittest +import weakref +from contextlib import contextmanager, nullcontext +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SOURCE_TEST = runpy.run_path(str(Path(__file__).with_name("test-tier-rebuild-source-756.py"))) +TABLES = ("vec_messages_custom", "vec_messages_sep_custom") + + +class Workspace: + pass + + +class Vectors(list): + pass + + +class TestStreamedWorker(unittest.TestCase): + def setUp(self): + self.modules = SOURCE_TEST["load_modules"]() + self.source = self.modules["tier_switch.source"] + self.tracking = self.modules["rebuild_source"] + self.now = 0.0 + self.sizes = [64] + self.admissions = [] + self.attempts = [] + self.forwards = [] + self.deadlines = [] + self.refs = [] + self.cleanup = [] + self.sql = [] + self.committed = [] + self.live = [] + self.prepared = self.closed = 0 + self.fail_stage = "" + self.failed_attempts = {1} + self.error_factory = lambda: RuntimeError("synthetic out of memory") + self.rollback_mode = "" + self.commit_after_success_timeout = False + self.commit_after_success_oom = "" + self.armed = False + self.stage = "" + self.before_hook = lambda: None + self.after_hook = lambda: None + self.prepare_hook = lambda: None + self.encode_hook = lambda: None + self.cleanup_hook = lambda kind: None + self.write_hook = lambda: None + self.finish_hook = lambda: None + owner = self + + class Connection(sqlite3.Connection): + tier_write = False + + def execute(self, sql, parameters=()): + owner.sql.append(sql) + result = super().execute(sql, parameters) + if owner.armed: + if sql.startswith(f'INSERT INTO main."{TABLES[0]}"'): + self.tier_write = True + owner.write_hook() + owner.maybe_fail("insert_completion") + elif sql.startswith(f'INSERT INTO main."{TABLES[1]}"'): + owner.maybe_fail("insert_separation") + elif sql.startswith("INSERT OR REPLACE INTO main.metadata"): + owner.maybe_fail("checkpoint") + return result + + def commit(self): + owner.sql.append("COMMIT") + wrote = self.tier_write + if owner.armed and self.tier_write: + owner.maybe_fail("commit") + super().commit() + self.tier_write = False + if owner.armed and wrote and owner.commit_after_success_timeout: + owner.now = 9001.0 + raise TimeoutError("synthetic timeout after committed pair") + if owner.armed and wrote and owner.commit_after_success_oom: + if owner.commit_after_success_oom == "cancel": + owner.worker.cancel() + elif owner.commit_after_success_oom == "timeout": + owner.now = 9001.0 + raise RuntimeError("synthetic out of memory after committed pair") + + def rollback(self): + owner.sql.append("ROLLBACK") + if owner.rollback_mode == "dirty_raise": + raise MemoryError("synthetic out of memory during rollback") + if owner.rollback_mode == "dirty_return": + return + super().rollback() + self.tier_write = False + if owner.rollback_mode == "clean_raise": + raise MemoryError("synthetic out of memory after rollback") + if owner.rollback_mode == "clean_timeout": + owner.now = 9001.0 + raise TimeoutError("synthetic rollback timeout after cleanup") + + self.conn = sqlite3.connect(":memory:", factory=Connection) + self.addCleanup(self.conn.close) + self.conn.executescript(""" + CREATE TABLE messages(id INTEGER PRIMARY KEY, content TEXT, sender TEXT, recipient TEXT, timestamp TEXT); + CREATE TABLE metadata(key TEXT PRIMARY KEY, value TEXT NOT NULL); + CREATE TABLE vec_messages_custom(embedding BLOB); + CREATE TABLE vec_messages_sep_custom(embedding BLOB); + CREATE TABLE serving_pair(embedding BLOB); + INSERT INTO serving_pair(rowid,embedding) VALUES (42,x'0102'); + CREATE TABLE rebuild_status(id INTEGER PRIMARY KEY, status TEXT); + INSERT INTO rebuild_status VALUES (1,'untouched'); + """) + self.tracking.ensure_rebuild_tracking(self.conn) + + class Throttler: + batch_size = 64 + + def before_batch(self): + if len(owner.admissions) > 2000: + raise AssertionError("Unbounded synthetic loop") + if owner.sizes: + self.batch_size = owner.sizes.pop(0) + owner.admissions.append(self.batch_size) + owner.before_hook() + return self.batch_size, {"synthetic": True} + + def after_batch(self, count, duration): + owner.committed.append(count) + owner.after_hook() + + def on_oom(self): + owner.observe_cleanup("backoff") + + def should_flush_cache(self): + return False + + @staticmethod + def flush_gpu_cache(): + owner.observe_cleanup("flush") + + registry = types.ModuleType("synthetic_registry") + registry.VectorCacheRegistry = type("ForbiddenRegistry", (), {}) + throttler_module = types.ModuleType("synthetic_throttler") + throttler_module.DynamicThrottler = Throttler + self.modules["tier_switch.cache"] = registry + self.modules["tier_switch.throttler"] = throttler_module + self.torch = types.SimpleNamespace(no_grad=nullcontext, cuda=types.SimpleNamespace(OutOfMemoryError=type("CudaOOM", (RuntimeError,), {}))) + + def safe_import(name, globals=None, locals=None, fromlist=(), level=0): + if name == "torch": + return self.torch + if name in ("numpy", "sqlite_vec", "sentence_transformers"): + raise AssertionError("No native imports in streamed worker tests") + if name.startswith("truememory."): + short = name.removeprefix("truememory.") + if short in self.modules: + return self.modules[short] + raise AssertionError("Unexpected application import: " + name) + return builtins.__import__(name, globals, locals, fromlist, level) + + def load(name): + module = types.ModuleType("synthetic_streamed_" + name.replace(".", "_")) + sys.modules[module.__name__] = module + module.__dict__["__builtins__"] = dict(vars(builtins), __import__=safe_import) + path = ROOT / "truememory" / (name.replace(".", "/") + ".py") + exec(compile(path.read_text(), str(path), "exec"), module.__dict__) + self.modules[name] = module + return module + + target_module = load("embedding_target") + self.target = target_module.EmbeddingTarget("custom", "synthetic/worker", 2, "custom") + self.vector = types.ModuleType("synthetic_streamed_vector") + self.vector.__dict__.update(math=math, struct=struct, np=types.SimpleNamespace(ndarray=type("NativeArray", (), {}))) + tree = ast.parse((ROOT / "truememory/vector_search.py").read_text()) + tree.body = [node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name in {"serialize_f32", "_build_sep_text"}] + exec(compile(tree, "actual-serialization-and-separation", "exec"), self.vector.__dict__) + serialize = self.vector.serialize_f32 + + def serialized(value): + self.maybe_fail("serialize_" + self.stage) + return serialize(value) + + self.vector.serialize_f32 = serialized + self.active_model = object() + self.vector._model = self.active_model + self.vector.EMBEDDING_MODEL = "serving-model" + self.vector._embedding_dim = 99 + self.vector._model_generation = 17 + + class Lease: + def encode(self, texts, *, timeout, batch_size): + owner.assertFalse(owner.conn.in_transaction) + owner.assertGreater(timeout, 0) + owner.assertEqual(batch_size, 32) + owner.deadlines.append(timeout) + owner.stage = "completion" if texts[0].startswith("content-") else "separation" + if owner.stage == "completion": + owner.attempts.append(list(texts)) + owner.forwards.append((owner.stage, list(texts))) + owner.encode_hook() + owner.maybe_fail(owner.stage) + values = Vectors([[3., 4.]] * len(texts)) + owner.refs.append(weakref.ref(values)) + return values + + @contextmanager + def prepare(target, *, timeout): + self.assertEqual(target, self.target) + self.assertFalse(self.conn.in_transaction) + self.assertGreater(timeout, 0) + self.prepared += 1 + self.prepare_hook() + try: + yield Lease() + finally: + self.closed += 1 + + self.vector.prepare_embedding_target = prepare + self.modules["vector_search"] = self.vector + self.worker_module = load("tier_switch.worker") + self.worker_module.time = types.SimpleNamespace(monotonic=lambda: self.now, time=lambda: self.now) + self.worker = self.worker_module.RebuildWorker(self.conn, "custom", "custom", Throttler(), status_id=1, + status_callback=lambda count, total, metrics: self.live.append((count, total, metrics))) + self.worker._update_status = lambda *args, **kwargs: self.fail("Streamed worker wrote status") + self.worker._process_batch = lambda *args, **kwargs: self.fail("Streamed worker invoked full-list batch path") + self.worker._count_vectors = lambda *args, **kwargs: self.fail("Streamed worker recounted target vectors") + + def seed(self, ids): + self.conn.executemany("INSERT INTO messages VALUES (?,?,?,?,?)", [ + (mid, f"content-{mid}", "sender", "recipient", "2026-01-02T03:04:05") for mid in ids + ]) + self.conn.commit() + + def start(self): + return self.source.initialize_tier_source(self.conn, self.source.plan_tier_source( + self.conn, model_id=self.target.model_id, dimension=self.target.dimension, targets=TABLES, + )) + + def run_worker(self, count=8, *, page_size=64, plan=None): + if plan is None: + self.seed(range(1, count + 1)) + plan = self.start() + self.armed = True + return self.worker.run_source(self.target, plan, page_size=page_size) + + def maybe_fail(self, stage): + if stage == self.fail_stage and (self.failed_attempts is None or len(self.attempts) in self.failed_attempts): + workspace = Workspace() + self.refs.append(weakref.ref(workspace)) + raise self.error_factory() + + def observe_cleanup(self, kind): + gc.collect() + self.cleanup.append((kind, sys.exc_info()[0] is None, all(ref() is None for ref in self.refs))) + self.cleanup_hook(kind) + + def ids(self): + return tuple([row[0] for row in self.conn.execute(f'SELECT rowid FROM "{table}" ORDER BY rowid')] for table in TABLES) + + def test_order_metadata_exact_vectors_no_normalization_or_activation(self): + self.seed((-8, 0, 3, 9)) + result = self.run_worker(plan=self.start(), page_size=3) + self.assertEqual((result.status, result.processed, result.captured_range_complete, result.current_source_complete, result.activated), + ("complete", 4, True, True, False)) + self.assertEqual(self.ids(), ([-8, 0, 3, 9], [-8, 0, 3, 9])) + self.assertEqual(self.forwards, [ + ("completion", ["content--8", "content-0", "content-3"]), + ("separation", [f"sender to recipient on 2026-01-02: content-{mid}" for mid in (-8, 0, 3)]), + ("completion", ["content-9"]), ("separation", ["sender to recipient on 2026-01-02: content-9"]), + ]) + for table in TABLES: + self.assertEqual({row[0] for row in self.conn.execute(f'SELECT embedding FROM "{table}"')}, {struct.pack("2f", 3., 4.)}) + self.assertIs(self.vector._model, self.active_model) + self.assertEqual((self.vector.EMBEDDING_MODEL, self.vector._embedding_dim, self.vector._model_generation), ("serving-model", 99, 17)) + self.assertEqual(self.conn.execute("SELECT rowid,embedding FROM serving_pair").fetchone(), (42, b"\x01\x02")) + self.assertEqual(self.conn.execute("SELECT status FROM rebuild_status").fetchone()[0], "untouched") + self.assertTrue(all(not metrics["activated"] for _, _, metrics in self.live)) + self.assertEqual((self.prepared, self.closed), (1, 1)) + + def test_long_source_retains_only_bounded_pending_pages_without_full_fetch(self): + self.seed(range(1, 513)) + plan = self.start() + pages = [] + weak_pages = [] + read = self.source.read_tier_page + audit = self.source._audit_pair + audits = [] + + def bounded(conn, candidate, *, size=64): + self.assertLessEqual(size, 7) + gc.collect() + self.assertLessEqual(sum(ref() is not None for ref in weak_pages), 1) + page = read(conn, candidate, size=size) + pages.append(len(page.rows)) + weak_pages.append(weakref.ref(page)) + return page + + self.source.read_tier_page = bounded + self.source._audit_pair = lambda *args: (audits.append(True), audit(*args))[1] + result = self.run_worker(plan=plan, page_size=7) + self.assertEqual(result.processed, 512) + self.assertLessEqual(max(pages), 7) + self.assertLessEqual(max(map(len, self.attempts)), 7) + self.assertEqual(audits, []) + source_queries = [sql for sql in self.sql if sql.startswith("SELECT id, content")] + self.assertTrue(source_queries) + self.assertTrue(all("LIMIT ?" in sql for sql in source_queries)) + self.assertEqual(self.ids(), (list(range(1, 513)), list(range(1, 513)))) + + def test_empty_input_still_prepares_certifies_and_finishes(self): + result = self.run_worker(0) + self.assertEqual((self.prepared, self.closed, self.attempts), (1, 1, [])) + self.assertEqual((result.status, result.processed, result.current_source_complete), ("complete", 0, True)) + + def test_suffix_pair_failure_replans_exact_checkpoint_and_recovers(self): + self.sizes = [2, 4] + self.fail_stage = "insert_separation" + self.failed_attempts = {2} + plans = [] + original = self.source.plan_tier_source + + def replan(*args, **kwargs): + plan = original(*args, **kwargs) + plans.append((plan.manifest.generation, plan.manifest.cursor, plan.manifest.consumed)) + return plan + + self.seed(range(1, 7)) + plan = self.start() + self.source.plan_tier_source = replan + result = self.run_worker(plan=plan, page_size=6) + self.assertEqual(result.processed, 6) + self.assertEqual([batch[0] for batch in self.attempts], ["content-1", "content-3", "content-3"]) + self.assertEqual([len(batch) for batch in self.attempts], [2, 4, 4]) + self.assertEqual(plans, [(plan.manifest.generation, 2, 2)]) + self.assertEqual(self.ids(), (list(range(1, 7)), list(range(1, 7)))) + self.assertTrue(all(clear and released for _, clear, released in self.cleanup)) + + def test_tracebacks_and_arrays_release_before_all_recovery_cleanup(self): + for stage in ("completion", "separation", "serialize_completion", "serialize_separation", "insert_completion", "insert_separation", "checkpoint", "commit"): + with self.subTest(stage=stage): + case = TestStreamedWorker() + case.setUp() + try: + case.fail_stage = stage + result = case.run_worker(1) + self.assertEqual((result.status, result.processed), ("complete", 1)) + self.assertEqual(case.cleanup, [("backoff", True, True), ("flush", True, True)]) + finally: + case.doCleanups() + + def test_finite_halving_uses_actual_failed_prefix_and_cannot_renew_credits(self): + for admissions, expected in ( + ([32] * 20, [8, 8, 4, 4, 2, 2, 1, 1]), + (list(range(16, 0, -1)), None), + ([16, 32, 15, 32, 7, 32, 3, 32, 1, 32], None), + ([16, 1, 32], [8, 1, 1]), + ): + with self.subTest(admissions=admissions): + case = TestStreamedWorker() + case.setUp() + try: + case.sizes = admissions + case.fail_stage = "completion" + case.failed_attempts = None + result = case.run_worker(8) + counts = list(map(len, case.attempts)) + self.assertEqual((result.status, result.processed), ("oom_exhausted", 0)) + self.assertLessEqual(len(counts), 2 * (8).bit_length()) + self.assertEqual(counts[-2:], [1, 1]) + self.assertTrue(all(right <= left for left, right in zip(counts, counts[1:]))) + if expected is not None: + self.assertEqual(counts, expected) + finally: + case.doCleanups() + + def test_sql_oom_revalidation_does_not_reset_halving_credits(self): + self.fail_stage = "insert_separation" + self.failed_attempts = None + result = self.run_worker(8) + self.assertEqual((result.status, result.processed), ("oom_exhausted", 0)) + self.assertEqual(list(map(len, self.attempts)), [8, 8, 4, 4, 2, 2, 1, 1]) + self.assertEqual(self.ids(), ([], [])) + + def test_oom_budget_resets_only_after_committed_pair(self): + self.sizes = [8] * 20 + self.fail_stage = "completion" + self.failed_attempts = {1, 2, 4, 5, 6, 7, 8, 9} + result = self.run_worker(8) + self.assertEqual((result.status, result.processed), ("oom_exhausted", 4)) + self.assertEqual(list(map(len, self.attempts)), [8, 8, 4, 4, 4, 2, 2, 1, 1]) + self.assertEqual(self.ids(), ([1, 2, 3, 4], [1, 2, 3, 4])) + + def test_rollback_uncertainty_never_classifies_or_runs_later_sql(self): + for mode in ("dirty_raise", "clean_raise", "dirty_return"): + with self.subTest(mode=mode): + case = TestStreamedWorker() + case.setUp() + try: + case.seed((1,)) + plan = case.start() + case.fail_stage = "insert_separation" + case.rollback_mode = mode + case.worker._is_oom_error = lambda error: self.fail("Classified an uncertain rollback") + with self.assertRaises((MemoryError, RuntimeError)): + case.run_worker(plan=plan) + self.assertEqual(case.sql[-1], "ROLLBACK") + self.assertEqual(case.cleanup, []) + self.assertEqual((case.prepared, case.closed), (1, 1)) + finally: + case.doCleanups() + + def test_bare_memory_error_is_not_reclassified_as_retryable(self): + self.fail_stage = "separation" + self.error_factory = MemoryError + with self.assertRaises(MemoryError): + self.run_worker(1) + self.assertEqual(self.cleanup, []) + self.assertEqual(self.ids(), ([], [])) + + def test_expired_deadline_does_not_translate_uncertain_rollback_timeout(self): + self.fail_stage = "insert_separation" + self.rollback_mode = "clean_timeout" + with self.assertRaisesRegex(TimeoutError, "rollback timeout after cleanup"): + self.run_worker(1) + self.assertEqual(self.sql[-1], "ROLLBACK") + self.assertFalse(self.conn.in_transaction) + self.assertEqual(self.cleanup, []) + self.assertEqual((self.prepared, self.closed), (1, 1)) + + def test_commit_then_timeout_propagates_instead_of_returning_old_cursor(self): + self.seed((1,)) + plan = self.start() + self.commit_after_success_timeout = True + with self.assertRaisesRegex(TimeoutError, "timeout after committed pair"): + self.run_worker(plan=plan) + self.assertEqual(self.sql[-1], "ROLLBACK") + self.assertEqual(self.ids(), ([1], [1])) + durable = self.tracking.load_manifest(self.conn, self.tracking.manifest_key(TABLES)) + self.assertEqual((durable.cursor, durable.consumed), (1, 1)) + self.assertEqual(plan.manifest.consumed, 0) + self.assertEqual(self.cleanup, []) + self.assertEqual((self.prepared, self.closed), (1, 1)) + + def test_terminal_singleton_oom_cannot_hide_successful_pair_commit(self): + self.fail_stage = "completion" + self.failed_attempts = {1} + self.commit_after_success_oom = "exhausted" + with self.assertRaisesRegex(self.source.TierSourceUntrusted, "same source checkpoint"): + self.run_worker(1) + self.assertEqual(list(map(len, self.attempts)), [1, 1]) + self.assertEqual(self.ids(), ([1], [1])) + self.assertEqual(self.tracking.load_manifest(self.conn, self.tracking.manifest_key(TABLES)).consumed, 1) + + def test_cancel_or_deadline_after_ambiguous_commit_propagates_without_more_sql(self): + for stop in ("cancel", "timeout"): + with self.subTest(stop=stop): + case = TestStreamedWorker() + case.setUp() + try: + case.commit_after_success_oom = stop + with self.assertRaisesRegex(case.source.TierSourceUntrusted, "durable checkpoint revalidation"): + case.run_worker(1) + self.assertEqual(case.sql[-1], "ROLLBACK") + self.assertEqual(case.cleanup, []) + self.assertEqual(case.ids(), ([1], [1])) + self.assertEqual(len(case.attempts), 1) + finally: + case.doCleanups() + + def test_preparation_and_each_external_boundary_observe_cancellation(self): + for point in ("before_prepare", "prepare", "admission", "completion", "separation", "after_batch", "callback"): + with self.subTest(point=point): + case = TestStreamedWorker() + case.setUp() + try: + if point == "before_prepare": + case.worker.cancel() + elif point == "prepare": + case.prepare_hook = case.worker.cancel + elif point == "admission": + case.before_hook = case.worker.cancel + elif point in ("completion", "separation"): + case.encode_hook = lambda: case.worker.cancel() if case.stage == point else None + elif point == "after_batch": + case.after_hook = case.worker.cancel + else: + case.worker.status_callback = lambda *args: case.worker.cancel() + result = case.run_worker(2) + self.assertEqual(result.status, "cancelled") + self.assertFalse(result.activated) + self.assertEqual(result.processed, 2 if point in ("after_batch", "callback") else 0) + expected_calls = {"before_prepare": 0, "prepare": 0, "admission": 0, "completion": 1, "separation": 2, "after_batch": 2, "callback": 2} + self.assertEqual(len(case.forwards), expected_calls[point]) + self.assertEqual(case.closed, case.prepared) + finally: + case.doCleanups() + + def test_cancel_inside_sql_call_reports_committed_cursor_without_next_native(self): + self.write_hook = self.worker.cancel + self.sizes = [1] + result = self.run_worker(2) + self.assertEqual((result.status, result.processed, result.plan.manifest.cursor), ("cancelled", 1, 1)) + self.assertEqual(self.ids(), ([1], [1])) + self.assertEqual(len(self.forwards), 2) + + def test_cancellation_and_timeout_after_cleanup_prevent_retry(self): + for stop in ("cancelled", "timeout"): + for point in ("backoff", "flush", "admission"): + with self.subTest(stop=stop, point=point): + case = TestStreamedWorker() + case.setUp() + try: + case.fail_stage = "completion" + case.failed_attempts = None + + def stopped(): + if stop == "cancelled": + case.worker.cancel() + else: + case.now = 9001.0 + + case.cleanup_hook = lambda kind: stopped() if kind == point else None + case.before_hook = lambda: stopped() if point == "admission" and len(case.admissions) == 2 else None + result = case.run_worker(1) + self.assertEqual(result.status, stop) + self.assertEqual(len(case.attempts), 1) + self.assertEqual(result.processed, 0) + finally: + case.doCleanups() + + def test_timeout_after_prepare_and_during_encode_blocks_followup(self): + self.prepare_hook = lambda: setattr(self, "now", 9001.0) + result = self.run_worker(1) + self.assertEqual((result.status, len(self.forwards), self.closed), ("timeout", 0, 1)) + + def test_timeout_during_completion_stops_before_separation_and_sql(self): + self.encode_hook = lambda: setattr(self, "now", 9001.0) + result = self.run_worker(1) + self.assertEqual((result.status, len(self.forwards), result.processed), ("timeout", 1, 0)) + self.assertEqual(self.ids(), ([], [])) + self.assertEqual(self.closed, 1) + + def test_remaining_deadline_reaches_both_lease_calls(self): + self.encode_hook = lambda: setattr(self, "now", self.now + 10.0) + result = self.run_worker(1) + self.assertEqual(self.deadlines, [9000.0, 8990.0]) + self.assertEqual(result.status, "complete") + + def test_finish_cancellation_preserves_completed_manifest_truth(self): + finish = self.source.finish_tier_source + + def finish_then_cancel(*args): + result = finish(*args) + self.worker.cancel() + return result + + self.source.finish_tier_source = finish_then_cancel + result = self.run_worker(1) + self.assertEqual((result.status, result.processed, result.plan.manifest.complete), ("cancelled", 1, True)) + self.assertTrue(result.captured_range_complete) + self.assertTrue(result.current_source_complete) + self.assertFalse(result.activated) + + def test_revalidation_cannot_resume_another_generation(self): + self.seed((1,)) + plan = self.start() + self.fail_stage = "insert_separation" + original = self.source.plan_tier_source + + def switched(*args, **kwargs): + from dataclasses import replace + resumed = original(*args, **kwargs) + return replace(resumed, manifest=replace(resumed.manifest, generation="0" * 32)) + + self.source.plan_tier_source = switched + with self.assertRaisesRegex(self.source.TierSourceUntrusted, "same source checkpoint"): + self.run_worker(plan=plan) + self.assertEqual(len(self.attempts), 1) + self.assertEqual(self.ids(), ([], [])) + + def test_lease_call_cancellation_witness_does_not_claim_in_call_interruption(self): + admitted_native = [] + + def simulated_internal_wait_then_native(): + self.worker.cancel() + admitted_native.append("already-admitted-forward") + + self.encode_hook = simulated_internal_wait_then_native + result = self.run_worker(1) + self.assertEqual(admitted_native, ["already-admitted-forward"]) + self.assertEqual(len(self.forwards), 1) + self.assertEqual((result.status, result.processed), ("cancelled", 0)) + self.assertEqual(self.ids(), ([], [])) + + def test_borrowed_connection_and_identity_mismatch_fail_before_model_or_sql(self): + plan = self.start() + self.conn.execute("INSERT INTO metadata VALUES ('borrowed','pending')") + before = len(self.sql) + with self.assertRaisesRegex(RuntimeError, "clean owned"): + self.worker.run_source(self.target, plan) + self.assertEqual(len(self.sql), before) + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.prepared, 0) + self.conn.rollback() + self.worker.target_group = "different" + with self.assertRaisesRegex(ValueError, "identities"): + self.worker.run_source(self.target, plan) + self.assertEqual(self.prepared, 0) + + def test_source_change_after_native_failure_refuses_before_retry(self): + self.fail_stage = "completion" + + def correct(kind): + if kind == "backoff": + self.conn.execute("UPDATE messages SET content='corrected' WHERE id=1") + self.conn.commit() + + self.cleanup_hook = correct + with self.assertRaises(self.tracking.RebuildSourceChanged): + self.run_worker(1) + self.assertEqual(len(self.attempts), 1) + self.assertEqual(self.ids(), ([], [])) + + def test_captured_completion_does_not_claim_new_tail_or_activation(self): + self.seed((1, 2)) + self.start() + self.seed((3,)) + plan = self.source.plan_tier_source(self.conn, model_id=self.target.model_id, dimension=2, targets=TABLES) + result = self.run_worker(plan=plan) + self.assertEqual((result.captured_range_complete, result.current_source_complete, result.activated), (True, False, False)) + self.assertEqual(self.ids(), ([1, 2], [1, 2])) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-tier-target-model-795.py b/tests/test-tier-target-model-795.py new file mode 100644 index 00000000..bbe9f241 --- /dev/null +++ b/tests/test-tier-target-model-795.py @@ -0,0 +1,526 @@ +"""Server target identity regressions using real control flow and stdlib fakes.""" +from __future__ import annotations + +import ast +from concurrent.futures import ThreadPoolExecutor +from contextlib import contextmanager +from dataclasses import dataclass +import json +import logging +import math +import os +from pathlib import Path +import sys +import threading +import time +import types +import unittest +from unittest.mock import Mock, patch +import weakref + + +SOURCE = Path(__file__).resolve().parents[1] / "truememory" / "model_server.py" + + +class Array: + def __init__(self, rows: list[list[float]], shape: tuple[int, int]) -> None: + self.rows = rows + self.shape = shape + self.ndim = 2 + + def __setitem__(self, key: slice, value: Array) -> None: + self.rows[key] = value.rows + + +class Numpy: + ndarray = Array + float32 = "float32" + + @staticmethod + def asarray(values: Array, **_kwargs: object) -> Array: + return values + + @staticmethod + def empty(shape: tuple[int, int], **_kwargs: object) -> Array: + return Array([[] for _ in range(shape[0])], shape) + + +class FakeModel: + def __init__(self, identity: str, device: str | None) -> None: + self.identity = identity + self.device = device + self.calls: list[int] = [] + self.before_encode = lambda: None + self.oom_once = False + + def encode(self, texts: list[str], **kwargs: object) -> Array: + self.before_encode() + self.calls.append(len(texts)) + if self.oom_once: + self.oom_once = False + raise RuntimeError("MPS backend out of memory") + if len(texts) > kwargs["batch_size"]: + raise AssertionError("Native microbatch ceiling exceeded") + return Array([[1.0] * 256 for _ in texts], (len(texts), 256)) + + def to(self, device: str) -> None: + self.device = device + + +def load_server() -> types.ModuleType: + tree = ast.parse(SOURCE.read_text(encoding="utf-8")) + definitions = [node for node in tree.body if isinstance(node, (ast.FunctionDef, ast.ClassDef))] + tree = ast.fix_missing_locations(ast.Module(body=[ + ast.ImportFrom(module="__future__", names=[ast.alias(name="annotations")], level=0), + *definitions, + ], type_ignores=[])) + module = types.ModuleType("synthetic_target_server_795") + module.__dict__.update( + dataclass=dataclass, contextmanager=contextmanager, ThreadPoolExecutor=ThreadPoolExecutor, + np=Numpy(), json=json, math=math, os=os, sys=sys, threading=threading, time=time, + log=logging.getLogger("synthetic-target-795"), _MAX_MESSAGE_SIZE=100 * 1024**2, + PROTOCOL_VERSION=1, _EMBED_BATCH_LIMIT=32, _RERANK_BATCH_LIMIT=64, + ) + with patch.dict(sys.modules, {module.__name__: module}): + exec(compile(tree, str(SOURCE), "exec"), module.__dict__) + return module + + +class TestTierTargetIdentity(unittest.TestCase): + def setUp(self) -> None: + package = types.ModuleType("truememory") + package.__path__ = [] + self.vector = types.ModuleType("truememory.vector_search") + self.vector._TIER_ALIASES = {"edge": "model2vec", "base": "qwen3_256", "pro": "qwen3_256"} + self.vector.EMBEDDING_MODEL = "model2vec" + self.vector._embedding_dim = 256 + self.vector._model_generation = 7 + self.vector._model = object() + + def set_model(value: str) -> None: + self.vector.EMBEDDING_MODEL = self.vector._resolve_model_name(value) + self.vector._embedding_dim = 256 + self.vector._model_generation += 1 + self.vector._model = None + + self.vector.set_embedding_model = Mock(side_effect=set_model) + self.custom = types.ModuleType("truememory.tier_config") + self.custom.get_embed_model = Mock(return_value="synthetic/encoder-one") + vector_tree = ast.parse(SOURCE.with_name("vector_search.py").read_text(encoding="utf-8")) + resolver_nodes = [node for node in vector_tree.body if ( + isinstance(node, ast.FunctionDef) and node.name == "_resolve_model_name" + or isinstance(node, ast.Assign) and any( + isinstance(target, ast.Name) and target.id == "_REMOVED_MODELS" + for target in node.targets + ) + )] + self.vector.__dict__.update( + logger=logging.getLogger("synthetic-vector-795"), + _MODEL_DIMS={"model2vec": 256, "qwen3_256": 256, "minilm": 384, "bge-small": 384}, + _cfg_get_embed_model=self.custom.get_embed_model, + ) + exec(compile(ast.Module(body=resolver_nodes, type_ignores=[]), "synthetic-real-vector-resolver", "exec"), + self.vector.__dict__) + self.mps = types.ModuleType("truememory.mps_utils") + self.mps.resolve_device = lambda _device: "cpu" + self.mps.is_mps_oom = lambda error: "MPS backend out of memory" in str(error) + self.mps.flush_mps_cache = Mock() + modules = patch.dict(sys.modules, { + "truememory": package, "truememory.vector_search": self.vector, + "truememory.tier_config": self.custom, "truememory.mps_utils": self.mps, + }) + modules.start() + self.addCleanup(modules.stop) + environment = patch.dict(os.environ, {"TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD": "1"}) + environment.start() + self.addCleanup(environment.stop) + self.ms = load_server() + self.server = self.ms.ModelServer() + self.addCleanup(self.server._workers.shutdown, wait=True) + self.server._write_status_file = lambda: None + self.server._SUSTAINED_THRESHOLD = 1000 + self.builds: list[tuple[str, str | None]] = [] + self.server._build_embed_model = self.build + + def build(self, identity: str, device: str | None) -> FakeModel: + self.assertTrue(self.server._lock.locked()) + self.assertTrue(self.server._inference_lock.locked()) + self.builds.append((identity, device)) + return FakeModel(identity, device) + + def request(self, tier: str, count: int = 2, **extra: object) -> dict: + return self.server.handle_request({ + "op": "embed_batched", "tier": tier, "texts": ["synthetic"] * count, + "batch_size": 32, **extra, + }) + + def globals(self) -> tuple: + return (self.vector.EMBEDDING_MODEL, self.vector._embedding_dim, + self.vector._model_generation, self.vector._model) + + def test_all_builtin_alias_pairs_reuse_one_model_without_global_mutation(self) -> None: + for aliases in (("edge", "model2vec"), ("base", "pro", "qwen3_256")): + for first in aliases: + for second in aliases: + with self.subTest(first=first, second=second): + self.server._embed_state = None + self.builds.clear() + before = self.globals() + self.assertTrue(self.request(first)["ok"]) + snapshot = self.server._embed_state + self.assertTrue(self.request(second)["ok"]) + self.assertIs(self.server._embed_state.model, snapshot.model) + self.assertEqual(self.server._embed_state.tier, second) + self.assertEqual(snapshot.tier, first) + self.assertEqual(len(self.builds), 1) + self.assertEqual(self.globals(), before) + self.vector.set_embedding_model.assert_not_called() + + def test_explicit_target_does_not_change_later_default_request(self) -> None: + before = self.globals() + for tier, identity in (("", "model2vec"), ("base", "qwen3_256"), ("", "model2vec")): + self.assertTrue(self.request(tier)["ok"]) + self.assertEqual(self.server._embed_state.model_id, identity) + self.assertEqual(self.globals(), before) + self.assertEqual([identity for identity, _ in self.builds], + ["model2vec", "qwen3_256", "model2vec"]) + + def test_main_default_snapshot_survives_global_change_until_invalidated(self) -> None: + self.request("") + old = self.server._embed_state.model + self.vector.set_embedding_model("pro") + self.assertTrue(self.request("")["ok"]) + self.assertIs(self.server._embed_state.model, old) + self.assertEqual(self.server._embed_state.model_id, "model2vec") + self.assertEqual(len(self.builds), 1) + self.server._embed_state = None + self.assertTrue(self.request("")["ok"]) + self.assertIsNot(self.server._embed_state.model, old) + self.assertEqual(self.server._embed_state.model_id, "qwen3_256") + self.vector.set_embedding_model.assert_called_once_with("pro") + + def test_failed_distinct_load_empties_cache_preserves_globals_and_allows_reload(self) -> None: + self.request("edge") + old = weakref.ref(self.server._embed_state.model) + before = self.globals() + self.server._build_embed_model = Mock(side_effect=RuntimeError("synthetic load failure")) + with self.assertRaisesRegex(RuntimeError, "synthetic load failure"): + self.request("pro") + self.assertIsNone(self.server._embed_state) + self.assertIsNone(old()) + self.assertEqual(self.globals(), before) + self.assertEqual(self.server._inflight, 0) + self.assertFalse(self.server._lock.locked()) + self.assertFalse(self.server._inference_lock.locked()) + self.server._build_embed_model = self.build + self.assertTrue(self.request("edge")["ok"]) + self.assertEqual(self.server._embed_state.model_id, "model2vec") + self.assertEqual(len(self.builds), 2) + + def test_removed_model_rejection_matches_real_resolver_without_mutation(self) -> None: + self.request("base") + before = self.globals() + snapshot = self.server._embed_state + builds = list(self.builds) + for removed in self.vector._REMOVED_MODELS: + for spelling in (removed, removed.upper(), " " + removed + " ", "\t" + removed.upper() + "\n"): + with self.subTest(spelling=spelling): + with self.assertRaises(ValueError) as reference: + self.vector._resolve_model_name(spelling) + with self.assertRaises(ValueError) as direct: + self.server._resolve_embed_model_id(spelling) + self.assertEqual(str(direct.exception), str(reference.exception)) + for count in (1, 2): + with self.assertRaises(ValueError) as request: + self.request(spelling, count=count) + self.assertEqual(str(request.exception), str(reference.exception)) + self.assertIs(self.server._embed_state, snapshot) + self.assertEqual(self.globals(), before) + self.assertEqual(self.builds, builds) + self.assertFalse(self.server._lock.locked()) + self.assertFalse(self.server._inference_lock.locked()) + self.vector.set_embedding_model.assert_not_called() + + def test_other_resolver_spellings_keep_existing_peek_behavior(self) -> None: + before = self.globals() + for spelling in ("base", "pro", "qwen3_256", "custom", " PRO ", "invalid-tier", "Synthetic/CaseSensitive"): + with self.subTest(spelling=spelling): + self.assertEqual(self.server._resolve_embed_model_id(spelling), + self.server._peek_embed_model_id(spelling)) + self.assertEqual(self.globals(), before) + self.vector.set_embedding_model.assert_not_called() + + def test_explicit_custom_cache_keeps_original_config_and_opt_in_semantics(self) -> None: + self.request("custom") + original = self.server._embed_state + self.custom.get_embed_model.side_effect = ValueError("synthetic invalid custom settings") + with patch.dict(os.environ, {"TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD": "0"}): + self.assertTrue(self.request("custom")["ok"]) + self.assertIs(self.server._embed_state, original) + self.assertEqual(len(self.builds), 1) + self.custom.get_embed_model.side_effect = None + self.custom.get_embed_model.return_value = "synthetic/encoder-two" + self.assertTrue(self.request("custom")["ok"]) + self.assertIs(self.server._embed_state, original) + # A different explicit key retains the legacy custom reload boundary. + self.assertTrue(self.request("synthetic/encoder-one")["ok"]) + self.assertIsNot(self.server._embed_state.model, original.model) + self.assertEqual(len(self.builds), 2) + + def test_legacy_fallback_identities_are_not_canonicalized_as_builtin_aliases(self) -> None: + for tier in ("minilm", "bge-small", "edge"): + self.assertTrue(self.request(tier)["ok"]) + self.assertEqual([identity for identity, _ in self.builds], ["minilm", "bge-small", "model2vec"]) + static = types.ModuleType("model2vec") + static.StaticModel = types.SimpleNamespace(from_pretrained=Mock(return_value=object())) + with patch.dict(sys.modules, {"model2vec": static}): + for identity in ("minilm", "bge-small"): + self.ms.ModelServer._build_embed_model(identity, "cpu") + self.assertEqual(static.StaticModel.from_pretrained.call_count, 2) + for call in static.StaticModel.from_pretrained.call_args_list: + self.assertEqual(call.args, ("minishlab/potion-base-8M",)) + self.assertEqual(call.kwargs, {"force_download": False}) + + def test_raw_custom_builtin_or_fallback_snapshot_survives_config_changes(self) -> None: + for initial in ("qwen3_256", "model2vec", None): + for opt_out in (False, True): + with self.subTest(initial=initial, opt_out=opt_out): + self.server._embed_state = None + self.builds.clear() + self.custom.get_embed_model.return_value = initial + self.custom.get_embed_model.side_effect = ( + ValueError("synthetic missing custom settings") if initial is None else None + ) + self.assertTrue(self.request("custom")["ok"]) + original = self.server._embed_state + self.assertEqual(original.model_id, initial or "model2vec") + self.custom.get_embed_model.reset_mock() + self.custom.get_embed_model.return_value = "synthetic/encoder-two" + self.custom.get_embed_model.side_effect = ( + ValueError("synthetic custom opt-out") if opt_out else None + ) + with patch.dict(os.environ, {"TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD": "0" if opt_out else "1"}): + with patch.object(self.ms, "_check_result_size") as check: + self.server._preflight_embed_result("custom", 2) + check.assert_called_once_with((2, 256), "vectors") + self.assertTrue(self.request("custom")["ok"]) + self.assertIs(self.server._embed_state, original) + self.assertEqual(len(self.builds), 1) + self.custom.get_embed_model.assert_not_called() + + def test_preflight_uses_same_resolved_identity_and_custom_cache_boundary(self) -> None: + self.request("") + self.vector.EMBEDDING_MODEL = "synthetic/encoder-one" + with patch.object(self.ms, "_check_result_size") as check: + self.server._preflight_embed_result("", 2) + check.assert_called_once_with((2, 256), "vectors") + check.reset_mock() + self.server._embed_state = None + self.server._preflight_embed_result("", 2) + check.assert_not_called() + self.request("custom") + with patch.dict(os.environ, {"TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD": "0"}): + with patch.object(self.ms, "_check_result_size") as check: + self.server._preflight_embed_result("custom", 2) + check.assert_not_called() + self.server._preflight_embed_result("synthetic/other", 2) + check.assert_called_once_with((2, 256), "vectors") + self.request("base") + with patch.object(self.ms, "_check_result_size") as check: + self.server._preflight_embed_result("pro", 2) + check.assert_called_once_with((2, 256), "vectors") + + def test_fast_and_main_lanes_keep_captured_default_identity(self) -> None: + self.vector.EMBEDDING_MODEL = "base" + self.request("") + snapshot = self.server._embed_state + self.vector.EMBEDDING_MODEL = "edge" + + def fast_build(identity: str, device: str | None) -> FakeModel: + self.assertTrue(self.server._fast_lock.locked()) + self.assertEqual((identity, device), ("qwen3_256", "cpu")) + return FakeModel(identity, device) + + self.server._build_embed_model = fast_build + with self.server._lock: + response = self.server._handle_fast_embed(["synthetic"], "") + self.assertTrue(response["ok"]) + self.assertIs(self.server._embed_state, snapshot) + self.assertEqual(self.server._fast_model_id, "qwen3_256") + self.server._build_embed_model = self.build + self.request("") + self.assertIs(self.server._embed_state, snapshot) + # A different explicit key leaves the original raw-default cache. + self.request("edge") + self.request("") + self.assertEqual(self.server._embed_state.model_id, "model2vec") + + def test_waiting_default_query_reuses_identity_captured_during_load(self) -> None: + self.vector.EMBEDDING_MODEL = "base" + build_started = threading.Event() + release_build = threading.Event() + query_waiting = threading.Event() + responses: dict[str, dict] = {} + failures: list[BaseException] = [] + main_lock = self.server._lock + + class ObservedLock: + def acquire(inner_self, blocking: bool = True, timeout: float = -1) -> bool: + if threading.current_thread().name == "synthetic-waiting-query" and blocking: + query_waiting.set() + return main_lock.acquire(blocking=blocking, timeout=timeout) + + def release(inner_self) -> None: + main_lock.release() + + def locked(inner_self) -> bool: + return main_lock.locked() + + self.server._lock = ObservedLock() + + def build(identity: str, device: str | None) -> FakeModel: + model = self.build(identity, device) + if threading.current_thread().name == "synthetic-default-loader": + build_started.set() + if not release_build.wait(2): + raise AssertionError("Synthetic model load was not released") + return model + + def request(key: str, count: int) -> None: + try: + responses[key] = self.request("", count=count, deadline=time.time() + 3) + except BaseException as error: + failures.append(error) + + self.server._build_embed_model = build + loader = threading.Thread(name="synthetic-default-loader", target=request, args=("batch", 2)) + query = threading.Thread(name="synthetic-waiting-query", target=request, args=("query", 1)) + loader.start() + try: + self.assertTrue(build_started.wait(2)) + self.assertIsNone(self.server._embed_state) + self.vector.EMBEDDING_MODEL = "edge" + query.start() + self.assertTrue(query_waiting.wait(2)) + self.assertNotIn("query", responses) + finally: + release_build.set() + loader.join(5) + if query.ident is not None: + query.join(5) + self.assertFalse(loader.is_alive()) + self.assertFalse(query.is_alive()) + self.assertEqual(failures, []) + self.assertEqual(set(responses), {"batch", "query"}) + self.assertTrue(all(response["ok"] for response in responses.values())) + self.assertEqual(self.builds, [("qwen3_256", "cpu")]) + self.assertEqual(self.server._embed_state.model.calls, [2, 1]) + self.assertEqual(self.server._embed_state.model_id, "qwen3_256") + self.assertFalse(main_lock.locked()) + self.assertFalse(self.server._inference_lock.locked()) + + def test_alias_publication_preserves_snapshot_for_fast_lane(self) -> None: + self.request("base") + snapshot = self.server._embed_state + self.request("pro") + with self.server._lock: + self.server._fast_encoder = FakeModel("qwen3_256", "cpu") + self.server._fast_model_id = "qwen3_256" + self.server._fast_generation = self.server._embed_state.generation + self.assertTrue(self.server._handle_fast_embed(["synthetic"], "pro")["ok"]) + self.assertEqual(snapshot.tier, "base") + self.assertIs(snapshot.model, self.server._embed_state.model) + + def test_expired_request_cannot_load_or_relabel_cached_alias(self) -> None: + self.request("base") + snapshot = self.server._embed_state + response = self.request("pro", deadline=time.time() - 1) + self.assertFalse(response["ok"]) + self.assertIn("deadline", response["error"]) + self.assertIs(self.server._embed_state, snapshot) + self.assertEqual(len(self.builds), 1) + + def test_distinct_target_cannot_load_while_existing_main_inference_is_owned(self) -> None: + self.request("edge") + entered = threading.Event() + release = threading.Event() + results: list[dict] = [] + + def blocked_encode() -> None: + self.assertTrue(self.server._inference_lock.locked()) + entered.set() + if not release.wait(2): + raise AssertionError("Synthetic inference was not released") + + self.server._embed_state.model.before_encode = blocked_encode + worker = threading.Thread(target=lambda: results.append(self.request("edge"))) + worker.start() + try: + self.assertTrue(entered.wait(2)) + response = self.request("pro", deadline=time.time() + 0.05) + self.assertFalse(response["ok"]) + self.assertIn("deadline", response["error"]) + self.assertEqual(self.builds, [("model2vec", "cpu")]) + finally: + release.set() + worker.join(2) + self.assertFalse(worker.is_alive()) + self.assertEqual(len(results), 1) + self.assertTrue(results[0]["ok"]) + self.request("pro") + self.assertEqual(self.builds[-1], ("qwen3_256", "cpu")) + + def test_alias_reuse_retains_native_batch_ceiling_and_sticky_recovery(self) -> None: + self.request("base") + model = self.server._embed_state.model + model.oom_once = True + self.assertTrue(self.request("pro", count=35)["ok"]) + self.assertEqual(model.calls, [2, 32, 32, 3]) + self.assertEqual(self.server._sticky_cpu, {"embed"}) + self.assertEqual(model.device, "cpu") + self.assertEqual(len(self.builds), 1) + + def test_expired_oom_invalidation_reloads_current_default_on_sticky_cpu(self) -> None: + now = [1000.0] + clock = types.SimpleNamespace(time=lambda: now[0], monotonic=lambda: now[0]) + self.vector.EMBEDDING_MODEL = "base" + self.mps.resolve_device = lambda _device: "mps" + with patch.object(self.ms, "time", clock): + self.request("") + failed = self.server._embed_state.model + failed.oom_once = True + + def expired_native_work() -> None: + now[0] += 2 + self.vector.EMBEDDING_MODEL = "edge" + + failed.before_encode = expired_native_work + response = self.request("", deadline=now[0] + 1) + self.assertFalse(response["ok"]) + self.assertIn("deadline", response["error"]) + self.assertIsNone(self.server._embed_state) + self.assertEqual(self.server._sticky_cpu, {"embed"}) + self.mps.flush_mps_cache.assert_not_called() + self.assertEqual(failed.device, "mps") + self.assertTrue(self.request("")["ok"]) + self.assertEqual(self.builds, [("qwen3_256", "mps"), ("model2vec", "cpu")]) + self.assertEqual(self.server._embed_state.model_id, "model2vec") + self.assertEqual(self.server._embed_state.model.device, "cpu") + + def test_old_instance_is_released_before_distinct_load(self) -> None: + self.request("edge") + old = weakref.ref(self.server._embed_state.model) + observed: list[bool] = [] + + def build(identity: str, device: str | None) -> FakeModel: + observed.append(old() is not None) + return self.build(identity, device) + + self.server._build_embed_model = build + self.request("base") + self.assertEqual(observed, [False]) + self.assertIsNone(old()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-tier-warm-admission-795.py b/tests/test-tier-warm-admission-795.py new file mode 100644 index 00000000..b566e223 --- /dev/null +++ b/tests/test-tier-warm-admission-795.py @@ -0,0 +1,494 @@ +"""Fresh public admission against real lifecycle code and memory SQLite only.""" +from __future__ import annotations + +import contextlib +from pathlib import Path +import runpy +import threading +import time +import types +import unittest +from unittest.mock import Mock, patch + +ACTIVE = runpy.run_path(str(Path(__file__).with_name("test-tier-active-initialization-795.py"))) +enter_context = ACTIVE["enter_context"] + + +class TestWarmAdmission(unittest.TestCase): + def setUp(self): + self.active = ACTIVE["TestActiveInitialization"]() + self.addCleanup(self.active.doCleanups) + self.active.setUp() + self.fixture = self.active.fixture + self.api, self.maintenance = self.active.api, self.active.maintenance + self.coordinator = self.active.coordinator + self.gate = self.fixture.fixture.gate + self.maintenance.threading.TIMEOUT_MAX = threading.TIMEOUT_MAX + loaded = ACTIVE["definitions"]("maintenance.py", {"wait_for_automatic_owner"}, + {**self.maintenance.__dict__, "time": time}) + self.maintenance.wait_for_automatic_owner = loaded.wait_for_automatic_owner + self.opened = self.fixture.memory_connection() + self.fixture.engine_namespace["create_db"] = Mock(return_value=self.opened) + self.errors = [] + initialization = ACTIVE["definitions"]("engine.py", { + "_initialize_connection", "_purge_legacy_entity_profile_summaries", + "_maybe_startup_consolidate", "_maybe_auto_consolidate"}, + {**self.fixture.engine_namespace, "_HAS_HYBRID": False, "os": __import__("os")}, methods=True) + for name in ("_initialize_connection", "_purge_legacy_entity_profile_summaries", + "_maybe_startup_consolidate", "_maybe_auto_consolidate"): + setattr(self.active.first.__class__, name, getattr(initialization, name)) + self.active.first.__class__._has_consolidation = False + + def drifted_candidate(self): + candidate = self.fixture.candidate(self.active.first) + candidate.execute("CREATE TABLE synthetic_changed_schema(value INTEGER)") + return candidate + + def run_public(self, engine): + try: + self.api.open_engine_for_operation(engine) + except BaseException as error: + self.errors.append(error) + + def observe_wait(self, action=None): + entered = threading.Event() + actual = self.maintenance.wait_for_automatic_owner + def wait(error, *, deadline): + self.assertIsNone(self.api.current_runtime_operation()) + self.assertFalse(self.gate.current_thread_admitted()) + self.assertTrue(self.maintenance._registry_lock.acquire(blocking=False)) + self.maintenance._registry_lock.release() + self.assertTrue(self.coordinator._mutex.acquire(blocking=False)) + self.coordinator._mutex.release() + entered.set() + if action is not None: + action() + return actual(error, deadline=deadline) + enter_context(self, patch.object(self.maintenance, "wait_for_automatic_owner", side_effect=wait)) + return entered + + def test_schema_drift_waits_then_uses_normal_factory_without_stale_readiness(self): + self.active.start() + candidate = self.drifted_candidate() + engine = self.active.fresh() + entered = self.observe_wait() + worker = threading.Thread(target=self.run_public, args=(engine,)) + worker.start() + try: + self.assertTrue(entered.wait(2)) + self.assertIsNone(engine.conn) + self.assertFalse(engine.ready) + self.assertTrue(engine._write_lock.acquire(blocking=False)) + engine._write_lock.release() + self.assertTrue(engine._init_lock.acquire(blocking=False)) + engine._init_lock.release() + self.fixture.assert_closed(candidate) + self.active.release.set() + worker.join(2) + self.assertFalse(worker.is_alive()) + self.assertEqual(self.errors, []) + self.assertIs(engine.conn, self.opened) + self.assertFalse(engine.ready) + self.assertIsNone(engine._reconnect_receipt) + self.assertEqual(self.fixture.fixture.loads, []) + self.fixture.engine_namespace["create_db"].assert_called_once() + finally: + self.active.release.set() + worker.join(3) + + def test_private_opener_still_refuses_same_schema_drift_without_wait(self): + self.active.start() + self.drifted_candidate() + engine = self.active.fresh() + with patch.object(self.maintenance, "wait_for_automatic_owner") as wait: + with self.assertRaises(self.maintenance.MaintenanceBusyError): + engine._open_connection_handle() + wait.assert_not_called() + self.assertIsNone(engine.conn) + + def test_successful_first_attempt_preserves_existing_raw_reader_and_exclusive_behavior(self): + for context in (lambda: self.gate.operation_lease(("synthetic",)), self.gate.exclusive_activation): + with self.subTest(context=context), context(): + engine = self.active.fresh() + with patch.object(self.maintenance, "wait_for_automatic_owner") as wait: + self.api.open_engine_for_operation(engine) + self.assertIs(engine.conn, self.opened) + wait.assert_not_called() + + def test_busy_runtime_raw_reader_and_exclusive_scopes_never_wait(self): + self.active.start() + scopes = (lambda: self.api.serving_operation(self.fixture.conn), + lambda: self.gate.operation_lease(("synthetic",)), self.gate.exclusive_activation) + for scope in scopes: + with self.subTest(scope=scope), scope(): + self.drifted_candidate() + engine = self.active.fresh() + with patch.object(self.maintenance, "wait_for_automatic_owner") as wait: + with self.assertRaises(self.maintenance.MaintenanceBusyError): + self.api.open_engine_for_operation(engine) + wait.assert_not_called() + self.assertIsNone(engine.conn) + + def test_current_writer_refuses_before_nonreentrant_open(self): + engine = self.active.fresh() + with engine._write_lock, patch.object(engine, "_open_connection_handle") as opened: + with self.assertRaises(self.maintenance.MaintenanceBusyError): + self.api.open_engine_for_operation(engine) + opened.assert_not_called() + + def test_existing_connection_and_caller_sql_never_enter_wait_loop(self): + engine = self.active.first + engine.conn.execute("BEGIN") + error = self.maintenance.MaintenanceBusyError("synthetic existing connection") + with patch.object(engine, "_open_connection_handle", side_effect=error) as opened, \ + patch.object(self.maintenance, "wait_for_automatic_owner") as wait: + with self.assertRaises(self.maintenance.MaintenanceBusyError) as raised: + self.api.open_engine_for_operation(engine) + self.assertIs(raised.exception, error) + self.assertTrue(engine.conn.in_transaction) + opened.assert_called_once() + wait.assert_not_called() + engine.conn.rollback() + + def test_manual_receiptless_and_foreign_owners_have_no_wait_permission(self): + for kind in ("manual", "receiptless", "foreign"): + with self.subTest(kind=kind): + if kind == "manual": + self.assertTrue(self.coordinator.request(self.active.work)) + self.assertTrue(self.active.entered.wait(2)) + elif kind == "receiptless": + self.active.start(receipt=False) + else: + self.maintenance._held_paths[self.active.first.db_path] = 991 + try: + with self.assertRaises(self.maintenance.MaintenanceBusyError) as raised: + self.api.open_engine_for_operation(self.active.fresh()) + self.assertIsNone(getattr(raised.exception, "_automatic_owner", None)) + finally: + if kind == "foreign": + self.maintenance._held_paths.clear() + else: + self.active.release.set() + self.assertTrue(self.coordinator.wait(2)) + self.active.entered.clear() + self.active.release.clear() + self.active.worker_conn = self.fixture.memory_connection() + + def test_launch_gap_wait_origin_precedes_active_receipt_publication(self): + acquired, continue_launch = threading.Event(), threading.Event() + actual = self.coordinator._begin_observation + def observe(): + if not acquired.is_set(): + acquired.set() + if not continue_launch.wait(3): + raise AssertionError("Synthetic launch barrier timed out") + return actual() + enter_context(self, patch.object(self.coordinator, "_begin_observation", side_effect=observe)) + requester = threading.Thread(target=lambda: self.coordinator.request_layers( + _initialization_receipt=self.active.first._reconnect_receipt)) + requester.start() + engine = self.active.fresh() + entered = self.observe_wait() + worker = threading.Thread(target=self.run_public, args=(engine,)) + try: + self.assertTrue(acquired.wait(2)) + self.assertIsNone(self.active.record()) + self.assertIsNotNone(self.coordinator._admission_owner) + worker.start() + self.assertTrue(entered.wait(2)) + continue_launch.set() + self.active.release.set() + worker.join(2) + self.assertFalse(worker.is_alive()) + self.assertEqual(self.errors, []) + self.assertIs(engine.conn, self.opened) + finally: + continue_launch.set() + self.active.release.set() + requester.join(3) + if worker.ident is not None: + worker.join(3) + + def test_teardown_gap_keeps_wait_origin_until_actual_descriptor_release(self): + self.active.start() + at_release, continue_release = threading.Event(), threading.Event() + actual = self.maintenance._release_owner + def release(fd): + at_release.set() + if not continue_release.wait(3): + raise AssertionError("Synthetic release barrier timed out") + return actual(fd) + enter_context(self, patch.object(self.maintenance, "_release_owner", side_effect=release)) + self.active.release.set() + self.assertTrue(at_release.wait(2)) + self.assertIsNone(self.active.record()) + origin = self.coordinator._admission_owner + self.assertIsNotNone(origin) + entered = self.observe_wait() + engine = self.active.fresh() + worker = threading.Thread(target=self.run_public, args=(engine,)) + worker.start() + try: + self.assertTrue(entered.wait(2)) + self.assertFalse(origin.released.is_set()) + continue_release.set() + worker.join(2) + self.assertFalse(worker.is_alive()) + self.assertEqual(self.errors, []) + self.assertTrue(origin.released.is_set()) + self.assertIsNone(self.coordinator._admission_owner) + finally: + continue_release.set() + worker.join(3) + + def test_cancel_wakes_waiter_without_releasing_owner_or_retrying(self): + self.active.start() + self.drifted_candidate() + engine = self.active.fresh() + entered = self.observe_wait() + worker = threading.Thread(target=self.run_public, args=(engine,)) + worker.start() + try: + self.assertTrue(entered.wait(2)) + origin = self.coordinator._admission_owner + self.coordinator.cancel() + worker.join(2) + self.assertFalse(worker.is_alive()) + self.assertEqual(len(self.errors), 1) + self.assertIs(type(self.errors[0]), self.maintenance.MaintenanceBusyError) + self.assertTrue(origin.cancelled.is_set()) + self.assertIn(self.active.first.db_path, self.maintenance._held_paths) + self.assertIsNone(engine.conn) + self.fixture.engine_namespace["create_db"].assert_not_called() + finally: + self.active.release.set() + worker.join(3) + + def test_timeout_leaves_owner_and_engine_untouched(self): + self.active.start() + candidate = self.drifted_candidate() + engine = self.active.fresh() + with patch.object(self.fixture.modules["storage"], "DEFAULT_BUSY_TIMEOUT_MS", 20): + with self.assertRaises(self.maintenance.MaintenanceBusyError): + self.api.open_engine_for_operation(engine) + self.assertIsNone(engine.conn) + self.assertFalse(engine.ready) + self.assertFalse(self.active.release.is_set()) + self.assertIn(self.active.first.db_path, self.maintenance._held_paths) + self.fixture.assert_closed(candidate) + + def test_successive_automatic_generations_share_one_deadline(self): + self.active.start() + self.drifted_candidate() + deadlines = [] + actual = self.maintenance.wait_for_automatic_owner + def wait(error, *, deadline): + deadlines.append(deadline) + self.active.release.set() + actual(error, deadline=deadline) + self.assertTrue(self.coordinator.wait(2)) + if len(deadlines) == 1: + self.active.entered.clear() + self.active.release.clear() + self.active.worker_conn = self.fixture.memory_connection() + self.active.start() + self.drifted_candidate() + engine = self.active.fresh() + with patch.object(self.maintenance, "wait_for_automatic_owner", side_effect=wait): + self.api.open_engine_for_operation(engine) + self.assertEqual(len(deadlines), 2) + self.assertEqual(deadlines[0], deadlines[1]) + self.assertIs(engine.conn, self.opened) + + def test_context_is_rechecked_after_wake_before_retry(self): + self.active.start() + self.drifted_candidate() + engine = self.active.fresh() + stack = contextlib.ExitStack() + self.addCleanup(stack.close) + actual = self.maintenance.wait_for_automatic_owner + def wait(error, *, deadline): + self.active.release.set() + actual(error, deadline=deadline) + stack.enter_context(self.gate.operation_lease(("synthetic",))) + with patch.object(self.maintenance, "wait_for_automatic_owner", side_effect=wait): + with self.assertRaises(self.maintenance.MaintenanceBusyError): + self.api.open_engine_for_operation(engine) + self.assertIsNone(engine.conn) + self.fixture.engine_namespace["create_db"].assert_not_called() + + def test_busy_origin_survives_release_before_wait_is_called(self): + self.active.start() + self.drifted_candidate() + engine = self.active.fresh() + actual = engine._open_connection_handle + calls = [] + def opening(): + calls.append(None) + try: + return actual() + except self.maintenance.MaintenanceBusyError as error: + self.assertIsNotNone(error._automatic_owner) + self.active.release.set() + self.assertTrue(self.coordinator.wait(2)) + self.assertTrue(error._automatic_owner.released.is_set()) + self.assertIsNone(self.coordinator._admission_owner) + raise + with patch.object(engine, "_open_connection_handle", side_effect=opening): + self.api.open_engine_for_operation(engine) + self.assertEqual(len(calls), 2) + self.assertIs(engine.conn, self.opened) + + def test_receiptless_successor_cannot_inherit_wait_permission(self): + self.active.start() + self.drifted_candidate() + actual = self.maintenance.wait_for_automatic_owner + origins, deadlines = [], [] + def wait(error, *, deadline): + origins.append(getattr(error, "_automatic_owner", None)) + deadlines.append(deadline) + if len(origins) == 1: + self.active.release.set() + actual(error, deadline=deadline) + self.assertTrue(self.coordinator.wait(2)) + self.active.entered.clear() + self.active.release.clear() + self.active.worker_conn = self.fixture.memory_connection() + self.active.start(receipt=False) + engine = self.active.fresh() + with patch.object(self.maintenance, "wait_for_automatic_owner", side_effect=wait): + with self.assertRaises(self.maintenance.MaintenanceBusyError): + self.api.open_engine_for_operation(engine) + self.assertEqual(len(origins), 2) + self.assertIsNotNone(origins[0]) + self.assertIsNone(origins[1]) + self.assertEqual(deadlines[0], deadlines[1]) + self.assertIsNone(engine.conn) + self.fixture.engine_namespace["create_db"].assert_not_called() + + def test_cancelled_origin_stays_cancelled_after_later_launch_and_fd_reuse(self): + self.active.start() + with self.assertRaises(self.maintenance.MaintenanceBusyError) as raised: + with self.maintenance.maintenance_owner(self.active.first.db_path): + self.fail("Automatic ownership was stolen") + old = raised.exception._automatic_owner + self.coordinator.cancel() + self.active.release.set() + self.assertTrue(self.coordinator.wait(2)) + self.active.entered.clear() + self.active.release.clear() + self.active.worker_conn = self.fixture.memory_connection() + self.active.start() + current = self.coordinator._admission_owner + self.assertEqual(old.descriptor, current.descriptor) + self.assertNotEqual(old.launch, current.launch) + self.assertTrue(old.cancelled.is_set()) + self.assertFalse(current.cancelled.is_set()) + with self.assertRaises(self.maintenance.MaintenanceBusyError) as again: + self.maintenance.wait_for_automatic_owner(raised.exception, deadline=time.monotonic() + 10) + self.assertIs(again.exception, raised.exception) + + def test_decorator_fact_and_ensure_connection_use_public_helper(self): + called = [] + engine = self.active.fresh() + actual = self.api.open_engine_for_operation + def opening(value): + called.append(value) + return actual(value) + @self.api.engine_operation + def body(value): + self.assertIsNotNone(self.api.current_runtime_operation()) + return value.conn + with patch.object(self.api, "open_engine_for_operation", side_effect=opening): + self.assertIs(body(engine), self.opened) + with self.api.fact_operation(types.SimpleNamespace(_engine=engine)): + self.assertIsNotNone(self.api.current_runtime_operation()) + methods = ACTIVE["definitions"]("engine.py", {"_ensure_connection"}, + self.fixture.namespace(), methods=True) + engine._initialize_connection = Mock() + methods._ensure_connection(engine) + self.assertEqual(called, [engine, engine, engine]) + engine._initialize_connection.assert_called_once_with(_suppress_maintenance=False) + + def test_gate_query_refuses_contention_without_waiting(self): + held, release = threading.Event(), threading.Event() + def holder(): + with self.gate._DEFAULT_GATE._condition: + held.set() + release.wait(3) + thread = threading.Thread(target=holder) + thread.start() + try: + self.assertTrue(held.wait(2)) + with self.assertRaises(self.gate.ServingLeaseError): + self.gate.current_thread_admitted() + finally: + release.set() + thread.join(3) + + def test_wait_eligibility_refuses_registry_and_coordinator_contention(self): + self.active.start() + with self.assertRaises(self.maintenance.MaintenanceBusyError) as raised: + with self.maintenance.maintenance_owner(self.active.first.db_path): + self.fail("Automatic ownership was stolen") + error = raised.exception + for lock in (self.maintenance._registry_lock, self.coordinator._mutex): + with self.subTest(lock=lock): + entered, done = threading.Event(), threading.Event() + errors = [] + def waiting(): + entered.set() + try: + self.maintenance.wait_for_automatic_owner(error, deadline=time.monotonic() + 0.035) + except BaseException as failure: + errors.append(failure) + finally: + done.set() + with lock: + worker = threading.Thread(target=waiting) + worker.start() + self.assertTrue(entered.wait(2)) + finished_while_held = done.wait(0.1) + worker.join(2) + self.assertTrue(finished_while_held) + self.assertEqual(errors, [error]) + self.assertFalse(self.active.release.is_set()) + + def test_runtime_presence_refuses_without_validating_lease_under_contended_gate(self): + self.active.start() + self.drifted_candidate() + engine = self.active.fresh() + admitted_connection = self.fixture.memory_connection() + admitted, proceed, caught = threading.Event(), threading.Event(), threading.Event() + errors = [] + def nested(): + with self.api.serving_operation(admitted_connection): + admitted.set() + if not proceed.wait(3): + raise AssertionError("Synthetic runtime barrier timed out") + try: + self.api.open_engine_for_operation(engine) + except BaseException as error: + errors.append(error) + finally: + caught.set() + worker = threading.Thread(target=nested) + worker.start() + try: + self.assertTrue(admitted.wait(2)) + with self.gate._DEFAULT_GATE._condition: + proceed.set() + refused_while_held = caught.wait(0.1) + worker.join(2) + self.assertTrue(refused_while_held) + self.assertFalse(worker.is_alive()) + self.assertEqual(len(errors), 1) + self.assertIs(type(errors[0]), self.maintenance.MaintenanceBusyError) + self.assertIsNone(engine.conn) + finally: + proceed.set() + worker.join(3) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-tier-writer-fence-795.py b/tests/test-tier-writer-fence-795.py new file mode 100644 index 00000000..f62dfe99 --- /dev/null +++ b/tests/test-tier-writer-fence-795.py @@ -0,0 +1,643 @@ +"""Real in-memory writer transactions with synthetic selected records and models. + +Certificate provenance and native vec0 are tested by their own gates. These +tests exercise actual journal parsing, SQL integrity, storage, Engine method +bodies, and vector writer control flow without a model or file-backed database. +""" + +from __future__ import annotations + +import ast +import builtins +import dataclasses +import json +import logging +import runpy +import sqlite3 +import sys +import threading +import types +import unittest +import uuid +from contextlib import contextmanager +from pathlib import Path +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] +BASE = runpy.run_path(str(Path(__file__).with_name("test-rebuild-source-streaming-756.py"))) + + +class Cancelled(BaseException): + pass + + +class Connection(sqlite3.Connection): + armed = False + mode = "" + failure: BaseException | None = None + commits = 0 + rollbacks = 0 + after_commit = None + after_execute = None + + def execute(self, sql: str, parameters: tuple = ()): + if hasattr(self, "statements"): + self.statements.append(sql) + if self.armed: + if self.mode == "rollback_to" and sql.startswith("ROLLBACK TO SAVEPOINT tier_writer_"): + raise self.failure + if self.mode == "inner_before" and sql == "RELEASE SAVEPOINT rebuild_source": + raise self.failure + if self.mode == "guard_before" and sql.startswith("RELEASE SAVEPOINT tier_writer_"): + raise self.failure + result = super().execute(sql, parameters) + if self.after_execute is not None: + self.after_execute(sql, parameters) + if self.armed: + if self.mode == "inner_after" and sql == "RELEASE SAVEPOINT rebuild_source": + raise self.failure + if self.mode == "guard_after" and sql.startswith("RELEASE SAVEPOINT tier_writer_"): + raise self.failure + return result + + def commit(self) -> None: + self.commits += 1 + if self.armed and self.mode == "commit_before": + raise self.failure + if self.armed and self.mode == "commit_retained": + return + super().commit() + if self.after_commit is not None: + callback, self.after_commit = self.after_commit, None + callback() + if self.armed and self.mode == "commit_after": + raise self.failure + + def rollback(self) -> None: + self.rollbacks += 1 + if self.armed and self.mode == "rollback_before": + raise self.failure + if self.armed and self.mode == "rollback_retained": + return + super().rollback() + if self.armed and self.mode == "rollback_after": + raise self.failure + + +class TestWriterFence(unittest.TestCase): + def setUp(self) -> None: + self.modules = BASE["load_modules"]() + self.vector = self.modules["vector_search"] + underlying = self.vector.__dict__["__builtins__"]["__import__"] + + def safe_import(name, globals=None, locals=None, fromlist=(), level=0): + if name in {"numpy", "torch", "model2vec", "sentence_transformers", "sqlite_vec"}: + raise AssertionError("Native imports are excluded from writer tests") + if name == "psutil": + return types.SimpleNamespace() + return underlying(name, globals, locals, fromlist, level) + + for name in ("embedding_target", "tier_config", "tier_switch.cache", "tier_switch.job", + "tier_switch.source", "tier_switch.activation"): + module = types.ModuleType("synthetic_writer_" + name.replace(".", "_")) + sys.modules[module.__name__] = module + module.__dict__["__builtins__"] = dict(vars(builtins), __import__=safe_import) + path = ROOT / "truememory" / (name.replace(".", "/") + ".py") + exec(compile(path.read_text(encoding="utf-8"), str(path), "exec"), module.__dict__) + self.modules[name] = module + self.api = self.modules["tier_switch.writer"] + self.activation = self.modules["tier_switch.activation"] + self.conn = self.connect() + self.conn.executescript(self.modules["storage"]._SCHEMA_SQL) + for name in ("vec_messages", "vec_messages_sep", "vec_messages_custom", "vec_messages_sep_custom"): + self.conn.execute(f"CREATE TABLE {name}(embedding BLOB)") + self.conn.commit() + self.calls = [] + self.encode_hook = lambda: None + self.vector._model = object() + self.vector.get_model = lambda: self.vector._model + + def encode(model, texts, **kwargs): + self.calls.append(list(texts)) + self.encode_hook() + return [[1., 0.] for _ in texts] + + self.vector._encode_with_mps_fallback = encode + self.target = self.modules["embedding_target"].EmbeddingTarget("custom", "synthetic/encoder", 2, "custom") + self.generation = 0 + self.addCleanup(patch.stopall) + # Keep the fixture's unbound operation contract, but classify actual + # publication/admission failures with the production helper. + serving_path = ROOT / "truememory/tier_switch/serving.py" + serving_tree = ast.parse(serving_path.read_text(encoding="utf-8")) + serving_names = {"ServingLeaseError", "ServingLeaseTimeout", "ServingLeaseCancelled"} + serving_namespace = {} + exec(compile(ast.Module(body=[node for node in serving_tree.body + if isinstance(node, ast.ClassDef) and node.name in serving_names], + type_ignores=[]), str(serving_path), "exec"), serving_namespace) + runtime_path = ROOT / "truememory/tier_switch/runtime.py" + runtime_tree = ast.parse(runtime_path.read_text(encoding="utf-8")) + runtime_names = {"TierRuntimeError", "raise_if_serving_rejection"} + runtime_namespace = { + "sys": types.SimpleNamespace(modules={"truememory." + name: module + for name, module in self.modules.items()}), + "serving": types.SimpleNamespace(**{name: serving_namespace[name] for name in serving_names}), + } + runtime_nodes = [ast.ImportFrom(module="__future__", names=[ast.alias(name="annotations")], level=0)] + runtime_nodes.extend(node for node in runtime_tree.body + if isinstance(node, (ast.ClassDef, ast.FunctionDef)) and node.name in runtime_names) + exec(compile(ast.fix_missing_locations(ast.Module(body=runtime_nodes, type_ignores=[])), + str(runtime_path), "exec"), runtime_namespace) + runtime = self.modules["tier_switch.runtime"] + for name in runtime_names: + setattr(runtime, name, runtime_namespace[name]) + key = "truememory.tier_switch.runtime" + missing = object() + previous = sys.modules.get(key, missing) + sys.modules[key] = types.SimpleNamespace( + current_operation=lambda conn: None, + **{name: runtime_namespace[name] for name in runtime_names}, + ) + + def restore_runtime(): + if previous is missing: + sys.modules.pop(key, None) + else: + sys.modules[key] = previous + + self.addCleanup(restore_runtime) + + def connect(self, uri: str | None = None): + conn = sqlite3.connect(uri or ":memory:", uri=uri is not None, factory=Connection) + conn.statements = [] + self.addCleanup(conn.close) + return conn + + def publish(self, conn=None, *, target=None, acknowledged=True, commit=True): + conn = self.conn if conn is None else conn + target = target or self.target + self.generation += 1 + generation = f"{self.generation:032x}" + intent = self.activation.ActivationIntent( + generation, None, target, "synthetic/reranker", generation, + "rebuild-bridge-v1", "synthetic-epoch", "a" * 64, + state="config_acknowledged" if acknowledged else "db_selected", + ) + selected = self.activation.TierSelection( + generation, generation, target, intent.reranker_id, target.tables, generation, + intent.tracker, intent.source_epoch, intent.source_schema_signature, + generation, "b" * 64, "c" * 64, "d" * 64, "e" * 64, 0, None, acknowledged, + ) + self.activation._put(conn, "tier_activation_v1", self.activation._dump(intent)) + self.activation._put(conn, "tier_selected_v1", self.activation._dump(selected)) + self.activation._put(conn, "embed_model", target.model_id) + self.activation._put(conn, "embed_dim", str(target.dimension)) + self.modules["tier_switch.cache"].VectorCacheRegistry.set( + conn, target.tier_group, model_name=target.model_id, embedding_dim=target.dimension, commit=False, + ) + if commit: + conn.commit() + return selected + + def selected_runtime(self): + selected = self.publish() + self.vector.EMBEDDING_MODEL = selected.target.model_id + self.vector._embedding_dim = selected.target.dimension + self.vector._active_vec_table = lambda conn: selected.tables[0] + self.vector._active_sep_table = lambda conn: selected.tables[1] + return selected + + def seed(self, mid=1, content="synthetic-original") -> None: + self.conn.execute("INSERT INTO messages(id,content,sender) VALUES(?,?,?)", (mid, content, "synthetic-sender")) + self.conn.commit() + + def contents(self, conn=None): + conn = self.conn if conn is None else conn + return conn.execute("SELECT id,content FROM messages ORDER BY id").fetchall() + + def engine(self, *, vectors=True): + tree = ast.parse((ROOT / "truememory/engine.py").read_text(encoding="utf-8")) + original = next(n for n in tree.body if isinstance(n, ast.ClassDef) and n.name == "TrueMemoryEngine") + methods = [n for n in original.body if isinstance(n, ast.FunctionDef) and n.name in {"add", "update", "delete", "delete_all"}] + cls = ast.ClassDef(name="SyntheticEngine", bases=[], keywords=[], body=methods, decorator_list=[]) + constants = [n for n in tree.body if isinstance(n, ast.Assign) and any( + isinstance(t, ast.Name) and t.id in {"_ALLOWED_TABLES", "_ALLOWED_COLUMNS", "_ALL_VEC_TABLES", "_SQLITE_IN_CHUNK"} + for t in n.targets + )] + helpers = [n for n in tree.body if isinstance(n, ast.FunctionDef) and n.name in {"_delete_in_chunks", "_resolve_vec_tables", "_validate_add_content", "_validate_delete_user"}] + namespace = dict(__builtins__=self.vector.__dict__["__builtins__"], logger=logging.getLogger(__name__), + MAX_CONTENT_LENGTH=10000, sqlite3=sqlite3, engine_operation=lambda function: function, + insert_message=self.modules["storage"].insert_message, + update_message=self.modules["storage"].update_message, + delete_message=self.modules["storage"].delete_message) + exec(compile(ast.fix_missing_locations(ast.Module(body=[*constants, *helpers, cls], type_ignores=[])), + "actual-writer-engine", "exec"), namespace) + engine = namespace["SyntheticEngine"]() + engine.conn = self.conn + engine._write_lock = threading.Lock() + engine._ensure_connection = lambda: None + engine._maybe_auto_consolidate = lambda: None + engine._has_vectors = vectors + engine._has_personality = engine._has_style_vec = False + engine.get = lambda mid: self.modules["storage"].get_message(self.conn, mid) + return engine + + def test_capture_is_frozen_read_only_and_connection_bound(self) -> None: + before = self.conn.total_changes + captured = self.api.capture_writer_selection(self.conn) + self.assertIsNone(captured.selection) + self.assertIs(captured.connection, self.conn) + self.assertNotIn("Connection", repr(captured)) + self.assertEqual(self.conn.total_changes, before) + self.assertFalse(self.conn.in_transaction) + with self.assertRaises(dataclasses.FrozenInstanceError): + captured.selection = "changed" + other = self.connect() + other.executescript("CREATE TABLE messages(id INTEGER PRIMARY KEY, content TEXT)") + with self.assertRaises(self.api.WriterSelectionChanged): + with self.api.writer_transaction(other, captured): + other.execute("INSERT INTO messages VALUES(1,'wrong connection')") + self.assertEqual(self.contents(other), []) + + def test_runtime_legacy_none_never_recaptures_first_selection(self) -> None: + selected = self.selected_runtime() + runtime = types.SimpleNamespace(current_operation=lambda conn: types.SimpleNamespace(selection=None)) + with patch.dict(sys.modules, {"truememory.tier_switch.runtime": runtime}): + captured = self.api.capture_writer_selection(self.conn) + self.assertIsNone(captured.selection) + with self.assertRaises(self.api.WriterSelectionChanged): + with self.api.writer_transaction(self.conn, captured): + self.fail("Legacy admission cannot publish into selected generation") + self.assertEqual(self.activation.read_activation_state(self.conn).selection, selected) + + def test_runtime_wrong_connection_refusal_propagates_without_fallback(self) -> None: + def current(conn): + raise RuntimeError("synthetic wrong accepted connection") + with patch.dict(sys.modules, {"truememory.tier_switch.runtime": types.SimpleNamespace(current_operation=current)}): + before = len(self.conn.statements) + with self.assertRaisesRegex(RuntimeError, "wrong accepted connection"): + self.api.capture_writer_selection(self.conn) + self.assertEqual(len(self.conn.statements), before) + + def test_two_actual_handles_reject_first_selection_and_same_width_generation_change(self) -> None: + uri = "file:synthetic-writer-" + uuid.uuid4().hex + "?mode=memory&cache=shared" + left, right = self.connect(uri), self.connect(uri) + left.executescript(self.modules["storage"]._SCHEMA_SQL) + captured = self.api.capture_writer_selection(left) + first = self.publish(right) + with self.assertRaises(self.api.WriterSelectionChanged): + with self.api.writer_transaction(left, captured): + left.execute("INSERT INTO messages(content) VALUES('stale legacy')") + self.assertEqual(self.contents(left), []) + captured = self.api.capture_writer_selection(left) + second = self.publish(right) + self.assertEqual(first.target.dimension, second.target.dimension) + self.assertNotEqual(first.generation, second.generation) + with self.assertRaises(self.api.WriterSelectionChanged): + with self.api.writer_transaction(left, captured): + left.execute("INSERT INTO messages(content) VALUES('stale selected')") + self.assertEqual(self.contents(left), []) + + def test_borrowed_reader_cannot_upgrade_past_another_writer(self) -> None: + uri = "file:synthetic-lock-" + uuid.uuid4().hex + "?mode=memory&cache=shared" + left, right = self.connect(uri), self.connect(uri) + left.executescript(self.modules["storage"]._SCHEMA_SQL) + right.execute("BEGIN IMMEDIATE") + left.execute("BEGIN") + captured = self.api.capture_writer_selection(left) + with self.assertRaises(sqlite3.OperationalError): + with self.api.writer_transaction(left, captured): + left.execute("INSERT INTO messages(content) VALUES('must not mutate')") + self.assertTrue(left.in_transaction) + self.assertTrue(right.in_transaction) + self.assertEqual(self.contents(left), []) + right.rollback() + left.rollback() + + def test_exact_model_dimension_pair_registry_and_metadata_guard(self) -> None: + self.selected_runtime() + captured = self.api.capture_writer_selection(self.conn) + for options in ({"model_id": "synthetic/wrong"}, {"dimension": 3}, + {"dimension": True}, {"tables": tuple(reversed(self.target.tables))}): + with self.subTest(options=options), self.assertRaises(self.api.WriterSelectionChanged): + with self.api.writer_transaction(self.conn, captured, **options): + self.fail("Mismatched output must not be admitted") + for sql in ("UPDATE vector_cache_registry SET vec_table='wrong'", + "UPDATE metadata SET value='wrong' WHERE key='embed_model'", + "UPDATE metadata SET value='02' WHERE key='embed_dim'"): + self.conn.execute("BEGIN") + self.conn.execute(sql) + with self.subTest(sql=sql), self.assertRaises(self.api.WriterSelectionChanged): + with self.api.writer_transaction(self.conn, captured): + self.fail("Changed metadata must not be admitted") + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + + def test_acknowledgement_alone_does_not_change_writer_space(self) -> None: + selected = self.publish(acknowledged=False) + captured = self.api.capture_writer_selection(self.conn) + self.activation.acknowledge_config(self.conn, generation=selected.generation) + with self.api.writer_transaction(self.conn, captured): + self.conn.execute("INSERT INTO messages(content) VALUES('same embedding space')") + self.assertEqual(self.contents(), [(1, "same embedding space")]) + + def test_borrowed_success_and_refusal_preserve_outer_work(self) -> None: + captured = self.api.capture_writer_selection(self.conn) + self.conn.execute("INSERT INTO messages(id,content) VALUES(11,'caller')") + before = self.conn.commits + with self.api.writer_transaction(self.conn, captured): + self.conn.execute("INSERT INTO messages(id,content) VALUES(12,'operation')") + self.assertEqual(self.conn.commits, before) + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.contents(), [(11, "caller"), (12, "operation")]) + self.conn.rollback() + self.assertEqual(self.contents(), []) + self.conn.execute("INSERT INTO messages(id,content) VALUES(11,'caller')") + self.publish(commit=False) + with self.assertRaises(self.api.WriterSelectionChanged): + with self.api.writer_transaction(self.conn, captured): + self.fail("Stale capture must refuse before mutation") + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.contents(), [(11, "caller")]) + + def test_capture_borrows_without_ending_or_upgrading_outer_transaction(self) -> None: + self.conn.execute("INSERT INTO messages(id,content) VALUES(11,'caller')") + before = self.conn.commits, self.conn.rollbacks + self.assertIsNone(self.api.capture_writer_selection(self.conn).selection) + self.assertEqual((self.conn.commits, self.conn.rollbacks), before) + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + self.assertEqual(self.contents(), []) + + def test_owned_commit_faults_before_after_and_retained_do_not_fake_success(self) -> None: + for mode in ("commit_before", "commit_after", "commit_retained"): + with self.subTest(mode=mode): + captured = self.api.capture_writer_selection(self.conn) + self.conn.armed, self.conn.mode, self.conn.failure = True, mode, Cancelled("synthetic commit fault") + rollbacks = self.conn.rollbacks + with self.assertRaises((Cancelled, self.api.WriterSelectionChanged)): + with self.api.writer_transaction(self.conn, captured): + self.conn.execute("INSERT INTO messages(id,content) VALUES(12,'operation')") + self.conn.armed = False + self.assertFalse(self.conn.in_transaction) + if mode == "commit_after": + self.assertEqual(self.conn.rollbacks, rollbacks) + self.assertEqual(self.contents(), [(12, "operation")]) + self.conn.execute("DELETE FROM messages") + self.conn.commit() + else: + self.assertEqual(self.conn.rollbacks, rollbacks + 1) + self.assertEqual(self.contents(), []) + + def test_owned_uncertain_rollback_is_attempted_once(self) -> None: + for mode in ("rollback_before", "rollback_after", "rollback_retained"): + with self.subTest(mode=mode): + captured = self.api.capture_writer_selection(self.conn) + self.conn.armed, self.conn.mode = True, mode + self.conn.failure = sqlite3.OperationalError("synthetic rollback uncertainty") + before = self.conn.rollbacks + try: + with self.assertRaises((sqlite3.OperationalError, self.api.WriterSelectionChanged)): + with self.api.writer_transaction(self.conn, captured): + self.conn.execute("INSERT INTO messages(id,content) VALUES(12,'operation')") + raise Cancelled("synthetic body cancellation") + self.assertEqual(self.conn.rollbacks, before + 1) + self.assertEqual(self.conn.in_transaction, mode != "rollback_after") + finally: + self.conn.armed = False + self.conn.rollback() + + def test_repeated_caller_savepoint_survives_inner_release_before_and_after_fault(self) -> None: + for mode in ("inner_before", "inner_after"): + with self.subTest(mode=mode): + captured = self.api.capture_writer_selection(self.conn) + self.conn.execute("BEGIN") + self.conn.execute("SAVEPOINT rebuild_source") + self.conn.execute("INSERT INTO messages(id,content) VALUES(11,'caller')") + self.conn.armed, self.conn.mode, self.conn.failure = True, mode, Cancelled("synthetic inner release fault") + with self.assertRaises(Cancelled): + with self.api.writer_transaction(self.conn, captured): + self.conn.execute("INSERT INTO messages(id,content) VALUES(12,'operation')") + self.conn.armed = False + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.contents(), [(11, "caller")]) + self.conn.execute("RELEASE SAVEPOINT rebuild_source") + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + + def test_ambiguous_final_guard_release_never_attempts_cleanup(self) -> None: + for mode in ("guard_before", "guard_after"): + with self.subTest(mode=mode): + captured = self.api.capture_writer_selection(self.conn) + self.conn.execute("INSERT INTO messages(id,content) VALUES(11,'caller')") + self.conn.armed, self.conn.mode, self.conn.failure = True, mode, Cancelled("synthetic guard release fault") + before = len(self.conn.statements) + with self.assertRaises(Cancelled): + with self.api.writer_transaction(self.conn, captured): + self.conn.execute("INSERT INTO messages(id,content) VALUES(12,'operation')") + tail = self.conn.statements[before:] + self.assertTrue(tail[-1].startswith("RELEASE SAVEPOINT tier_writer_")) + self.assertFalse(any(sql.startswith("ROLLBACK") for sql in tail)) + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.contents(), [(11, "caller"), (12, "operation")]) + self.conn.armed = False + self.conn.rollback() + + def test_borrowed_rollback_to_failure_never_releases_or_discards_outer_work(self) -> None: + captured = self.api.capture_writer_selection(self.conn) + self.conn.execute("INSERT INTO messages(id,content) VALUES(11,'caller')") + self.conn.armed, self.conn.mode = True, "rollback_to" + self.conn.failure = sqlite3.OperationalError("synthetic rollback-to failure") + before = len(self.conn.statements) + with self.assertRaises(sqlite3.OperationalError): + with self.api.writer_transaction(self.conn, captured): + self.conn.execute("INSERT INTO messages(id,content) VALUES(12,'operation')") + raise Cancelled("synthetic cancellation") + tail = self.conn.statements[before:] + self.assertEqual(sum(sql.startswith("ROLLBACK TO") for sql in tail), 1) + self.assertFalse(any(sql.startswith("RELEASE") for sql in tail)) + self.assertTrue(self.conn.in_transaction) + self.assertEqual(self.contents(), [(11, "caller"), (12, "operation")]) + self.conn.armed = False + self.conn.rollback() + + def test_add_stale_capture_refuses_even_when_native_preparation_fails(self) -> None: + self.selected_runtime() + def fail(): + self.publish() + raise RuntimeError("synthetic encoder unavailable") + self.encode_hook = fail + with self.assertRaises(self.api.WriterSelectionChanged): + self.engine().add("must not store") + self.assertEqual(self.contents(), []) + + def test_update_stale_capture_refuses_before_source_change(self) -> None: + self.selected_runtime() + self.seed() + fired = [] + def change(): + if not fired: + fired.append(True) + self.publish() + self.encode_hook = change + with self.assertRaises(self.api.WriterSelectionChanged): + self.engine().update(1, "must not store") + self.assertEqual(self.contents(), [(1, "synthetic-original")]) + + def test_source_commit_then_selection_change_keeps_retry_wording(self) -> None: + self.selected_runtime() + self.seed() + self.conn.after_commit = self.publish + with self.assertRaisesRegex(self.vector.VectorPublicationChanged, "Source update committed; retry update to publish vectors"): + self.engine().update(1, "committed-source") + self.assertEqual(self.contents(), [(1, "committed-source")]) + self.assertEqual(self.conn.execute("SELECT * FROM vec_messages_custom").fetchall(), []) + + def test_delete_and_delete_all_reject_stale_admitted_operation_before_any_delete(self) -> None: + selected = self.selected_runtime() + self.seed() + runtime = types.SimpleNamespace(current_operation=lambda conn: types.SimpleNamespace(selection=selected)) + self.publish() + with patch.dict(sys.modules, {"truememory.tier_switch.runtime": runtime}): + for operation in (lambda: self.engine().delete(1), lambda: self.engine().delete_all(), + lambda: self.engine().delete_all("synthetic-sender")): + with self.assertRaises(self.api.WriterSelectionChanged): + operation() + self.assertEqual(self.contents(), [(1, "synthetic-original")]) + + def test_selected_delete_all_uses_exact_custom_pair_and_borrowed_rollback(self) -> None: + self.selected_runtime() + self.seed() + for table in self.target.tables: + self.conn.execute(f"INSERT INTO {table}(rowid,embedding) VALUES(1,x'0102')") + self.conn.commit() + self.conn.execute("INSERT INTO metadata(key,value) VALUES('caller','pending')") + self.assertTrue(self.engine(vectors=False).delete_all("synthetic-sender")) + self.assertEqual(self.contents(), []) + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + self.assertEqual(self.contents(), [(1, "synthetic-original")]) + for table in self.target.tables: + self.assertEqual(self.conn.execute(f"SELECT rowid FROM {table}").fetchall(), [(1,)]) + self.assertTrue(self.engine(vectors=False).delete_all("synthetic-sender")) + self.assertFalse(self.conn.in_transaction) + for table in self.target.tables: + self.assertEqual(self.conn.execute(f"SELECT rowid FROM {table}").fetchall(), []) + + def test_direct_embed_refuses_changed_selection_without_new_metadata(self) -> None: + self.selected_runtime() + self.seed() + fired = [] + def change(): + if not fired: + fired.append(self.publish()) + self.encode_hook = change + with self.assertRaises(self.vector.VectorPublicationChanged): + self.vector.embed_single(self.conn, 1, "synthetic-original") + self.assertEqual(self.conn.execute("SELECT * FROM vec_messages_custom").fetchall(), []) + self.assertEqual(self.activation.read_activation_state(self.conn).selection, fired[0]) + + def test_legacy_crud_and_borrowed_update_delete_remain_default_compatible(self) -> None: + engine = self.engine(vectors=False) + item = engine.add("legacy") + self.assertEqual(engine.update(item["id"], "legacy-updated")["content"], "legacy-updated") + self.conn.execute("INSERT INTO metadata(key,value) VALUES('caller','pending')") + engine.update(item["id"], "pending-update") + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + self.assertEqual(self.contents(), [(item["id"], "legacy-updated")]) + self.conn.execute("INSERT INTO metadata(key,value) VALUES('caller','pending')") + self.assertTrue(engine.delete(item["id"])) + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + self.assertEqual(self.contents(), [(item["id"], "legacy-updated")]) + self.assertTrue(engine.delete(item["id"])) + self.assertFalse(self.conn.in_transaction) + + def test_storage_defaults_commit_and_false_never_commits(self) -> None: + self.seed() + before = self.conn.commits + self.modules["storage"].update_message(self.conn, 1, content="default") + self.assertEqual(self.conn.commits, before + 1) + self.modules["storage"].update_message(self.conn, 1, content="pending", commit=False) + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + self.assertEqual(self.contents(), [(1, "default")]) + self.modules["storage"].delete_message(self.conn, 1, commit=False) + self.assertTrue(self.conn.in_transaction) + self.conn.rollback() + self.assertEqual(self.contents(), [(1, "default")]) + self.modules["storage"].delete_message(self.conn, 1) + self.assertFalse(self.conn.in_transaction) + self.assertEqual(self.contents(), []) + + def test_bulk_prefix_cannot_recapture_new_generation(self) -> None: + self.selected_runtime() + self.seed() + @contextmanager + def owner(path): + yield None + maintenance = self.modules["maintenance"] + fired = [] + def change(): + if not fired: + fired.append(self.publish()) + self.encode_hook = change + with patch.object(maintenance, "maintenance_owner", owner), patch.object(maintenance, "connection_database_path", lambda conn: Path("/synthetic/database")): + with self.assertRaises(self.api.WriterSelectionChanged): + self.vector.build_vectors(self.conn) + self.assertEqual(self.conn.execute("SELECT * FROM vec_messages_custom").fetchall(), []) + self.assertEqual(self.activation.read_activation_state(self.conn).selection, fired[0]) + + def test_bulk_initial_and_empty_clear_refuse_stale_capture(self) -> None: + for nonempty in (False, True): + with self.subTest(nonempty=nonempty): + fixture = TestWriterFence() + fixture.setUp() + try: + fixture.selected_runtime() + if nonempty: + fixture.seed() + fixture.conn.execute("INSERT INTO vec_messages_custom(rowid,embedding) VALUES(99,x'0102')") + fixture.conn.commit() + @contextmanager + def owner(path): + fixture.publish() + yield None + maintenance = fixture.modules["maintenance"] + with patch.object(maintenance, "maintenance_owner", owner), patch.object(maintenance, "connection_database_path", lambda conn: Path("/synthetic/database")): + with self.assertRaises(fixture.api.WriterSelectionChanged): + fixture.vector.build_vectors(fixture.conn) + self.assertEqual(fixture.calls, []) + self.assertEqual(fixture.conn.execute("SELECT rowid,embedding FROM vec_messages_custom").fetchall(), [(99, b"\x01\x02")]) + finally: + fixture.doCleanups() + + def test_bulk_finish_cannot_publish_completion_or_metadata_after_selection_change(self) -> None: + self.selected_runtime() + self.seed() + @contextmanager + def owner(path): + yield None + changed = [] + def observe(sql, parameters): + if sql.startswith("INSERT OR REPLACE INTO metadata(key,value,updated_at)") and parameters[0].startswith("vec_source_v1:"): + value = json.loads(parameters[1]) + if value["consumed"] == 1 and not value["complete"]: + self.conn.after_commit = lambda: changed.append(self.publish()) + self.conn.after_execute = observe + maintenance = self.modules["maintenance"] + with patch.object(maintenance, "maintenance_owner", owner), patch.object(maintenance, "connection_database_path", lambda conn: Path("/synthetic/database")): + with self.assertRaises(self.api.WriterSelectionChanged): + self.vector.build_vectors(self.conn) + self.conn.after_execute = None + self.assertEqual(len(changed), 1) + self.assertEqual(self.conn.execute("SELECT rowid FROM vec_messages_custom").fetchall(), [(1,)]) + manifest = self.modules["rebuild_source"].load_manifest(self.conn, "vec_source_v1:vec_messages_custom") + self.assertEqual(manifest.consumed, 1) + self.assertFalse(manifest.complete) + self.assertEqual(self.activation.read_activation_state(self.conn).selection, changed[0]) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-transcript-allowlists-761.py b/tests/test-transcript-allowlists-761.py new file mode 100644 index 00000000..c3221888 --- /dev/null +++ b/tests/test-transcript-allowlists-761.py @@ -0,0 +1,605 @@ +"""Synthetic stdlib controls for trusted transcript admission (#761). + +Run directly with Python. Production functions execute from their AST with +model, database and spawn collaborators replaced by inert boundary stubs. +""" + +from __future__ import annotations + +import argparse +import ast +from contextlib import contextmanager +import io +import json +import logging +import os +from pathlib import Path +import shlex +import subprocess +import sys +import tempfile +import types +from typing import Iterator +import unittest +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] + + +def source_module(name: str, relative_path: str, names: set[str] | None = None) -> types.ModuleType: + tree = ast.parse((ROOT / relative_path).read_text(encoding="utf-8")) + if names is not None: + tree.body = [ + node for node in tree.body + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name in names + ] + module = types.ModuleType(name) + module.__file__ = str(ROOT / relative_path) + if names is not None: + module.__dict__.update({ + "Path": Path, "json": json, "os": os, "sys": sys, + "subprocess": subprocess, "argparse": argparse, + "log": logging.getLogger(name), "logging": logging, + }) + sys.modules[name] = module + exec(compile(tree, module.__file__, "exec"), module.__dict__) + return module + + +@contextmanager +def scoped_module_bindings(packages: dict[str, types.ModuleType], owned: set[str]) -> Iterator[None]: + missing = object() + saved = {name: sys.modules.get(name, missing) for name in owned} + sys.modules.update(packages) + try: + yield + finally: + for name, previous in saved.items(): + if previous is missing: + sys.modules.pop(name, None) + else: + sys.modules[name] = previous + + +@contextmanager +def allowed_gate(): + yield True + + +class TranscriptAdmissionTests(unittest.TestCase): + def setUp(self) -> None: + self.temp = tempfile.TemporaryDirectory(prefix="synthetic-transcripts-761-") + self.addCleanup(self.temp.cleanup) + self.home = Path(self.temp.name) / "home" + self.home.mkdir() + self.home_patch = patch.object(Path, "home", return_value=self.home) + self.home_patch.start() + self.addCleanup(self.home_patch.stop) + env = {key: value for key, value in os.environ.items() if key not in ( + "CODEX_HOME", "GEMINI_CLI_HOME", "SANDBOX", "TRUEMEMORY_TRANSCRIPT_DIR", + "TRUEMEMORY_EXTRACTION", "TRUEMEMORY_MAINTENANCE_CHILD", + )} + self.env_patch = patch.dict(os.environ, env, clear=True) + self.env_patch.start() + self.addCleanup(self.env_patch.stop) + packages: dict[str, types.ModuleType] = {} + for name in ( + "truememory", "truememory.ingest", "truememory.ingest.hooks", + "truememory.hooks", "truememory.hooks.adapters", + ): + module = types.ModuleType(name) + module.__path__ = [] + packages[name] = module + platform = types.ModuleType("truememory._platform") + platform._env_int = lambda name, default, **kwargs: default + platform.spawn_kwargs = lambda: {} + platform.pid_is_alive = lambda pid: False + packages["truememory"]._platform = platform + packages["truememory._platform"] = platform + core = types.ModuleType("truememory.hooks.core") + core.spawn_gate = allowed_gate + core.register_spawned_pid = lambda pid: None + core._load_cap_state = lambda: {"cap": 2} + packages["truememory.hooks.core"] = core + owned = set(packages) | { + "truememory.hooks.adapters.base", "truememory.hooks.adapters.codex", + "truememory.hooks.adapters.gemini", "truememory.ingest.hooks._shared", + "truememory.ingest.hooks.stop", "truememory.ingest.hooks.compact", + "truememory.ingest.hooks.session_start", "truememory.ingest.cli", + "truememory.ingest.transcript", + } + self.module_bindings = scoped_module_bindings(packages, owned) + self.module_bindings.__enter__() + self.addCleanup(self.module_bindings.__exit__, None, None, None) + source_module("truememory.hooks.adapters.base", "truememory/hooks/adapters/base.py") + self.shared = source_module( + "truememory.ingest.hooks._shared", "truememory/ingest/hooks/_shared.py", + ) + self.shared.check_extraction_budget = lambda: True + self.shared.refund_extraction_budget = lambda: None + self.cleanup_stale_processing = self.shared.cleanup_stale_processing + self.shared.cleanup_stale_processing = lambda path: None + self.shared.mark_session_extracted = lambda *args, **kwargs: None + self.stop = source_module("truememory.ingest.hooks.stop", "truememory/ingest/hooks/stop.py") + self.stop._writable_dirs_ok = lambda: True + self.stop.TRACE_DIR.mkdir(parents=True) + self.stop.LOG_DIR.mkdir(parents=True) + self.compact = source_module("truememory.ingest.hooks.compact", "truememory/ingest/hooks/compact.py") + self.session_start = source_module( + "truememory.ingest.hooks.session_start", "truememory/ingest/hooks/session_start.py", + {"_drain_backlog", "_run_maintenance_background"}, + ) + self.session_start.BACKLOG_DIR = self.shared.BACKLOG_DIR + self.session_start._DRAIN_CAP = 3 + self.session_start._platform = platform + self.cli = source_module("truememory.ingest.cli", "truememory/ingest/cli.py", {"_cascade_next"}) + self.parser = source_module("truememory.ingest.transcript", "truememory/ingest/transcript.py") + + def write(self, path: Path, text: str = "synthetic conversation") -> Path: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text, encoding="utf-8") + return path + + def native_path(self, host: str, name: str = "session.jsonl") -> Path: + roots = { + "claude": self.home / ".claude/projects/workspace", + "codex": self.home / ".codex/sessions/2026/10/10", + "gemini": self.home / ".gemini/tmp/workspace/chats", + } + return roots[host] / name + + def marker(self, host: str, name: str, path: Path, **extra: object) -> Path: + data = {"transcript_path": str(path), "transcript_host": host, + "session_id": name, "user_id": "synthetic", "db_path": ""} + data.update(extra) + return self.write(self.shared.BACKLOG_DIR / f"{name}.json", json.dumps(data)) + + def test_native_roots_are_isolated_by_installed_host(self) -> None: + for source in ("claude", "codex", "gemini"): + path = self.write(self.native_path(source)) + for host in ("claude", "codex", "gemini", "cursor", "invalid"): + with self.subTest(source=source, host=host): + self.assertEqual(self.shared.is_allowed_transcript(str(path), host), source == host) + + def test_legacy_default_remains_claude(self) -> None: + self.assertTrue(self.shared.is_allowed_transcript(str(self.native_path("claude")))) + self.assertFalse(self.shared.is_allowed_transcript(str(self.native_path("codex")))) + self.assertFalse(self.shared.is_allowed_transcript(str(self.native_path("gemini")))) + + def test_unrelated_home_and_gemini_runtime_paths_are_rejected(self) -> None: + for path in (self.home / "notes.txt", self.home / ".gemini/settings.json", + self.home / ".gemini/tmp/workspace/logs.json", + self.home / ".gemini/tmp/workspace/memory/item.json", + self.home / ".gemini/tmp/chats/item.json"): + with self.subTest(path=path): + self.assertFalse(self.shared.is_allowed_transcript(str(path), "gemini")) + + def test_gemini_supports_nested_subagent_chats(self) -> None: + self.assertTrue(self.shared.is_allowed_transcript( + str(self.native_path("gemini") / "subagent/turn.jsonl"), "gemini", + )) + + def test_codex_home_override_selects_only_sessions(self) -> None: + custom = self.home / "custom-codex" + os.environ["CODEX_HOME"] = str(custom) + self.assertTrue(self.shared.is_allowed_transcript(str(custom / "sessions/date/turn.jsonl"), "codex")) + self.assertFalse(self.shared.is_allowed_transcript(str(custom / "config.toml"), "codex")) + self.assertFalse(self.shared.is_allowed_transcript(str(self.native_path("codex")), "codex")) + self.assertFalse(self.shared.is_allowed_transcript(str(custom / "sessions/date/turn.jsonl"), "gemini")) + + def test_gemini_home_and_sandbox_select_only_documented_runtime(self) -> None: + custom = self.home / "custom-gemini" + os.environ["GEMINI_CLI_HOME"] = str(custom) + path = custom / ".gemini/tmp/workspace/chats/turn.json" + self.assertTrue(self.shared.is_allowed_transcript(str(path), "gemini")) + self.assertFalse(self.shared.is_allowed_transcript(str(self.native_path("gemini")), "gemini")) + os.environ["SANDBOX"] = "sandbox-exec" + sandbox_path = custom / ".cache/.gemini/tmp/workspace/chats/turn.jsonl" + self.assertTrue(self.shared.is_allowed_transcript(str(sandbox_path), "gemini")) + self.assertFalse(self.shared.is_allowed_transcript(str(path), "gemini")) + self.assertFalse(self.shared.is_allowed_transcript(str(sandbox_path), "codex")) + + def test_explicit_scoped_override_and_broad_home_rejection(self) -> None: + custom = self.home / "synthetic-transcripts" + os.environ["TRUEMEMORY_TRANSCRIPT_DIR"] = str(custom) + self.assertTrue(self.shared.is_allowed_transcript(str(custom / "turn.json"), "cursor")) + self.assertFalse(self.shared.is_allowed_transcript(str(custom / "turn.json"), "invalid")) + for broad in (self.home, self.home.parent, Path("/")): + os.environ["TRUEMEMORY_TRANSCRIPT_DIR"] = str(broad) + self.assertFalse(self.shared.is_allowed_transcript(str(self.home / "notes.txt"))) + + def test_path_shape_is_validated_without_coercion(self) -> None: + for value in (None, 42, True, [], {}, "", " ", "bad\0path"): + with self.subTest(value=value): + self.assertFalse(self.shared.is_allowed_transcript(value, "codex")) + for value in (None, [], {}, "CODEX", " codex", "codex;command"): + self.assertEqual(self.shared.normalize_transcript_host(value), "") + + def test_traversal_and_root_itself_are_rejected(self) -> None: + for host in ("claude", "codex", "gemini"): + with self.subTest(host=host): + self.assertFalse(self.shared.is_allowed_transcript(str(self.native_path(host).parent / "../outside.txt"), host)) + self.assertFalse(self.shared.is_allowed_transcript(str(self.home / ".codex/sessions"), "codex")) + self.assertFalse(self.shared.is_allowed_transcript(str(self.home / ".gemini/tmp/workspace/chats"), "gemini")) + + def test_symlink_escapes_and_loops_are_rejected(self) -> None: + outside = self.write(self.home / "outside.txt") + for host in ("claude", "codex", "gemini"): + path = self.native_path(host) + path.parent.mkdir(parents=True, exist_ok=True) + path.symlink_to(outside) + self.assertFalse(self.shared.is_allowed_transcript(str(path), host)) + loop = path.parent / "loop.jsonl" + loop.symlink_to(loop) + self.assertFalse(self.shared.is_allowed_transcript(str(loop), host)) + subtree = self.home / ".gemini/tmp/other/chats" + subtree.parent.mkdir(parents=True) + subtree.symlink_to(self.home) + self.assertFalse(self.shared.is_allowed_transcript(str(subtree / "outside.txt"), "gemini")) + + def test_marker_shape_and_source_are_sanitized(self) -> None: + path = str(self.native_path("codex")) + data = {"transcript_path": path, "transcript_host": "codex", "session_id": "../synthetic:id"} + self.assertEqual(self.shared.backlog_transcript_source(data, "codex"), (path, "syntheticid")) + self.assertIsNone(self.shared.backlog_transcript_source(data, "gemini")) + for key, value in (("transcript_host", []), ("transcript_host", "invalid"), + ("transcript_path", 42), ("user_id", []), ("db_path", "bad\0db"), + ("session_id", {})): + bad = dict(data) + bad[key] = value + self.assertIsNone(self.shared.backlog_transcript_source(bad, "codex")) + for bad in (None, [], 42, "text"): + self.assertIsNone(self.shared.backlog_transcript_source(bad, "codex")) + + def test_legacy_marker_is_only_admitted_by_claude(self) -> None: + data = {"transcript_path": str(self.native_path("claude")), "session_id": "synthetic"} + self.assertIsNotNone(self.shared.backlog_transcript_source(data)) + self.assertIsNone(self.shared.backlog_transcript_source(data, "codex")) + + def test_stale_marker_shape_cannot_crash_source_admission(self) -> None: + for i, data in enumerate(([], {"claimed_pid": []}, {"claimed_pid": True}, {"claimed_pid": -1})): + path = self.write(self.shared.BACKLOG_DIR / f"malformed-{i}.processing", json.dumps(data)) + os.utime(path, (1, 1)) + self.cleanup_stale_processing(self.shared.BACKLOG_DIR) + self.assertEqual(len(list(self.shared.BACKLOG_DIR.glob("*.corrupt"))), 4) + self.assertEqual(list(self.shared.BACKLOG_DIR.glob("*.json")), []) + + def test_stop_correct_host_reaches_parser_once_and_stdin_identity_cannot_override(self) -> None: + for host in ("codex", "gemini"): + text = json.dumps([{"role": "user", "content": "synthetic turn"} for _ in range(5)]) + path = self.write(self.native_path(host), text) + calls: list[tuple[str, str]] = [] + + def extract(transcript: str, session: str, user: str, db: str, transcript_host: str) -> int: + calls.append((transcript, transcript_host)) + self.assertEqual(len(self.parser.parse_transcript(Path(transcript))), 5) + return 17 + + with patch.object(self.stop, "_run_background_ingestion", side_effect=extract), \ + patch.object(sys, "argv", ["stop.py", "--host", host]), \ + patch.object(sys, "stdin", io.StringIO(json.dumps({ + "transcript_path": str(path), "session_id": "synthetic", + "host": "claude", "transcript_host": "claude", + }))): + self.stop.main() + self.assertEqual(calls, [(str(path), host)]) + calls.clear() + with patch.object(self.stop, "_run_background_ingestion", side_effect=extract), \ + patch.object(sys, "argv", ["stop.py", "--host", "claude"]), \ + patch.object(sys, "stdin", io.StringIO(json.dumps({ + "transcript_path": str(path), "session_id": "synthetic", "host": host, + }))): + self.stop.main() + self.assertEqual(calls, []) + + def test_stop_malformed_input_never_probes_source(self) -> None: + for payload in ([], 42, None, {"transcript_path": 42}, {"transcript_path": "bad\0path"}): + with patch.object(self.stop, "_has_enough_messages", side_effect=AssertionError("unexpected read")), \ + patch.object(sys, "argv", ["stop.py", "--host", "codex"]), \ + patch.object(sys, "stdin", io.StringIO(json.dumps(payload))): + self.stop.main() + + def test_compact_correct_host_snapshot_and_background_admission(self) -> None: + path = self.write(self.native_path("gemini"), json.dumps([ + {"role": "user", "content": "synthetic turn"} for _ in range(5) + ])) + with patch.object(self.compact, "save_snapshot") as snapshot, \ + patch.object(self.stop, "_run_background_ingestion", return_value=17) as spawn, \ + patch.object(sys, "argv", ["compact.py", "--host", "gemini", "--event", "PreCompress"]), \ + patch.object(sys, "stdin", io.StringIO(json.dumps({"transcript_path": str(path), "session_id": "synthetic"}))): + self.compact.main() + snapshot.assert_called_once() + self.assertEqual(spawn.call_args.kwargs["transcript_host"], "gemini") + with patch.object(self.compact, "save_snapshot") as snapshot, \ + patch.object(sys, "argv", ["compact.py", "--host", "codex"]), \ + patch.object(sys, "stdin", io.StringIO(json.dumps({"transcript_path": str(path), "session_id": "synthetic"}))): + self.compact.main() + snapshot.assert_not_called() + + def test_background_spawn_is_admitted_and_carries_trusted_identity(self) -> None: + path = self.write(self.native_path("codex")) + with patch.object(subprocess, "Popen", return_value=types.SimpleNamespace(pid=17)) as spawn: + self.assertEqual(self.stop._run_background_ingestion(str(path), "synthetic", "", "", "codex"), 17) + cmd = spawn.call_args.args[0] + self.assertEqual(cmd[cmd.index("--transcript-host") + 1], "codex") + self.assertEqual(self.stop._run_background_ingestion(str(path), "synthetic", "", "", "gemini"), 0) + self.assertEqual(spawn.call_count, 1) + + def test_queue_records_valid_source_and_refuses_wrong_host(self) -> None: + path = self.write(self.native_path("gemini")) + self.stop._queue_to_backlog(str(path), "../synthetic:id", "", "", "synthetic", "gemini") + marker = self.shared.BACKLOG_DIR / "syntheticid.json" + data = json.loads(marker.read_text()) + self.assertEqual(data["transcript_host"], "gemini") + self.assertEqual(data["session_id"], "syntheticid") + self.stop._queue_to_backlog(str(path), "wrong", "", "", "synthetic", "codex") + self.assertFalse((self.shared.BACKLOG_DIR / "wrong.json").exists()) + + def test_session_drain_preserves_other_host_and_filters_before_cap(self) -> None: + for i in range(4): + self.marker("gemini", f"a-other-{i}", self.write(self.native_path("gemini", f"{i}.json"))) + path = self.write(self.native_path("codex")) + own = self.marker("codex", "z-own", path) + with patch.object(subprocess, "Popen", return_value=types.SimpleNamespace(pid=17)) as spawn: + self.session_start._drain_backlog("codex") + self.assertEqual(spawn.call_count, 1) + cmd = spawn.call_args.args[0] + self.assertEqual(cmd[cmd.index("--transcript-host") + 1], "codex") + self.assertFalse(own.exists()) + self.assertTrue(own.with_suffix(".processing").exists()) + self.assertEqual(len(list(self.shared.BACKLOG_DIR.glob("a-other-*.json"))), 4) + + def test_session_drain_quarantines_invalid_source_without_spawn(self) -> None: + marker = self.marker("codex", "bad", self.write(self.home / "notes.txt")) + with patch.object(subprocess, "Popen") as spawn: + self.session_start._drain_backlog("codex") + spawn.assert_not_called() + self.assertTrue(marker.with_suffix(".corrupt").exists()) + + def test_cascade_uses_same_admission_and_spawns_at_most_one(self) -> None: + other = self.marker("gemini", "a-other", self.write(self.native_path("gemini"))) + bad = self.marker("codex", "b-bad", self.write(self.home / "notes.txt")) + for name in ("c-own", "d-own"): + self.marker("codex", name, self.write(self.native_path("codex", f"{name}.jsonl"))) + with patch.object(subprocess, "Popen", return_value=types.SimpleNamespace(pid=17)) as spawn: + self.cli._cascade_next("codex") + self.assertEqual(spawn.call_count, 1) + cmd = spawn.call_args.args[0] + self.assertEqual(cmd[cmd.index("--transcript-host") + 1], "codex") + self.assertTrue(other.exists()) + self.assertTrue(bad.with_suffix(".corrupt").exists()) + self.assertTrue((self.shared.BACKLOG_DIR / "d-own.json").exists()) + + def test_maintenance_child_retains_host_and_only_claude_scans_claude_archive(self) -> None: + for host in ("claude", "codex", "gemini", "cursor"): + with patch.object(subprocess, "Popen") as spawn: + self.session_start._run_maintenance_background(host) + script = spawn.call_args.args[0][2] + self.assertIn(f"_drain_backlog({host!r})", script) + self.assertEqual(script.endswith("_scan_stale_sessions()"), host == "claude") + with patch.object(subprocess, "Popen") as spawn: + self.session_start._run_maintenance_background("codex;invalid") + spawn.assert_not_called() + + def test_codex_installer_refreshes_identity_and_preserves_external_hook(self) -> None: + module = source_module("truememory.hooks.adapters.codex", "truememory/hooks/adapters/codex.py") + module._CONFIG_PATH = self.home / ".codex/config.toml" + before = ('[[hooks.Stop]]\n[[hooks.Stop.hooks]]\n' + 'type = "command"\ncommand = "external-helper"\n' + '[[hooks.SessionStart]]\n[[hooks.SessionStart.hooks]]\n' + 'type = "command"\ncommand = "python /truememory/ingest/hooks/session_start.py"\n' + 'timeout = 10\n') + self.write(module._CONFIG_PATH, before) + adapter = module.CodexAdapter() + adapter.install_hooks(python_path="/synthetic path/python", user_id="synthetic") + first = module._CONFIG_PATH.read_text() + self.assertIn('command = "external-helper"', first) + hooks = adapter._parse_existing_hooks(first) + for event in module._HOOK_EVENTS: + commands = [hook["command"] for group in hooks[event] for hook in group["hooks"] + if "truememory" in hook.get("command", "")] + self.assertEqual(len(commands), 1) + parts = shlex.split(commands[0]) + self.assertEqual(parts[parts.index("--host") + 1], "codex") + self.assertEqual(parts[parts.index("--event") + 1], event) + adapter.install_hooks(python_path="/synthetic path/python", user_id="synthetic") + self.assertEqual(module._CONFIG_PATH.read_text(), first) + + def test_gemini_installer_refreshes_identity_and_preserves_external_hook(self) -> None: + module = source_module("truememory.hooks.adapters.gemini", "truememory/hooks/adapters/gemini.py") + module._CONFIG_PATH = self.home / ".gemini/settings.json" + external = {"hooks": [{"type": "command", "command": "external-helper"}]} + self.write(module._CONFIG_PATH, json.dumps({"hooks": {"SessionStart": [external, + {"command": "python /truememory/ingest/hooks/session_start.py", "timeout": 10000}]}})) + adapter = module.GeminiAdapter() + adapter.install_hooks(python_path="/synthetic path/python", user_id="synthetic") + first = module._CONFIG_PATH.read_text() + data = json.loads(first) + self.assertEqual(data["hooks"]["SessionStart"][0], external) + for event in module._HOOK_EVENTS: + commands = [hook["command"] for group in data["hooks"][event] for hook in group["hooks"] + if "truememory" in hook["command"]] + self.assertEqual(len(commands), 1) + parts = shlex.split(commands[0]) + self.assertEqual(parts[parts.index("--host") + 1], "gemini") + self.assertEqual(parts[parts.index("--event") + 1], event) + adapter.install_hooks(python_path="/synthetic path/python", user_id="synthetic") + self.assertEqual(module._CONFIG_PATH.read_text(), first) + + def test_codex_foreign_mentions_are_preserved_and_do_not_suppress_registration(self) -> None: + module = source_module("truememory.hooks.adapters.codex", "truememory/hooks/adapters/codex.py") + module._CONFIG_PATH = self.home / ".codex/config.toml" + foreign_commands = ( + "/synthetic/foreign-policy --target truememory", + "/synthetic/foreign-policy --target /pkg/truememory/ingest/hooks/session_start.py", + "python /synthetic/foreign-policy.py --target truememory", + "python -c 'print(\"truememory\")'", + "echo python /pkg/truememory/ingest/hooks/session_start.py", + "/synthetic/foreign-policy # python /pkg/truememory/ingest/hooks/session_start.py", + "python /pkg/not-truememory/ingest/hooks/session_start.py", + "python /pkg/truememory/ingest/hooks/session_start.py && /synthetic/foreign-policy", + "python /pkg/truememory/session_start.py", + "python /pkg/truememory/hooks/session_start.py", + ) + for foreign in foreign_commands: + with self.subTest(foreign=foreign): + command_line = 'command = ' + json.dumps(foreign) + '\n' + before = ('[[hooks.SessionStart]]\n[[hooks.SessionStart.hooks]]\n' + 'type = "command"\n' + command_line + 'timeout = 21\n') + self.write(module._CONFIG_PATH, before) + adapter = module.CodexAdapter() + adapter.install_hooks(python_path="/synthetic/python") + first = module._CONFIG_PATH.read_text() + self.assertIn(before, first) + commands = [hook["command"] for group in adapter._parse_existing_hooks(first)["SessionStart"] + for hook in group["hooks"]] + self.assertIn(foreign, commands) + self.assertEqual(len(commands), 2) + adapter.install_hooks(python_path="/synthetic/python") + self.assertEqual(module._CONFIG_PATH.read_text(), first) + + def test_gemini_foreign_mentions_are_preserved_and_do_not_suppress_registration(self) -> None: + module = source_module("truememory.hooks.adapters.gemini", "truememory/hooks/adapters/gemini.py") + module._CONFIG_PATH = self.home / ".gemini/settings.json" + foreign_commands = ( + "/synthetic/foreign-policy --target truememory", + "/synthetic/foreign-policy --target /pkg/truememory/ingest/hooks/session_start.py", + "python /synthetic/foreign-policy.py --target truememory", + "python -c 'print(\"truememory\")'", + "echo python /pkg/truememory/ingest/hooks/session_start.py", + "/synthetic/foreign-policy # python /pkg/truememory/ingest/hooks/session_start.py", + "python /pkg/not-truememory/ingest/hooks/session_start.py", + "python /pkg/truememory/ingest/hooks/session_start.py && /synthetic/foreign-policy", + "python /pkg/truememory/session_start.py", + "python /pkg/truememory/hooks/session_start.py", + ) + for foreign in foreign_commands: + for nested in (True, False): + with self.subTest(foreign=foreign, nested=nested): + hook = {"command": foreign, "timeout": 21} + definition = {"hooks": [hook]} if nested else hook + self.write(module._CONFIG_PATH, json.dumps({"hooks": {"SessionStart": [definition]}})) + adapter = module.GeminiAdapter() + adapter.install_hooks(python_path="/synthetic/python") + first = module._CONFIG_PATH.read_text() + definitions = json.loads(first)["hooks"]["SessionStart"] + self.assertEqual(definitions[0], definition) + self.assertEqual(len(definitions), 2) + adapter.install_hooks(python_path="/synthetic/python") + self.assertEqual(module._CONFIG_PATH.read_text(), first) + + def test_owned_hook_predicate_requires_actual_python_invocation(self) -> None: + predicate = sys.modules["truememory.hooks.adapters.base"].is_owned_hook_command + for platform in ("linux", "win32"): + with patch.object(sys, "platform", platform): + for stem in ("session_start", "user_prompt_submit", "stop", "compact"): + quoted_command = ( + subprocess.list2cmdline([ + r"C:\Program Files\Python\python.exe", + rf"C:\Synthetic Package\truememory\ingest\hooks\{stem}.py", + ]) + ' --user ";"' if platform == "win32" else + f"'/pkg with space/python3.10' '/pkg with space/truememory/ingest/hooks/{stem}.py' --user ';'" + ) + for command in ( + f"python /pkg/truememory/ingest/hooks/{stem}.py", + quoted_command, + f"python -B -m truememory.ingest.hooks.{stem}", + ): + with self.subTest(platform=platform, command=command): + self.assertTrue(predicate(command)) + for command in (None, [], "", "python -c 'truememory.ingest.hooks.stop'", + "python -m foreign --target truememory.ingest.hooks.stop", + "foreign /pkg/truememory/ingest/hooks/stop.py", "python-policy /pkg/truememory/ingest/hooks/stop.py", + "python /pkg/truememory/ingest/hooks/stop.py; foreign", "python /pkg/truememory/ingest/hooks/stop.py\nforeign", + "python /pkg/truememory/ingest/hooks/stop.py > output", "python '/pkg/truememory/ingest/hooks/stop.py", + "python \"$(foreign)/truememory/ingest/hooks/stop.py\"", "python /pkg/truememory/ingest/hooks/not-a-hook.py"): + with self.subTest(platform=platform, command=command): + self.assertFalse(predicate(command)) + with patch.object(sys, "platform", "win32"): + command = subprocess.list2cmdline([ + r"C:\Program Files\Python\python.exe", + r"C:\Synthetic Package\truememory\ingest\hooks\stop.py", + "--host", "codex", "--user", "synthetic user", + ]) + self.assertTrue(predicate(command)) + self.assertFalse(predicate(r"C:\foreign.exe --target truememory")) + + def test_owned_module_invocations_are_upgraded_without_duplication(self) -> None: + codex = source_module("truememory.hooks.adapters.codex", "truememory/hooks/adapters/codex.py") + codex._CONFIG_PATH = self.home / ".codex/config.toml" + self.write(codex._CONFIG_PATH, '[[hooks.SessionStart]]\n[[hooks.SessionStart.hooks]]\n' + 'command = "python -m truememory.ingest.hooks.session_start"\n') + adapter = codex.CodexAdapter() + adapter.install_hooks(python_path="/synthetic/python") + groups = adapter._parse_existing_hooks(codex._CONFIG_PATH.read_text())["SessionStart"] + self.assertEqual(len(groups), 1) + self.assertIn("--host codex --event SessionStart", groups[0]["hooks"][0]["command"]) + gemini = source_module("truememory.hooks.adapters.gemini", "truememory/hooks/adapters/gemini.py") + gemini._CONFIG_PATH = self.home / ".gemini/settings.json" + self.write(gemini._CONFIG_PATH, json.dumps({"hooks": {"SessionStart": [{"hooks": [ + {"command": "python -m truememory.ingest.hooks.session_start"}]}]}})) + gemini.GeminiAdapter().install_hooks(python_path="/synthetic/python") + groups = json.loads(gemini._CONFIG_PATH.read_text())["hooks"]["SessionStart"] + self.assertEqual(len(groups), 1) + self.assertIn("--host gemini --event SessionStart", groups[0]["hooks"][0]["command"]) + + def test_cursor_extraction_uses_native_conversation_identity(self) -> None: + os.environ["TRUEMEMORY_TRANSCRIPT_DIR"] = str(self.home / "synthetic-transcripts") + path = self.write(self.home / "synthetic-transcripts/session.jsonl", json.dumps([ + {"role": "user", "content": "synthetic turn"} for _ in range(5) + ])) + for module, argv in ((self.stop, ["stop.py", "--host", "cursor", "--event", "stop"]), + (self.compact, ["compact.py", "--host", "cursor", "--event", "preCompact"])): + for conversation in ("synthetic-cursor-a", "synthetic-cursor-b"): + with self.subTest(module=module.__name__, conversation=conversation), \ + patch.object(self.compact, "save_snapshot") as snapshot, \ + patch.object(self.stop, "_run_background_ingestion", return_value=17) as spawn, \ + patch.object(sys, "argv", argv), \ + patch.object(sys, "stdin", io.StringIO(json.dumps({ + "transcript_path": str(path), "conversation_id": conversation, + }))): + module.main() + self.assertEqual(spawn.call_args.args[1], conversation) + if module is self.compact: + self.assertEqual(snapshot.call_args.args[1], conversation) + + def test_hook_identity_is_host_scoped_and_preserves_each_caller_default(self) -> None: + payload = {"conversation_id": "synthetic-native", "session_id": "synthetic-legacy", "host": "cursor"} + self.assertEqual(self.shared.hook_session_id(payload, "cursor"), "synthetic-native") + for host in ("claude", "codex", "gemini"): + self.assertEqual(self.shared.hook_session_id(payload, host), "synthetic-legacy") + self.assertEqual(self.shared.hook_session_id({"conversation_id": "synthetic-native"}, host), "unknown") + for malformed in (None, {}, [], 42, "", " ", "bad\0id"): + with self.subTest(malformed=malformed): + payload = {"conversation_id": malformed, "session_id": "synthetic-legacy"} + self.assertEqual(self.shared.hook_session_id(payload, "cursor"), "synthetic-legacy") + self.assertEqual(self.shared.hook_session_id({"conversation_id": malformed}, "cursor"), "unknown") + self.assertEqual(self.shared.hook_session_id({"conversation_id": malformed}, "cursor", default=""), "") + for malformed in (None, [], 42, "text"): + self.assertEqual(self.shared.hook_session_id(malformed, "cursor"), "unknown") + self.assertEqual(self.shared.hook_session_id(payload, "invalid", default=""), "") + + def test_module_binding_scope_retains_unrelated_imports_and_restores_owned_keys(self) -> None: + owned = "synthetic_owned_module_761" + unrelated = "synthetic_later_import_761" + previous = types.ModuleType(owned) + replacement = types.ModuleType(owned) + imported = types.ModuleType(unrelated) + with scoped_module_bindings({owned: previous}, {owned, unrelated}): + with scoped_module_bindings({owned: replacement}, {owned}): + sys.modules[unrelated] = imported + self.assertIs(sys.modules[owned], replacement) + self.assertIs(sys.modules[owned], previous) + self.assertIs(sys.modules[unrelated], imported) + + def test_spawn_fixtures_require_five_real_user_turns(self) -> None: + path = self.native_path("gemini") + self.write(path, json.dumps([{"role": "user", "content": "synthetic turn"}])) + self.assertFalse(self.stop._has_enough_messages(str(path), 5)) + self.write(path, json.dumps([{"role": "user", "content": "synthetic turn"} for _ in range(5)])) + self.assertTrue(self.stop._has_enough_messages(str(path), 5)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-transcript-inline-306.py b/tests/test-transcript-inline-306.py new file mode 100644 index 00000000..a1f0e707 --- /dev/null +++ b/tests/test-transcript-inline-306.py @@ -0,0 +1,157 @@ +"""Legacy transcript path handling must not expose or ingest source names.""" +from __future__ import annotations + +import errno +import json +from pathlib import Path +import stat +import sys +import tempfile +import types +import unittest +from unittest.mock import patch + + +SENTINEL = "synthetic-private-content-sentinel" +PATH_SENTINEL = "synthetic-private-path-sentinel" + + +def load_parser() -> types.ModuleType: + path = Path(__file__).resolve().parents[1] / "truememory/ingest/transcript.py" + module = types.ModuleType("inline_boundary_parser") + with patch.dict(sys.modules, {module.__name__: module}): + exec(compile(path.read_text(encoding="utf-8"), str(path), "exec"), module.__dict__) + return module + + +class InlineBoundaryTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.parser = load_parser() + + def assert_private_failure(self, source: str | Path) -> None: + with self.assertLogs(self.parser.log, level="ERROR") as captured: + self.assertEqual(self.parser.parse_transcript(source), []) + self.assertEqual(len(captured.records), 1) + record = captured.records[0] + self.assertEqual(getattr(record, "error_category", None), "source_unreadable") + self.assertIn("cannot read transcript", record.getMessage().lower()) + self.assertEqual(record.args, ()) + self.assertIsNone(record.exc_info) + self.assertNotIn(SENTINEL, str(record.__dict__)) + self.assertNotIn(PATH_SENTINEL, str(record.__dict__)) + + def test_real_long_inline_input(self) -> None: + content = SENTINEL + "x" * 600 + for text in ( + json.dumps([{"role": "user", "content": content}]), + json.dumps({"role": "user", "content": content}), + "Human: " + content, + ): + with self.subTest(format=text[0]): + messages = self.parser.parse_transcript(text) + self.assertEqual(len(messages), 1) + self.assertEqual(messages[0].content, content) + self.assertEqual(messages[0].role, "human") + + def test_missing_or_invalid_path_probe_parses_inline(self) -> None: + text = json.dumps([{"role": "user", "content": SENTINEL}]) + invalid_windows_name = OSError(errno.EINVAL, SENTINEL, text) + invalid_windows_name.winerror = 123 + for error in (OSError(errno.ENAMETOOLONG, SENTINEL, text), + FileNotFoundError(errno.ENOENT, SENTINEL, text), + NotADirectoryError(errno.ENOTDIR, SENTINEL, text), + invalid_windows_name, ValueError(SENTINEL)): + with self.subTest(error=type(error).__name__, errno=getattr(error, "errno", None)): + with patch.object(Path, "stat", side_effect=error): + messages = self.parser.parse_transcript(text) + detailed = self.parser.parse_transcript_outcome(text) + self.assertEqual([(m.role, m.content) for m in messages], [("human", SENTINEL)]) + self.assertEqual(detailed.messages, messages) + self.assertTrue(detailed.complete) + + def test_denied_or_broken_stat_does_not_ingest_path(self) -> None: + for code in (errno.EACCES, errno.EIO, errno.ELOOP, errno.EINVAL): + with self.subTest(errno=code): + with patch.object(Path, "stat", side_effect=OSError(code, SENTINEL, PATH_SENTINEL)): + self.assert_private_failure(PATH_SENTINEL) + detailed = self.parser.parse_transcript_outcome(PATH_SENTINEL) + self.assertEqual((detailed.status, detailed.messages), ("unreadable", [])) + self.assertEqual(detailed.error_categories, ("source_unreadable",)) + self.assertNotIn(SENTINEL, repr(detailed)) + self.assertNotIn(PATH_SENTINEL, repr(detailed)) + + def test_python314_suppressed_error_probes_are_not_used(self) -> None: + with patch.object(Path, "exists", return_value=False) as exists: + with patch.object(Path, "is_file", return_value=False) as is_file: + with patch.object(Path, "stat", side_effect=PermissionError(errno.EACCES, SENTINEL, PATH_SENTINEL)): + self.assert_private_failure(PATH_SENTINEL) + detailed = self.parser.parse_transcript_outcome(PATH_SENTINEL) + self.assertEqual((detailed.status, detailed.messages), ("unreadable", [])) + exists.assert_not_called() + is_file.assert_not_called() + + def test_post_probe_read_failure_never_falls_back(self) -> None: + for error in (PermissionError(errno.EACCES, SENTINEL, PATH_SENTINEL), + OSError(errno.EIO, SENTINEL, PATH_SENTINEL), + OSError(errno.ENAMETOOLONG, SENTINEL, PATH_SENTINEL), + ValueError(SENTINEL)): + with self.subTest(error=type(error).__name__): + with patch.object(Path, "stat", return_value=types.SimpleNamespace(st_mode=stat.S_IFREG)): + with patch.object(Path, "read_text", side_effect=error): + self.assert_private_failure(PATH_SENTINEL) + + def test_explicit_unreadable_path_never_falls_back(self) -> None: + for error in (PermissionError(errno.EACCES, SENTINEL, PATH_SENTINEL), + OSError(errno.EIO, SENTINEL, PATH_SENTINEL), + OSError(errno.ENAMETOOLONG, SENTINEL, PATH_SENTINEL), + ValueError(SENTINEL)): + with self.subTest(error=type(error).__name__): + with patch.object(Path, "read_text", side_effect=error), patch.object(Path, "stat") as probe: + self.assert_private_failure(Path(PATH_SENTINEL)) + probe.assert_not_called() + + def test_explicit_missing_path_preserves_empty_result(self) -> None: + with patch.object(Path, "stat", return_value=types.SimpleNamespace(st_mode=stat.S_IFREG)): + with patch.object(Path, "read_text", side_effect=FileNotFoundError(PATH_SENTINEL)): + with patch.object(self.parser.log, "error") as error: + for source in (Path(PATH_SENTINEL), PATH_SENTINEL): + self.assertEqual(self.parser.parse_transcript(source), []) + error.assert_not_called() + + def test_existing_file_is_read_once_after_probe(self) -> None: + text = json.dumps([{"role": "assistant", "content": SENTINEL}]) + with patch.object(Path, "stat", return_value=types.SimpleNamespace(st_mode=stat.S_IFREG)) as probe: + with patch.object(Path, "read_text", return_value=text) as read: + messages = self.parser.parse_transcript(PATH_SENTINEL) + probe.assert_called_once_with() + read.assert_called_once_with(encoding="utf-8", errors="replace") + self.assertEqual([(m.role, m.content) for m in messages], [("assistant", SENTINEL)]) + + def test_plain_non_path_and_empty_inputs_preserve_behavior(self) -> None: + with patch.object(Path, "stat", side_effect=FileNotFoundError(errno.ENOENT, "synthetic missing")): + self.assertEqual(self.parser.parse_transcript(""), []) + messages = self.parser.parse_transcript("Human: synthetic ordinary message") + self.assertEqual([(m.role, m.content) for m in messages], [("human", "synthetic ordinary message")]) + + def test_nonregular_string_keeps_inline_compatibility(self) -> None: + with patch.object(Path, "stat", return_value=types.SimpleNamespace(st_mode=stat.S_IFDIR)): + messages = self.parser.parse_transcript("Human: synthetic inline text") + self.assertEqual([(m.role, m.content) for m in messages], [("human", "synthetic inline text")]) + + +class InlineBoundaryFileTests(unittest.TestCase): + def test_real_file_string_and_explicit_path(self) -> None: + parser = load_parser() + with tempfile.TemporaryDirectory() as folder: + path = Path(folder) / "synthetic-transcript.json" + path.write_text(json.dumps([{"role": "user", "content": SENTINEL}]), encoding="utf-8") + for source in (path, str(path)): + with self.subTest(explicit=isinstance(source, Path)): + messages = parser.parse_transcript(source) + self.assertEqual([(m.role, m.content) for m in messages], [("human", SENTINEL)]) + self.assertEqual(parser.parse_transcript(Path(folder) / "missing.json"), []) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_codex_adapter.py b/tests/test_codex_adapter.py index 6a3184a4..6bd884de 100644 --- a/tests/test_codex_adapter.py +++ b/tests/test_codex_adapter.py @@ -218,7 +218,7 @@ def test_install_hooks_migrates_legacy_format(tmp_path, monkeypatch): '[general]\ntheme = "dark"\n\n' '[[hooks]]\n' 'event = "SessionStart"\n' - 'command = "/usr/bin/python3 /path/to/truememory/session_start.py"\n' + 'command = "/usr/bin/python3 /path/to/truememory/ingest/hooks/session_start.py"\n' 'timeout = 10000\n', encoding="utf-8", ) @@ -338,7 +338,7 @@ def test_uninstall_cleans_legacy_format(tmp_path, monkeypatch): '[general]\ntheme = "dark"\n\n' '[[hooks]]\n' 'event = "SessionStart"\n' - 'command = "/usr/bin/python3 /path/to/truememory/session_start.py"\n' + 'command = "/usr/bin/python3 /path/to/truememory/ingest/hooks/session_start.py"\n' 'timeout = 10000\n', encoding="utf-8", ) @@ -380,7 +380,7 @@ def test_remove_hook_blocks_correct_format(): '[[hooks.SessionStart]]\n\n' '[[hooks.SessionStart.hooks]]\n' 'type = "command"\n' - 'command = "/usr/bin/python3 /path/to/truememory/session_start.py"\n' + 'command = "/usr/bin/python3 /path/to/truememory/ingest/hooks/session_start.py"\n' 'timeout = 10\n\n' '[[hooks.MyCustomHook]]\n\n' '[[hooks.MyCustomHook.hooks]]\n' @@ -403,7 +403,7 @@ def test_remove_legacy_hook_blocks_with_blank_lines(): '[[hooks]]\n' 'event = "SessionStart"\n' '\n' - 'command = "/usr/bin/python3 /path/to/truememory/session_start.py"\n' + 'command = "/usr/bin/python3 /path/to/truememory/ingest/hooks/session_start.py"\n' 'timeout = 10000\n\n' '[[hooks]]\n' 'event = "MyCustomHook"\n' @@ -432,12 +432,12 @@ def test_parse_existing_hooks_correct_format(): '[[hooks.SessionStart]]\n\n' '[[hooks.SessionStart.hooks]]\n' 'type = "command"\n' - 'command = "/usr/bin/python3 /path/to/truememory/session_start.py"\n' + 'command = "/usr/bin/python3 /path/to/truememory/ingest/hooks/session_start.py"\n' 'timeout = 10\n\n' '[[hooks.Stop]]\n\n' '[[hooks.Stop.hooks]]\n' 'type = "command"\n' - 'command = "/usr/bin/python3 /path/to/truememory/stop.py"\n' + 'command = "/usr/bin/python3 /path/to/truememory/ingest/hooks/stop.py"\n' 'timeout = 5\n' ) result = CodexAdapter._parse_existing_hooks(text) @@ -446,7 +446,7 @@ def test_parse_existing_hooks_correct_format(): assert "Stop" in result mg = result["SessionStart"][0] assert "hooks" in mg - assert mg["hooks"][0]["command"] == "/usr/bin/python3 /path/to/truememory/session_start.py" + assert mg["hooks"][0]["command"] == "/usr/bin/python3 /path/to/truememory/ingest/hooks/session_start.py" def test_parse_existing_hooks_empty(): @@ -465,7 +465,7 @@ def test_event_already_registered_correct_format(): "hooks": [ { "type": "command", - "command": "/usr/bin/python3 /path/to/truememory/session_start.py", + "command": "/usr/bin/python3 /path/to/truememory/ingest/hooks/session_start.py", "timeout": 10, } ] diff --git a/tests/test_configure_reembed.py b/tests/test_configure_reembed.py index e267b578..4738762c 100644 --- a/tests/test_configure_reembed.py +++ b/tests/test_configure_reembed.py @@ -31,8 +31,29 @@ def server(monkeypatch, tmp_path): import truememory.mcp_server as ms monkeypatch.setattr(ms, "_TRUEMEMORY_DIR", home / ".truememory") monkeypatch.setattr(ms, "_CONFIG_PATH", home / ".truememory" / "config.json") + monkeypatch.setattr(ms, "_CONFIG_LOCK_PATH", ms._CONFIG_PATH.with_name("config.json.lock")) monkeypatch.setattr(ms, "_DB_PATH", str(db_path)) monkeypatch.setattr(ms, "_memory", None) + import numpy as np + import truememory.vector_search as vs + + class SyntheticEncoder: + def encode(self, texts, **kwargs): + # 256 * (1 / 16) ** 2 = 1, so the synthetic vectors have unit norm. + return np.full((len(texts), 256), 1 / 16, dtype=np.float32) + + encoder = SyntheticEncoder() + monkeypatch.setattr(vs, "get_model", lambda: encoder) + memory = ms._get_memory() + memory._engine._ensure_connection(_suppress_maintenance=True) + # The no-op contract requires coherent source identity even when this + # Python build cannot load the vector extension. + with memory._engine._write_lock: + memory._engine.conn.executemany( + "INSERT OR REPLACE INTO metadata(key,value) VALUES (?,?)", + (("embed_model", vs.EMBEDDING_MODEL), ("embed_dim", str(vs._embedding_dim))), + ) + memory._engine.conn.commit() yield ms # Teardown: drop cached Memory and any model-level state mutated by # truememory_configure (which modifies vector_search globals via diff --git a/tests/test_configure_tier_persist.py b/tests/test_configure_tier_persist.py index 2d60164c..34b149e9 100644 --- a/tests/test_configure_tier_persist.py +++ b/tests/test_configure_tier_persist.py @@ -32,6 +32,7 @@ def server(monkeypatch, tmp_path): import truememory.mcp_server as ms monkeypatch.setattr(ms, "_TRUEMEMORY_DIR", home / ".truememory") monkeypatch.setattr(ms, "_CONFIG_PATH", home / ".truememory" / "config.json") + monkeypatch.setattr(ms, "_CONFIG_LOCK_PATH", ms._CONFIG_PATH.with_name("config.json.lock")) monkeypatch.setattr(ms, "_DB_PATH", str(db_path)) monkeypatch.setattr(ms, "_memory", None) yield ms @@ -46,12 +47,17 @@ def server(monkeypatch, tmp_path): def _no_op_model(server, monkeypatch): - """Stub out the model/reranker side effects so the test stays unit-level.""" + """Exercise lazy identity setters while refusing native model work.""" import truememory.vector_search as vs import truememory.reranker as rr - monkeypatch.setattr(vs, "set_embedding_model", lambda tier: None) - monkeypatch.setattr(rr, "set_active_tier", lambda tier: None) - monkeypatch.setattr(server, "_set_reranker", lambda name: None) + + def forbidden_model_load(*args, **kwargs): + raise AssertionError("Config-only setup must not construct or probe a model") + + monkeypatch.setattr(vs, "get_model", forbidden_model_load) + monkeypatch.setattr(vs, "_load_frozen_embedding_target", forbidden_model_load) + monkeypatch.setattr(rr, "get_reranker", forbidden_model_load) + monkeypatch.setattr(server, "_set_reranker", forbidden_model_load) def test_config_only_switch_persists_tier(server, monkeypatch): diff --git a/tests/test_consolidation_gaps.py b/tests/test_consolidation_gaps.py index 3c92e12c..12ce1cc1 100644 --- a/tests/test_consolidation_gaps.py +++ b/tests/test_consolidation_gaps.py @@ -1,134 +1,59 @@ -"""Regression tests for consolidation gaps (clustering + preferences). - -Verifies that consolidate() calls cluster_messages and extract_preferences — -features that were previously only available via ingest(). -""" -from __future__ import annotations - -import tempfile -import os -from unittest.mock import patch, MagicMock - - -from truememory.engine import TrueMemoryEngine -from truememory.storage import create_db - - -def _make_engine_with_messages(n=20): - """Create an engine with messages in a temp DB (no vectors needed). - - Disables startup consolidation to prevent background threads from - accessing SQLite cross-thread (causes segfault on Python 3.14). - """ - td = tempfile.mkdtemp() - db = os.path.join(td, "test.db") - conn = create_db(db) - for i in range(n): - conn.execute( - "INSERT INTO messages (content, sender, recipient, timestamp, category, modality) " - "VALUES (?, ?, '', '', '', '')", - (f"Message {i} about topic {i % 5}", "alice" if i % 2 == 0 else "bob"), - ) - conn.commit() - conn.close() - - eng = TrueMemoryEngine(db_path=db) - eng._has_consolidation = False - eng._ensure_connection() - return eng, td - - -def test_consolidate_calls_cluster_messages(): - """consolidate() must call cluster_messages when clustering is available.""" - eng, td = _make_engine_with_messages() - eng._has_vectors = True - - with patch("truememory.engine._HAS_CLUSTERING", True), \ - patch("truememory.clustering.cluster_messages", return_value=3) as mock_cm: - result = eng.consolidate() - - assert "cluster_messages" in result - assert "ERROR" not in result["cluster_messages"] - assert "3 clusters" in result["cluster_messages"] - mock_cm.assert_called_once_with(eng.conn) - eng.close() - - -def test_consolidate_calls_extract_preferences(): - """consolidate() must call extract_preferences when personality is available.""" - eng, td = _make_engine_with_messages() - - with patch("truememory.engine._HAS_PERSONALITY", True), \ - patch("truememory.personality.extract_preferences", return_value={}) as mock_ep: - result = eng.consolidate() - - assert "extract_preferences" in result - assert "ERROR" not in result["extract_preferences"] - mock_ep.assert_called_once_with(eng.conn) - eng.close() - - -def test_consolidate_skips_clustering_without_vectors(): - """consolidate() must skip clustering when vectors are unavailable.""" - eng, td = _make_engine_with_messages() - eng._has_vectors = False - - result = eng.consolidate() - assert "cluster_messages" not in result - eng.close() - - -def test_consolidate_handles_cluster_error_gracefully(): - """consolidate() must catch and report clustering errors.""" - eng, td = _make_engine_with_messages() - eng._has_vectors = True - - with patch("truememory.engine._HAS_CLUSTERING", True), \ - patch("truememory.clustering.cluster_messages", side_effect=RuntimeError("boom")): - result = eng.consolidate() - - assert "cluster_messages" in result - assert "ERROR" in result["cluster_messages"] - eng.close() - - -def test_auto_consolidation_threshold_lowered(): - """Default auto-consolidation threshold is 25 (was 100).""" - eng, td = _make_engine_with_messages() - assert eng._auto_consolidate_threshold == 25 - eng.close() - - -def test_startup_consolidation_triggers_when_clusters_empty(): - """On startup, if messages exist but clusters are empty, consolidate fires.""" - eng, td = _make_engine_with_messages(n=30) - eng._has_consolidation = True - - import truememory.engine as _eng_mod - with patch.object(_eng_mod.threading, "Thread") as mock_thread_cls: - mock_thread = MagicMock() - mock_thread_cls.return_value = mock_thread - eng._maybe_startup_consolidate() - - mock_thread_cls.assert_called_once() - assert mock_thread_cls.call_args.kwargs["name"] == "startup-consolidate" - mock_thread.start.assert_called_once() - eng.close() - - -def test_startup_consolidation_skips_when_few_messages(): - """Don't consolidate on startup if fewer messages than threshold.""" - eng, td = _make_engine_with_messages(n=5) - - consolidate_called = False - orig = eng._bg_consolidate - def spy(): - nonlocal consolidate_called - consolidate_called = True - return orig() - - eng._bg_consolidate = spy - eng._has_consolidation = True - eng._maybe_startup_consolidate() - assert not consolidate_called - eng.close() +"""Behavioral coverage for all maintenance layers and startup provenance.""" + +import runpy +from pathlib import Path +from unittest.mock import patch + + +_ROUTING = runpy.run_path(str(Path(__file__).with_name("test-maintenance-engine-routing-753.py"))) + + +class TestConsolidationGaps(_ROUTING["RoutingFixture"]): + def test_clustering_count_matches_real_published_centroids(self): + self.add(5) + self.native.fit = lambda: setattr(self.native, "labels", [0, 0, 1, 1, 2]) + result = self.engine.consolidate() + self.assertIn("3 clusters", result["cluster_messages"]) + self.assertEqual(self.conn.execute("SELECT count(*) FROM cluster_centroids").fetchone()[0], 3) + + def test_scheduled_preferences_report_unavailable_without_extraction(self): + function = self.modules["truememory.personality"].extract_preferences + with patch.object(self.modules["truememory.personality"], "extract_preferences", wraps=function) as preference: + result = self.engine.consolidate() + self.assertIn("extract_preferences", result) + self.assertEqual(result["extract_preferences"], "UNAVAILABLE (ScheduledPreferencesUnsupported)") + preference.assert_not_called() + + def test_missing_vector_index_reports_unavailable_without_skipping_siblings(self): + self.conn.execute("DROP TABLE vec_messages_edge") + self.conn.commit() + result = self.engine.consolidate() + self.assertIn("UNAVAILABLE", result["cluster_messages"]) + self.assertIn("build_summaries", result) + self.assertNotIn("ERROR", result["build_summaries"]) + + def test_cluster_error_is_categorical_and_siblings_still_run(self): + with patch.object(self.cluster, "cluster_messages", side_effect=RuntimeError("synthetic private sentinel")): + result = self.engine.consolidate() + self.assertEqual(result["cluster_messages"], "ERROR (RuntimeError)") + self.assertNotIn("synthetic private sentinel", str(result)) + self.assertNotIn("ERROR", result["detect_episodes"]) + + def test_startup_attempts_initial_small_corpus_once(self): + self.add(5) + self.engine._has_consolidation = True + self.engine._maybe_startup_consolidate() + self.completed() + self.assertEqual(self.state().outcome, "success_empty") + with patch.object(self.coordinator, "request_layers") as request: + self.engine._maybe_startup_consolidate() + request.assert_not_called() + + def test_successful_empty_clusters_do_not_retrigger_all_layers(self): + self.add(30) + self.engine.consolidate() + self.assertEqual(self.conn.execute("SELECT count(*) FROM cluster_centroids").fetchone()[0], 0) + self.engine._has_consolidation = True + with patch.object(self.coordinator, "request_layers") as request: + self.engine._maybe_startup_consolidate() + request.assert_not_called() diff --git a/tests/test_cursor_adapter.py b/tests/test_cursor_adapter.py index f2417737..aba07a8a 100644 --- a/tests/test_cursor_adapter.py +++ b/tests/test_cursor_adapter.py @@ -244,7 +244,7 @@ def test_uninstall_preserves_other_entries(tmp_path, monkeypatch): "hooks": { "sessionStart": [ {"command": "my-hook", "timeout": 5000}, - {"command": "/path/to/truememory/hooks/session_start.py", "timeout": 10000}, + {"command": "/usr/bin/python3 /path/to/truememory/ingest/hooks/session_start.py", "timeout": 10000}, ] }, }), encoding="utf-8") @@ -416,16 +416,16 @@ def test_install_migrates_stale_user_prompt_submit(tmp_path, monkeypatch): "version": 1, "hooks": { "sessionStart": [ - {"command": "/usr/bin/python3 /path/to/truememory/session_start.py", "timeout": 10000} + {"command": "/usr/bin/python3 /path/to/truememory/ingest/hooks/session_start.py", "timeout": 10000} ], "stop": [ - {"command": "/usr/bin/python3 /path/to/truememory/stop.py", "timeout": 5000} + {"command": "/usr/bin/python3 /path/to/truememory/ingest/hooks/stop.py", "timeout": 5000} ], "userPromptSubmit": [ - {"command": "/usr/bin/python3 /path/to/truememory/user_prompt_submit.py", "timeout": 5000} + {"command": "/usr/bin/python3 /path/to/truememory/ingest/hooks/user_prompt_submit.py", "timeout": 5000} ], "preCompact": [ - {"command": "/usr/bin/python3 /path/to/truememory/compact.py", "timeout": 5000} + {"command": "/usr/bin/python3 /path/to/truememory/ingest/hooks/compact.py", "timeout": 5000} ], }, } @@ -460,13 +460,13 @@ def test_install_updates_stale_millisecond_timeouts(tmp_path, monkeypatch): "version": 1, "hooks": { "sessionStart": [ - {"command": "/usr/bin/python3 /path/to/truememory/session_start.py", "timeout": 10000} + {"command": "/usr/bin/python3 /path/to/truememory/ingest/hooks/session_start.py", "timeout": 10000} ], "stop": [ - {"command": "/usr/bin/python3 /path/to/truememory/stop.py", "timeout": 5000} + {"command": "/usr/bin/python3 /path/to/truememory/ingest/hooks/stop.py", "timeout": 5000} ], "preCompact": [ - {"command": "/usr/bin/python3 /path/to/truememory/compact.py", "timeout": 5000} + {"command": "/usr/bin/python3 /path/to/truememory/ingest/hooks/compact.py", "timeout": 5000} ], }, } @@ -496,11 +496,11 @@ def test_install_preserves_non_truememory_hooks(tmp_path, monkeypatch): "version": 1, "hooks": { "userPromptSubmit": [ - {"command": "/usr/bin/python3 /path/to/truememory/user_prompt_submit.py", "timeout": 5000}, + {"command": "/usr/bin/python3 /path/to/truememory/ingest/hooks/user_prompt_submit.py", "timeout": 5000}, {"command": "/usr/local/bin/my-custom-linter", "timeout": 10}, ], "sessionStart": [ - {"command": "/usr/bin/python3 /path/to/truememory/session_start.py", "timeout": 10000} + {"command": "/usr/bin/python3 /path/to/truememory/ingest/hooks/session_start.py", "timeout": 10000} ], }, } diff --git a/tests/test_gemini_adapter.py b/tests/test_gemini_adapter.py index b783b073..837399c3 100644 --- a/tests/test_gemini_adapter.py +++ b/tests/test_gemini_adapter.py @@ -252,7 +252,7 @@ def test_uninstall_preserves_other_entries(tmp_path, monkeypatch): "hooks": { "SessionStart": [ {"command": "my-hook", "timeout": 5000}, - {"command": "/path/to/truememory/hooks/session_start.py", "timeout": 10000}, + {"command": "/usr/bin/python3 /path/to/truememory/ingest/hooks/session_start.py", "timeout": 10000}, ] }, "theme": "dark", @@ -278,7 +278,7 @@ def test_uninstall_removes_nested_format(tmp_path, monkeypatch): "hooks": { "SessionStart": [ {"hooks": [{"type": "command", "command": "my-hook", "timeout": 5000}]}, - {"hooks": [{"type": "command", "command": "/path/to/truememory/session_start.py", "timeout": 10000, "name": "truememory-sessionstart"}]}, + {"hooks": [{"type": "command", "command": "/usr/bin/python3 /path/to/truememory/ingest/hooks/session_start.py", "timeout": 10000, "name": "truememory-sessionstart"}]}, ] }, }), encoding="utf-8") diff --git a/tests/test_hardening.py b/tests/test_hardening.py index 654845ed..c0fe657b 100644 --- a/tests/test_hardening.py +++ b/tests/test_hardening.py @@ -35,11 +35,11 @@ def _make_engine(n_messages=0, consolidation=False, vectors=False): eng = TrueMemoryEngine(db_path=db) eng._has_consolidation = False + eng._has_style_vec = False eng._ensure_connection() eng._has_consolidation = consolidation eng._has_vectors = vectors eng._has_hybrid = vectors - eng._has_style_vec = False return eng, td @@ -188,15 +188,19 @@ class TestAutoConsolidation: def test_auto_consolidation_doesnt_fire_below_threshold(self): eng, td = _make_engine(n_messages=0) + # Establish the initial successful/empty or unavailable attempt. The + # threshold applies after that durable baseline, including on reopen. + eng.consolidate() eng._has_consolidation = True eng._auto_consolidate_threshold = 25 + eng.conn.executemany("INSERT INTO messages(content) VALUES (?)", [("synthetic append",)] * 24) + eng.conn.commit() - import truememory.engine as _eng_mod - with patch.object(_eng_mod.threading, "Thread") as mock_thread_cls: - for i in range(24): + with patch.object(eng._maintenance_coordinator, "request_layers") as request: + for _ in range(3): eng._maybe_auto_consolidate() - mock_thread_cls.assert_not_called() + request.assert_not_called() eng.close() def test_startup_consolidation_no_crash_on_closed_conn(self): diff --git a/tests/test_hermes_adapter.py b/tests/test_hermes_adapter.py index 8af3fb5f..855a9895 100644 --- a/tests/test_hermes_adapter.py +++ b/tests/test_hermes_adapter.py @@ -114,13 +114,14 @@ def test_install_hooks_creates_hooks_config(tmp_path, monkeypatch): data = yaml.safe_load(config_path.read_text(encoding="utf-8")) hooks = data["hooks"] - # Should have 4 events registered - assert len(hooks) == 4 + # Native capture needs the post-turn assistant event too. + assert len(hooks) == 5 # Verify correct Hermes event names (from VALID_HOOKS) assert "on_session_start" in hooks assert "on_session_end" in hooks assert "pre_llm_call" in hooks + assert "post_llm_call" in hooks assert "on_session_finalize" in hooks # Each event should map to a list of entries @@ -131,11 +132,14 @@ def test_install_hooks_creates_hooks_config(tmp_path, monkeypatch): assert isinstance(entry, dict) assert "command" in entry # command is required assert "truememory" in entry["command"].lower() + assert "hermes.py" in entry["command"] + assert f"--event {event}" in entry["command"] # Verify timeouts are set (in seconds, as per Hermes source) assert hooks["on_session_start"][0]["timeout"] == 30 assert hooks["on_session_end"][0]["timeout"] == 30 assert hooks["pre_llm_call"][0]["timeout"] == 10 + assert hooks["post_llm_call"][0]["timeout"] == 30 assert hooks["on_session_finalize"][0]["timeout"] == 15 # Verify no invalid events used diff --git a/tests/test_issue_396_memory_intensity.py b/tests/test_issue_396_memory_intensity.py index 5424e0ff..eb3341ad 100644 --- a/tests/test_issue_396_memory_intensity.py +++ b/tests/test_issue_396_memory_intensity.py @@ -35,6 +35,7 @@ def server(monkeypatch, tmp_path): import truememory.mcp_server as ms monkeypatch.setattr(ms, "_TRUEMEMORY_DIR", home / ".truememory") monkeypatch.setattr(ms, "_CONFIG_PATH", home / ".truememory" / "config.json") + monkeypatch.setattr(ms, "_CONFIG_LOCK_PATH", ms._CONFIG_PATH.with_name("config.json.lock")) monkeypatch.setattr(ms, "_DB_PATH", str(db_path)) monkeypatch.setattr(ms, "_memory", None) # Invalidate config cache so each test starts clean @@ -56,9 +57,15 @@ def _no_op_model(server, monkeypatch): """Stub out model/reranker side effects.""" import truememory.vector_search as vs import truememory.reranker as rr - monkeypatch.setattr(vs, "set_embedding_model", lambda tier: None) - monkeypatch.setattr(rr, "set_active_tier", lambda tier: None) - monkeypatch.setattr(server, "_set_reranker", lambda name: None) + def forbidden_model_load(*args, **kwargs): + raise AssertionError("Configuration must preserve lazy model slots") + + monkeypatch.setattr(vs, "_model", None) + monkeypatch.setattr(rr, "_model", None) + monkeypatch.setattr(vs, "get_model", forbidden_model_load) + monkeypatch.setattr(vs, "_load_frozen_embedding_target", forbidden_model_load) + monkeypatch.setattr(rr, "get_reranker", forbidden_model_load) + monkeypatch.setattr(server, "_set_reranker", forbidden_model_load) # --------------------------------------------------------------------------- diff --git a/tests/test_issue_458_socket_bounds.py b/tests/test_issue_458_socket_bounds.py index 59aa3e2f..085024a7 100644 --- a/tests/test_issue_458_socket_bounds.py +++ b/tests/test_issue_458_socket_bounds.py @@ -13,6 +13,7 @@ import threading import time from pathlib import Path +from unittest.mock import Mock import pytest @@ -39,25 +40,42 @@ def test_issue_458_max_message_size_constant_exists(self): f"Server MAX_MESSAGE_SIZE too large ({model_server._MAX_MESSAGE_SIZE})" ) - def test_issue_458_server_bounds_check_in_handle_client(self): - """model_server.py handle_client must check length before allocating.""" - import inspect - from truememory.model_server import ModelServer - - source = inspect.getsource(ModelServer.handle_client) - assert "_MAX_MESSAGE_SIZE" in source or "MAX_MESSAGE" in source, ( - "handle_client has no message size check — unbounded allocation DoS" - ) - - def test_issue_458_client_bounds_check_in_send_request(self): - """model_client.py _send_request must check response length.""" - import inspect - from truememory.model_client import _send_request - - source = inspect.getsource(_send_request) - assert "_MAX_MESSAGE_SIZE" in source or "MAX_MESSAGE" in source, ( - "_send_request has no response size check — unbounded allocation DoS" - ) + def test_issue_458_server_bounds_check_in_handle_client(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Reject an oversized header without reading or decoding its body.""" + from truememory import model_server + + server = model_server.ModelServer() + conn = Mock(spec=socket.socket) + conn.recv.side_effect = [ + struct.pack(">I", model_server._MAX_MESSAGE_SIZE + 1), + AssertionError("Oversized request body was read"), + ] + handled = Mock(side_effect=AssertionError("Oversized request was handled")) + monkeypatch.setattr(model_server, "_USE_UNIX", True) + monkeypatch.setattr(server, "handle_request", handled) + + server.handle_client(conn) + + conn.recv.assert_called_once_with(4) + conn.close.assert_called_once() + handled.assert_not_called() + + def test_issue_458_client_bounds_check_in_send_request(self, monkeypatch: pytest.MonkeyPatch) -> None: + """Reject an oversized response header before requesting its body.""" + from truememory import model_client + + conn = Mock(spec=socket.socket) + conn.recv.side_effect = [ + struct.pack(">I", model_client._MAX_MESSAGE_SIZE + 1), + AssertionError("Oversized response body was read"), + ] + monkeypatch.setattr(model_client, "_connect", lambda deadline: conn) + + with pytest.raises(ConnectionError, match="Response too large"): + model_client._send_request({"op": "ping"}, timeout=1) + + conn.recv.assert_called_once_with(4) + conn.close.assert_called_once() @_SKIP_NO_UNIX def test_issue_458_oversized_message_rejected(self): diff --git a/tests/test_issue_460_warm_search.py b/tests/test_issue_460_warm_search.py index 3380734c..ff204a16 100644 --- a/tests/test_issue_460_warm_search.py +++ b/tests/test_issue_460_warm_search.py @@ -89,6 +89,8 @@ def counting_encode(texts, **kw): m1._engine.close() m2._engine.close() + from truememory.maintenance import get_coordinator + assert get_coordinator(db_path).wait(10), "Synthetic maintenance teardown did not finish" def test_issue_460_metadata_survives_multiple_adds(self): """Metadata must persist across multiple add() calls.""" diff --git a/tests/test_issue_489_mps_safety.py b/tests/test_issue_489_mps_safety.py index 67dc971d..030b4db5 100644 --- a/tests/test_issue_489_mps_safety.py +++ b/tests/test_issue_489_mps_safety.py @@ -42,8 +42,8 @@ def test_encode_with_mps_fallback_normal_path(self): self.assertEqual(result, [[0.1, 0.2]]) model.encode.assert_called_once_with(["hello"]) - def test_encode_with_mps_fallback_oom_falls_to_cpu_and_restores(self): - """H4+H8: On OOM, should move to CPU, encode, then restore to MPS.""" + def test_encode_with_mps_fallback_oom_stays_on_cpu(self): + """The local policy now matches the server's no-repromotion policy.""" from truememory.mps_utils import encode_with_mps_fallback model = MagicMock() model.encode.side_effect = [ @@ -55,11 +55,7 @@ def test_encode_with_mps_fallback_oom_falls_to_cpu_and_restores(self): result = encode_with_mps_fallback(model, ["hello"]) self.assertEqual(result, [[0.1, 0.2]]) to_calls = [c.args[0] for c in model.to.call_args_list] - self.assertIn("cpu", to_calls) - self.assertIn("mps", to_calls) - cpu_idx = to_calls.index("cpu") - mps_idx = to_calls.index("mps") - self.assertLess(cpu_idx, mps_idx) + self.assertEqual(to_calls, ["cpu"]) def test_encode_with_mps_fallback_reraises_non_oom(self): from truememory.mps_utils import encode_with_mps_fallback diff --git a/tests/test_issue_497_edge_persist.py b/tests/test_issue_497_edge_persist.py index d7cb5f8c..ce0361f2 100644 --- a/tests/test_issue_497_edge_persist.py +++ b/tests/test_issue_497_edge_persist.py @@ -37,6 +37,7 @@ def server(monkeypatch, tmp_path): monkeypatch.setattr(ms, "_TRUEMEMORY_DIR", home / ".truememory") monkeypatch.setattr(ms, "_CONFIG_PATH", home / ".truememory" / "config.json") + monkeypatch.setattr(ms, "_CONFIG_LOCK_PATH", ms._CONFIG_PATH.with_name("config.json.lock")) monkeypatch.setattr(ms, "_DB_PATH", str(db_path)) monkeypatch.setattr(ms, "_memory", None) yield ms @@ -56,9 +57,15 @@ def _no_op_model(server, monkeypatch): import truememory.vector_search as vs import truememory.reranker as rr - monkeypatch.setattr(vs, "set_embedding_model", lambda tier: None) - monkeypatch.setattr(rr, "set_active_tier", lambda tier: None) - monkeypatch.setattr(server, "_set_reranker", lambda name: None) + def forbidden_model_load(*args, **kwargs): + raise AssertionError("Configuration must preserve lazy model slots") + + monkeypatch.setattr(vs, "_model", None) + monkeypatch.setattr(rr, "_model", None) + monkeypatch.setattr(vs, "get_model", forbidden_model_load) + monkeypatch.setattr(vs, "_load_frozen_embedding_target", forbidden_model_load) + monkeypatch.setattr(rr, "get_reranker", forbidden_model_load) + monkeypatch.setattr(server, "_set_reranker", forbidden_model_load) def test_issue_497_edge_tier_persists(server, monkeypatch): diff --git a/tests/test_issue_498_auto_consolidate.py b/tests/test_issue_498_auto_consolidate.py index 901d7f73..4295082f 100644 --- a/tests/test_issue_498_auto_consolidate.py +++ b/tests/test_issue_498_auto_consolidate.py @@ -1,37 +1,49 @@ -"""Regression test for M1 #498: auto-consolidation trigger. - -Consolidation should run automatically after a configurable number of -add() calls, not require manual truememory_consolidate invocation. -""" - -import inspect -import unittest - - -class TestAutoConsolidation(unittest.TestCase): - - def test_auto_consolidate_threshold_exists(self): - from truememory.engine import TrueMemoryEngine - source = inspect.getsource(TrueMemoryEngine.__init__) - self.assertIn("_auto_consolidate_threshold", source) - self.assertIn("TRUEMEMORY_AUTO_CONSOLIDATE_EVERY", source) - - def test_add_calls_maybe_auto_consolidate(self): - from truememory.engine import TrueMemoryEngine - source = inspect.getsource(TrueMemoryEngine.add) - self.assertIn("_maybe_auto_consolidate", source, - "add() should call _maybe_auto_consolidate()") - - def test_maybe_auto_consolidate_checks_threshold(self): - from truememory.engine import TrueMemoryEngine - source = inspect.getsource(TrueMemoryEngine._maybe_auto_consolidate) - self.assertIn("_auto_consolidate_threshold", source) - self.assertIn("_has_consolidation", source) - - def test_bg_consolidate_exists(self): - from truememory.engine import TrueMemoryEngine - self.assertTrue(hasattr(TrueMemoryEngine, "_bg_consolidate")) - - -if __name__ == "__main__": - unittest.main() +"""Durable automatic maintenance thresholds across engine instances.""" + +import runpy +from pathlib import Path +from unittest.mock import patch + + +_ROUTING = runpy.run_path(str(Path(__file__).with_name("test-maintenance-engine-routing-753.py"))) + + +class TestAutoConsolidation(_ROUTING["RoutingFixture"]): + def assert_empty_bootstrap(self) -> None: + maintenance = _ROUTING["MAINTENANCE"] + states = maintenance.read_layer_states( + self.conn, maintenance.engine_layer_specs(self.conn, self.coordinator)) + self.assertEqual(len(states), 8) + self.assertTrue(all(state.outcome == "success_empty" for state in states.values()), states) + self.assertTrue(all(state.attempted_insert_count == 0 for state in states.values()), states) + + def test_default_threshold_is_twenty_five(self): + self.assertEqual(self.engine._auto_consolidate_threshold, 25) + + def test_configured_threshold_uses_committed_source_inserts(self): + self.engine.consolidate() + self.assert_empty_bootstrap() + self.engine._auto_consolidate_threshold = 3 + self.engine._has_consolidation = True + with patch.object(self.coordinator, "request_layers", wraps=self.coordinator.request_layers) as request: + for _ in range(2): + self.engine.add("synthetic committed append") + request.assert_not_called() + self.engine.add("synthetic threshold append") + self.completed() + self.assertTrue(request.called) + self.assertTrue(all(call.kwargs["threshold"] == 3 for call in request.call_args_list)) + + def test_close_and_reopen_does_not_reset_threshold_progress(self): + self.engine.consolidate() + self.assert_empty_bootstrap() + self.add(24) + self.engine.close() + reopened = self.new_engine() + reopened._has_consolidation = True + with patch.object(self.coordinator, "request_layers", wraps=self.coordinator.request_layers) as request: + reopened._maybe_auto_consolidate() + request.assert_not_called() + reopened.add("synthetic twenty fifth") + self.completed() + self.assertTrue(request.called) diff --git a/tests/test_issue_502_config.py b/tests/test_issue_502_config.py index 9d03caf0..cc36c27c 100644 --- a/tests/test_issue_502_config.py +++ b/tests/test_issue_502_config.py @@ -16,6 +16,7 @@ import json import os +from pathlib import Path import threading import time @@ -35,6 +36,7 @@ def server(monkeypatch, tmp_path): monkeypatch.setattr(ms, "_TRUEMEMORY_DIR", home / ".truememory") monkeypatch.setattr(ms, "_CONFIG_PATH", home / ".truememory" / "config.json") + monkeypatch.setattr(ms, "_CONFIG_LOCK_PATH", ms._CONFIG_PATH.with_name("config.json.lock")) monkeypatch.setattr(ms, "_DB_PATH", str(db_path)) monkeypatch.setattr(ms, "_memory", None) # Reset config cache so each test starts clean @@ -58,9 +60,15 @@ def _no_op_model(server, monkeypatch): import truememory.vector_search as vs import truememory.reranker as rr - monkeypatch.setattr(vs, "set_embedding_model", lambda tier: None) - monkeypatch.setattr(rr, "set_active_tier", lambda tier: None) - monkeypatch.setattr(server, "_set_reranker", lambda name: None) + def forbidden_model_load(*args, **kwargs): + raise AssertionError("Configuration must preserve lazy model slots") + + monkeypatch.setattr(vs, "_model", None) + monkeypatch.setattr(rr, "_model", None) + monkeypatch.setattr(vs, "get_model", forbidden_model_load) + monkeypatch.setattr(vs, "_load_frozen_embedding_target", forbidden_model_load) + monkeypatch.setattr(rr, "get_reranker", forbidden_model_load) + monkeypatch.setattr(server, "_set_reranker", forbidden_model_load) # ----------------------------------------------------------------------- @@ -86,11 +94,14 @@ def test_load_config_returns_cached_copy(self, server, monkeypatch): # Second call within staleness window, file NOT modified on disk # (mtime unchanged) — should use cache, no disk read read_count = 0 - original_read_text = type(server._CONFIG_PATH).read_text + config_path = server._CONFIG_PATH + invoking_thread = threading.get_ident() + original_read_text = type(config_path).read_text - def counting_read(self_path, *args, **kwargs): + def counting_read(self_path: Path, *args: object, **kwargs: object) -> str: nonlocal read_count - read_count += 1 + if self_path == config_path and threading.get_ident() == invoking_thread: + read_count += 1 return original_read_text(self_path, *args, **kwargs) monkeypatch.setattr(type(server._CONFIG_PATH), "read_text", counting_read) @@ -99,6 +110,28 @@ def counting_read(self_path, *args, **kwargs): assert result1 == result2, ( "_load_config() did not return cached result within staleness window" ) + + # Prove that unrelated reads cannot contaminate this caller's count. + # Run controls after the second call so they cannot consume its TTL. + unrelated_path = config_path.with_name("unrelated.json") + unrelated_path.write_text(json.dumps(cfg), encoding="utf-8") + assert json.loads(unrelated_path.read_text(encoding="utf-8")) == cfg + background_results: list[str] = [] + background_errors: list[Exception] = [] + + def background_read() -> None: + try: + background_results.append(config_path.read_text(encoding="utf-8")) + except Exception as error: + background_errors.append(error) + + reader = threading.Thread(target=background_read) + reader.start() + reader.join(timeout=5) + assert not reader.is_alive() + assert not background_errors + assert len(background_results) == 1 + assert json.loads(background_results[0]) == cfg assert read_count == 0, ( f"_load_config() re-read disk {read_count} times within staleness window" ) diff --git a/tests/test_issue_511_write_path_opt.py b/tests/test_issue_511_write_path_opt.py index c2f1cbe4..0d1858a3 100644 --- a/tests/test_issue_511_write_path_opt.py +++ b/tests/test_issue_511_write_path_opt.py @@ -5,7 +5,9 @@ M16: Fallback embed_single inside write lock — removed in favor of skip-and-log """ +import ast import inspect +import textwrap import unittest @@ -30,13 +32,17 @@ class TestM15StyleVecOutsideLock(unittest.TestCase): def test_add_precomputes_style_vec(self): from truememory.engine import TrueMemoryEngine - source = inspect.getsource(TrueMemoryEngine.add) - lock_pos = source.find("self._write_lock") - precompute_pos = source.find("compute_style_vector") - self.assertGreater(precompute_pos, -1, - "add() should reference compute_style_vector for pre-computation") - self.assertLess(precompute_pos, lock_pos, - "compute_style_vector call should appear before _write_lock") + tree = ast.parse(textwrap.dedent(inspect.getsource(TrueMemoryEngine.add))) + calls = [node for node in ast.walk(tree) if isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) and node.func.id == "compute_style_vector"] + self.assertEqual(len(calls), 1) + for block in (node for node in ast.walk(tree) if isinstance(node, ast.With)): + self.assertNotIn(calls[0], list(ast.walk(block)), + "Style computation must run outside lock and transaction contexts") + publications = [node for node in ast.walk(tree) if isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) and node.func.id == "_update_style_vec"] + self.assertEqual(len(publications), 1) + self.assertLess(calls[0].lineno, publications[0].lineno) def test_style_vec_accepts_precomputed(self): from truememory.personality_style_vec import update_entity_style_vector_incremental diff --git a/tests/test_issue_514_model_server.py b/tests/test_issue_514_model_server.py index d34016b7..68baf2fe 100644 --- a/tests/test_issue_514_model_server.py +++ b/tests/test_issue_514_model_server.py @@ -7,7 +7,11 @@ """ import inspect +import os +from pathlib import Path +import tempfile import unittest +from unittest.mock import patch class TestM17DtypeWhitelist(unittest.TestCase): @@ -62,11 +66,44 @@ def test_pid_check_handles_permission_error(self): self.assertIn("PermissionError", source, "pid_is_alive() should catch PermissionError") - def test_main_uses_pid_is_alive(self): - from truememory.model_server import main - source = inspect.getsource(main) - self.assertIn("pid_is_alive", source, - "main() should use pid_is_alive for cross-platform PID check") + def test_main_uses_bind_owner_despite_reused_live_pid(self) -> None: + from truememory import model_server as module + acquired = [] + test_case = self + + class FakeServer: + def __init__(self, *, automatic_rss_budget: bool = False) -> None: + test_case.assertTrue(automatic_rss_budget) + + def run(self) -> None: + fd = module.try_file_lock(module.LOCK_PATH) + if fd is None: + raise RuntimeError("another model server holds the bind lock") + acquired.append(True) + os.close(fd) + + def _cleanup(self) -> None: + pass + + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + pid_path = root / "model_server.pid" + pid_path.write_text(str(os.getpid())) + with patch.object(module, "PID_PATH", pid_path), \ + patch.object(module, "LOCK_PATH", root / "model_server.lock"), \ + patch.object(module, "ModelServer", FakeServer), \ + patch.object(module.signal, "signal"), \ + patch.object(module.atexit, "register"), \ + patch.dict("sys.modules", {"setproctitle": None}): + module.main() + self.assertEqual(acquired, [True]) + owner = module.try_file_lock(module.LOCK_PATH) + try: + with self.assertRaises(SystemExit): + module.main() + self.assertEqual(acquired, [True]) + finally: + os.close(owner) class TestM20RebuildLock(unittest.TestCase): diff --git a/tests/test_issue_577_model_server_oom.py b/tests/test_issue_577_model_server_oom.py index 83f1859f..a441dc65 100644 --- a/tests/test_issue_577_model_server_oom.py +++ b/tests/test_issue_577_model_server_oom.py @@ -22,10 +22,13 @@ """ from __future__ import annotations +import os import socket +import sys import threading import types -from unittest.mock import MagicMock +import unittest +from unittest.mock import MagicMock, patch import numpy as np import pytest @@ -150,7 +153,9 @@ def test_issue_577_device_env_override_cpu_server_embed(self, monkeypatch): def test_issue_577_device_env_override_cpu_local_reranker(self, monkeypatch): """reranker.get_reranker local fallback honors TRUEMEMORY_DEVICE=cpu.""" import sys - from truememory import reranker + from truememory import mps_utils, reranker + + monkeypatch.setattr(mps_utils, "ensure_mps_memory_budget", lambda device: None) captured: dict = {} monkeypatch.setitem( @@ -327,7 +332,7 @@ def predict(self, pairs, **kwargs): raise _make_mps_oom_error() return np.full(len(pairs), 0.5, dtype=np.float32) - def fake_get_reranker(self, model_name=None): + def fake_get_reranker(self, model_name=None, deadline=None): # Before sticky degradation the loaded model is on MPS and OOMs; # after, the reload is CPU-resident and succeeds. return FakeReranker(fail="rerank" not in self._sticky_cpu) @@ -366,7 +371,7 @@ def test_issue_577_rerank_non_oom_error_propagates(self, no_flush, monkeypatch): broken = MagicMock() broken.predict = MagicMock(side_effect=RuntimeError("tokenizer exploded")) monkeypatch.setattr( - ModelServer, "_get_reranker", lambda self, model_name=None: broken + ModelServer, "_get_reranker", lambda self, model_name=None, deadline=None: broken ) server = ModelServer() with pytest.raises(RuntimeError, match="tokenizer"): @@ -403,7 +408,7 @@ def encode(self, texts, **kwargs): return np.zeros((len(texts), 4), dtype=np.float32) monkeypatch.setattr( - ModelServer, "_get_fast_encoder", lambda self, tier: FakeFastEncoder() + ModelServer, "_get_fast_encoder", lambda self, tier, deadline=None: FakeFastEncoder() ) # Simulate an in-flight ingestion batch occupying the heavy path. @@ -562,11 +567,10 @@ def fake_build(model_id, device): monkeypatch.setattr(vs, "EMBEDDING_MODEL", "edge") server = ModelServer() - # Arm the snapshot via a main-path load (the historical mutating - # sync happens exactly here, under the lock). + # Arm the snapshot without changing the daemon's default selection. resp = server.handle_request({"op": "embed", "texts": ["a", "b"], "tier": "base"}) assert resp["ok"] is True - assert calls == ["base"] + assert calls == [] calls.clear() # Contended fast lane: serves from the snapshot, zero mutation. @@ -803,6 +807,190 @@ def test_issue_577_hooks_pass_short_deadline(self, monkeypatch): ) +class TestIssue739LocalEmbeddingDevice(unittest.TestCase): + """Exercise real local routing and device resolution with synthetic factories.""" + + def setUp(self) -> None: + from truememory import model_client, mps_utils, tier_config, vector_search + + self.vector_search = vector_search + self.mps_utils = mps_utils + self.transformer_calls: list[tuple[str, dict]] = [] + self.static_calls: list[tuple[str, dict]] = [] + self.availability_calls: list[str] = [] + self.available = {"cuda": True, "mps": True} + self.local_model = object() + self.proxy = object() + + def transformer(name: str, **kwargs: object) -> object: + self.transformer_calls.append((name, kwargs)) + return self.local_model + + def static_model(name: str, **kwargs: object) -> object: + self.static_calls.append((name, kwargs)) + return self.local_model + + def available(kind: str) -> bool: + self.availability_calls.append(kind) + return self.available[kind] + + transformers = types.ModuleType("sentence_transformers") + transformers.SentenceTransformer = transformer + model2vec = types.ModuleType("model2vec") + model2vec.StaticModel = types.SimpleNamespace(from_pretrained=static_model) + torch = types.ModuleType("torch") + torch.cuda = types.SimpleNamespace(is_available=lambda: available("cuda")) + torch.backends = types.SimpleNamespace( + mps=types.SimpleNamespace(is_available=lambda: available("mps")), + ) + self.ready = MagicMock(return_value=True) + self.alive = MagicMock(return_value=True) + self.get_proxy = MagicMock(return_value=self.proxy) + for patcher in ( + patch.dict("sys.modules", { + "sentence_transformers": transformers, "model2vec": model2vec, + "torch": torch, + }), + patch.dict(os.environ, { + "TRUEMEMORY_DEVICE": "cpu", "TRUEMEMORY_NO_MODEL_SERVER": "1", + "TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD": "1", + }), + patch.object(vector_search, "_model", None), + patch.object(vector_search, "EMBEDDING_MODEL", "qwen3_256"), + patch.object(vector_search, "_embedding_dim", 256), + patch.object(model_client, "_server_ready", self.ready), + patch.object(model_client, "_server_is_alive", self.alive), + patch.object(model_client, "get_embedding_proxy", self.get_proxy), + patch.object(tier_config, "resolve_custom_tier", return_value={"embed_dim": 192}), + patch.object(mps_utils, "ensure_mps_memory_budget"), + ): + patcher.start() + self.addCleanup(patcher.stop) + + def load(self, model_id: str, override: str | None) -> object: + self.vector_search._model = None + self.vector_search.EMBEDDING_MODEL = model_id + self.vector_search._embedding_dim = -1 + self.transformer_calls.clear() + self.static_calls.clear() + self.availability_calls.clear() + if override is None: + os.environ.pop("TRUEMEMORY_DEVICE", None) + else: + os.environ["TRUEMEMORY_DEVICE"] = override + return self.vector_search.get_model() + + def test_issue_739_all_transformer_factories_preserve_identity_and_device_policy(self) -> None: + factories = ( + ("minilm", "all-MiniLM-L6-v2", 384, {}), + ("bge-small", "BAAI/bge-small-en-v1.5", 384, {}), + ("qwen3_256", "Qwen/Qwen3-Embedding-0.6B", 256, { + "truncate_dim": 256, + "model_kwargs": {"attn_implementation": "eager"} if sys.platform == "darwin" else None, + }), + ("synthetic/custom-encoder", "synthetic/custom-encoder", 192, { + "truncate_dim": 192, "trust_remote_code": False, + }), + ) + policies = ( + ("cpu", True, True, "cpu"), (" CPU ", True, True, "cpu"), + ("cuda", True, True, "cuda:0"), ("mps", True, True, "mps"), + ("auto", True, True, None), (None, True, True, None), + ("invalid-device", True, True, None), ("cuda:99", True, True, None), + ("cuda", False, True, None), ("mps", True, False, None), + ) + for model_id, expected_name, dimension, expected_kwargs in factories: + for override, cuda, mps, expected_device in policies: + with self.subTest(model=model_id, override=override, cuda=cuda, mps=mps): + self.available.update(cuda=cuda, mps=mps) + self.assertIs(self.load(model_id, override), self.local_model) + self.assertEqual(len(self.transformer_calls), 1) + name, kwargs = self.transformer_calls[0] + self.assertEqual(name, expected_name) + self.assertEqual(kwargs.get("device"), expected_device) + self.assertEqual({key: value for key, value in kwargs.items() if key != "device"}, + expected_kwargs) + self.assertEqual(self.vector_search._embedding_dim, dimension) + self.assertEqual(self.static_calls, []) + if expected_device == "cpu" or override in ("auto", None): + self.assertEqual(self.availability_calls, []) + + def test_issue_739_unsupported_device_warnings_keep_framework_auto(self) -> None: + for override in ("invalid-device", "mps", "cuda"): + with self.subTest(override=override): + self.available.update(cuda=False, mps=False) + with self.assertLogs("truememory.mps_utils", level="WARNING") as logs: + self.load("qwen3_256", override) + self.assertIn("auto device selection", logs.output[0]) + self.assertIsNone(self.transformer_calls[0][1].get("device")) + + def test_issue_739_qwen_platform_options_are_preserved(self) -> None: + for platform, model_kwargs in (("darwin", {"attn_implementation": "eager"}), + ("linux", None), ("win32", None)): + with self.subTest(platform=platform), patch.object(sys, "platform", platform): + self.load("qwen3_256", "cpu") + self.assertEqual(self.transformer_calls[0][1], { + "truncate_dim": 256, "model_kwargs": model_kwargs, "device": "cpu", + }) + + def test_issue_739_model2vec_and_custom_opt_in_fallback_remain_cpu_only(self) -> None: + os.environ["TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD"] = "0" + for model_id in ("model2vec", "synthetic/custom-encoder"): + for override in ("cpu", "mps", "cuda", "auto"): + with self.subTest(model=model_id, override=override): + self.assertIs(self.load(model_id, override), self.local_model) + self.assertEqual(self.static_calls, [ + ("minishlab/potion-base-8M", {"force_download": False}), + ]) + self.assertEqual(self.transformer_calls, []) + self.assertEqual(self.availability_calls, []) + self.assertEqual(self.vector_search._embedding_dim, 256) + + def test_issue_739_explicitly_disabled_server_never_probes_endpoint(self) -> None: + self.load("qwen3_256", "cpu") + self.ready.assert_not_called() + self.alive.assert_not_called() + self.get_proxy.assert_not_called() + self.assertEqual(self.transformer_calls[0][1].get("device"), "cpu") + + def test_issue_739_unavailable_server_keeps_shared_policy(self) -> None: + os.environ.pop("TRUEMEMORY_NO_MODEL_SERVER") + for ready, alive in ((False, True), (True, False)): + with self.subTest(ready=ready, alive=alive): + self.ready.return_value = ready + self.alive.return_value = alive + self.get_proxy.reset_mock() + self.assertIs(self.load("qwen3_256", "cpu"), self.proxy) + self.get_proxy.assert_called_once_with(tier="qwen3_256") + self.assertEqual(self.transformer_calls, []) + self.assertEqual(self.static_calls, []) + + def test_issue_739_available_server_preserves_proxy_path_without_local_device_resolution(self) -> None: + os.environ.pop("TRUEMEMORY_NO_MODEL_SERVER") + with patch.object(self.mps_utils, "resolve_device", side_effect=AssertionError("local resolution")): + self.assertIs(self.load("qwen3_256", "cpu"), self.proxy) + self.get_proxy.assert_called_once_with(tier="qwen3_256") + self.assertEqual(self.transformer_calls, []) + self.assertEqual(self.static_calls, []) + + def test_issue_739_proxy_failure_does_not_construct_local_model(self) -> None: + os.environ.pop("TRUEMEMORY_NO_MODEL_SERVER") + self.get_proxy.side_effect = RuntimeError("synthetic unavailable proxy") + with self.assertRaisesRegex(RuntimeError, "synthetic unavailable proxy"): + self.load("qwen3_256", "cpu") + self.get_proxy.assert_called_once_with(tier="qwen3_256") + self.assertEqual(self.transformer_calls, []) + self.assertEqual(self.static_calls, []) + + def test_issue_739_cached_model_keeps_existing_lifetime_semantics(self) -> None: + self.load("qwen3_256", "cpu") + os.environ["TRUEMEMORY_DEVICE"] = "mps" + self.assertIs(self.vector_search.get_model(), self.local_model) + self.assertEqual(len(self.transformer_calls), 1) + self.assertEqual(self.transformer_calls[0][1].get("device"), "cpu") + self.assertEqual(self.availability_calls, []) + + if __name__ == "__main__": import sys sys.exit(pytest.main([__file__, "-v"])) diff --git a/tests/test_issue_589_directives_completion.py b/tests/test_issue_589_directives_completion.py index ff370c48..4c998b98 100644 --- a/tests/test_issue_589_directives_completion.py +++ b/tests/test_issue_589_directives_completion.py @@ -25,10 +25,29 @@ import logging import sqlite3 +from contextlib import ExitStack +from typing import TYPE_CHECKING from unittest.mock import MagicMock, patch import pytest +if TYPE_CHECKING: + from truememory import Memory + + +def _close_and_drain_memory(memory: Memory) -> None: + coordinator = memory._engine._maintenance_coordinator + try: + try: + if coordinator is not None: + coordinator.cancel() + finally: + memory.close() + finally: + if coordinator is not None: + assert coordinator.wait(10), "Test-owned maintenance did not finish" + + # --------------------------------------------------------------------------- # v0.7.5.0 legacy schema (pre-directive) — extracted from the tagged release # --------------------------------------------------------------------------- @@ -253,13 +272,14 @@ def test_issue_589_directive_visible_regardless_of_user_scope(tmp_path): all_got = _load_directives(m) assert len(all_got) == 3 finally: - m.close() + _close_and_drain_memory(m) def test_issue_589_directive_injection_cap(tmp_path, caplog): """More directives than the cap -> only DIRECTIVE_LIMIT are injected, a warning is logged, and the block carries an overflow note pointing at truememory_directives.""" + from truememory import Memory from truememory.storage import create_db, insert_message db = tmp_path / "cap.db" @@ -273,8 +293,14 @@ def test_issue_589_directive_injection_cap(tmp_path, caplog): assert DIRECTIVE_LIMIT == 50 - with caplog.at_level(logging.WARNING): - ctx = recall_memories({}, db_path=str(db)) + with patch("truememory.Memory") as factory, ExitStack() as cleanup: + def tracked_memory(*args: object, **kwargs: object) -> Memory: + memory = Memory(*args, **kwargs) + cleanup.callback(_close_and_drain_memory, memory) + return memory + factory.side_effect = tracked_memory + with caplog.at_level(logging.WARNING): + ctx = recall_memories({}, db_path=str(db)) assert "" in ctx block = ctx.split("")[1].split("")[0] @@ -312,7 +338,7 @@ def _boom(*a, **kw): f"directive load failure must be logged, got: {warnings}" ) finally: - m.close() + _close_and_drain_memory(m) # --------------------------------------------------------------------------- @@ -493,7 +519,7 @@ def test_issue_589_directive_full_lifecycle(tmp_path): stats = m._engine.get_stats() assert stats.get("directive_count") == 0 finally: - m.close() + _close_and_drain_memory(m) # --------------------------------------------------------------------------- diff --git a/tests/test_issue_590_build_vectors_batch.py b/tests/test_issue_590_build_vectors_batch.py index a678b1c8..d3821993 100644 --- a/tests/test_issue_590_build_vectors_batch.py +++ b/tests/test_issue_590_build_vectors_batch.py @@ -73,6 +73,9 @@ def _make_server(monkeypatch, tmp_path): def _seed_messages(ms, n: int) -> None: """Add *n* messages to the database via the server module.""" m = ms._get_memory() + # These focused tests own explicit rebuild maintenance. + m._engine._has_consolidation = False + m._engine._has_style_vec = False for i in range(n): m.add(f"memory number {i}", user_id="test") diff --git a/tests/test_issue_598_windows_socket.py b/tests/test_issue_598_windows_socket.py index 3ac572ba..a7ec1ed3 100644 --- a/tests/test_issue_598_windows_socket.py +++ b/tests/test_issue_598_windows_socket.py @@ -390,7 +390,9 @@ def test_cleanup_removes_all_transport_files(self, tmp_path): with patch("truememory.model_server.SOCK_PATH", sock_path), \ patch("truememory.model_server.PID_PATH", pid_path), \ patch("truememory.model_server.PORT_PATH", port_path), \ - patch("truememory.model_server.TOKEN_PATH", token_path): + patch("truememory.model_server.TOKEN_PATH", token_path), \ + patch("truememory.model_server.LOCK_PATH", tmp_path / "model_server.lock"): + srv._lock_fd = srv._acquire_bind_lock() srv._cleanup() for p in (sock_path, pid_path, port_path, token_path): diff --git a/tests/test_issue_600_antigravity_groq_deepsearch.py b/tests/test_issue_600_antigravity_groq_deepsearch.py index 62881d8c..b8688c93 100644 --- a/tests/test_issue_600_antigravity_groq_deepsearch.py +++ b/tests/test_issue_600_antigravity_groq_deepsearch.py @@ -201,7 +201,9 @@ def test_groq_configure_accepted(tmp_path, monkeypatch): config_path = tmp_path / "config.json" config_path.write_text("{}") monkeypatch.setattr(mcp_server, "_CONFIG_PATH", config_path) + monkeypatch.setattr(mcp_server, "_CONFIG_LOCK_PATH", mcp_server._CONFIG_PATH.with_name("config.json.lock")) monkeypatch.setattr(mcp_server, "_TRUEMEMORY_DIR", tmp_path) + monkeypatch.setattr(mcp_server, "_DB_PATH", str(tmp_path / "synthetic-configure.db")) # Invalidate config cache monkeypatch.setattr(mcp_server, "_config_cache", None) monkeypatch.setattr(mcp_server, "_config_cache_time", 0.0) @@ -224,6 +226,7 @@ def test_deepsearch_falls_back_when_not_configured(tmp_path, monkeypatch): config_path = tmp_path / "config.json" config_path.write_text(json.dumps({"tier": "pro"})) monkeypatch.setattr(mcp_server, "_CONFIG_PATH", config_path) + monkeypatch.setattr(mcp_server, "_CONFIG_LOCK_PATH", mcp_server._CONFIG_PATH.with_name("config.json.lock")) monkeypatch.setattr(mcp_server, "_config_cache", None) monkeypatch.setattr(mcp_server, "_config_cache_time", 0.0) @@ -243,6 +246,7 @@ def test_deepsearch_uses_custom_provider_when_configured(tmp_path, monkeypatch): "groq_api_key": "gsk_test_deepsearch", })) monkeypatch.setattr(mcp_server, "_CONFIG_PATH", config_path) + monkeypatch.setattr(mcp_server, "_CONFIG_LOCK_PATH", mcp_server._CONFIG_PATH.with_name("config.json.lock")) monkeypatch.setattr(mcp_server, "_config_cache", None) monkeypatch.setattr(mcp_server, "_config_cache_time", 0.0) @@ -261,6 +265,7 @@ def test_deepsearch_returns_none_for_unknown_provider(tmp_path, monkeypatch): "deepsearch_provider": "nonexistent_provider", })) monkeypatch.setattr(mcp_server, "_CONFIG_PATH", config_path) + monkeypatch.setattr(mcp_server, "_CONFIG_LOCK_PATH", mcp_server._CONFIG_PATH.with_name("config.json.lock")) monkeypatch.setattr(mcp_server, "_config_cache", None) monkeypatch.setattr(mcp_server, "_config_cache_time", 0.0) @@ -279,6 +284,7 @@ def test_deepsearch_returns_none_when_no_api_key(tmp_path, monkeypatch): # No groq_api_key set })) monkeypatch.setattr(mcp_server, "_CONFIG_PATH", config_path) + monkeypatch.setattr(mcp_server, "_CONFIG_LOCK_PATH", mcp_server._CONFIG_PATH.with_name("config.json.lock")) monkeypatch.setattr(mcp_server, "_config_cache", None) monkeypatch.setattr(mcp_server, "_config_cache_time", 0.0) monkeypatch.delenv("GROQ_API_KEY", raising=False) @@ -298,6 +304,7 @@ def test_deepsearch_resolve_uses_override(tmp_path, monkeypatch): "openai_api_key": "sk-test-deepsearch", })) monkeypatch.setattr(mcp_server, "_CONFIG_PATH", config_path) + monkeypatch.setattr(mcp_server, "_CONFIG_LOCK_PATH", mcp_server._CONFIG_PATH.with_name("config.json.lock")) monkeypatch.setattr(mcp_server, "_config_cache", None) monkeypatch.setattr(mcp_server, "_config_cache_time", 0.0) # Reset deepsearch cache @@ -315,6 +322,7 @@ def test_deepsearch_resolve_falls_back_to_default_on_pro(tmp_path, monkeypatch): config_path = tmp_path / "config.json" config_path.write_text(json.dumps({"tier": "pro"})) monkeypatch.setattr(mcp_server, "_CONFIG_PATH", config_path) + monkeypatch.setattr(mcp_server, "_CONFIG_LOCK_PATH", mcp_server._CONFIG_PATH.with_name("config.json.lock")) monkeypatch.setattr(mcp_server, "_config_cache", None) monkeypatch.setattr(mcp_server, "_config_cache_time", 0.0) # Reset caches @@ -334,6 +342,7 @@ def test_deepsearch_resolve_returns_none_on_edge_no_override(tmp_path, monkeypat config_path = tmp_path / "config.json" config_path.write_text(json.dumps({"tier": "edge"})) monkeypatch.setattr(mcp_server, "_CONFIG_PATH", config_path) + monkeypatch.setattr(mcp_server, "_CONFIG_LOCK_PATH", mcp_server._CONFIG_PATH.with_name("config.json.lock")) monkeypatch.setattr(mcp_server, "_config_cache", None) monkeypatch.setattr(mcp_server, "_config_cache_time", 0.0) # Reset caches diff --git a/tests/test_issue_630_search_supplement_merge.py b/tests/test_issue_630_search_supplement_merge.py index a6938df4..e5862f8b 100644 --- a/tests/test_issue_630_search_supplement_merge.py +++ b/tests/test_issue_630_search_supplement_merge.py @@ -39,6 +39,7 @@ def fts_engine(tmp_path, monkeypatch): monkeypatch.delenv("TRUEMEMORY_ALPHA_SURPRISE", raising=False) from truememory.engine import TrueMemoryEngine + from truememory.maintenance import canonical_database_path from truememory.storage import create_db db_path = tmp_path / "i630.db" @@ -54,11 +55,27 @@ def fts_engine(tmp_path, monkeypatch): conn.close() eng = TrueMemoryEngine(db_path) - eng.open(rebuild_vectors=False) - # Force the bare path: no cross-encoder rerank, no salience filtering. - eng._has_reranker = False - eng._has_salience = False - return eng + coordinator = None + try: + eng.open(rebuild_vectors=False) + coordinator = eng._maintenance_coordinator + # Force the bare path: no cross-encoder rerank, no salience filtering. + eng._has_reranker = False + eng._has_salience = False + yield eng + finally: + try: + if coordinator is None: + coordinator = eng._maintenance_coordinator + if coordinator is not None: + fixture_path = canonical_database_path(db_path) + if fixture_path is None or canonical_database_path(coordinator.path) != fixture_path: + raise RuntimeError("Fixture maintenance coordinator belongs to a different database") + coordinator.cancel() + if not coordinator.wait(10): + raise RuntimeError("Fixture-owned maintenance worker did not finish") + finally: + eng.close() # Query that _has_personality_intent() recognizes (matches "favorite food"). diff --git a/tests/test_issue_644_ingest_races.py b/tests/test_issue_644_ingest_races.py index e92fe190..6366f86e 100644 --- a/tests/test_issue_644_ingest_races.py +++ b/tests/test_issue_644_ingest_races.py @@ -174,6 +174,7 @@ def test_queue_to_backlog_refuses_to_clobber_live_claim(monkeypatch, tmp_path): claim with a fresh ``.json`` (POSIX rename-overwrite race / M-15). """ from truememory.ingest.hooks import stop as stop_mod + monkeypatch.setenv("TRUEMEMORY_TRANSCRIPT_DIR", str(tmp_path)) backlog = tmp_path / "backlog" backlog.mkdir(parents=True) diff --git a/tests/test_issue_646_model_server.py b/tests/test_issue_646_model_server.py index 52cc2a86..18d73a60 100644 --- a/tests/test_issue_646_model_server.py +++ b/tests/test_issue_646_model_server.py @@ -59,6 +59,7 @@ def test_cleanup_does_not_delete_other_servers_artifacts( ms.PORT_PATH.write_text("12345") server = ms.ModelServer() + server._lock_fd = server._acquire_bind_lock() server._cleanup() # All four artifacts survive — they belong to the successor. @@ -77,6 +78,7 @@ def test_cleanup_removes_own_artifacts(self, monkeypatch, tmp_path): ms.PORT_PATH.write_text("12345") server = ms.ModelServer() + server._lock_fd = server._acquire_bind_lock() server._cleanup() assert not ms.PID_PATH.exists() @@ -170,11 +172,11 @@ def test_embed_with_active_throttler_does_not_crash(self, monkeypatch): fake_model = MagicMock() fake_model.encode.return_value = np.zeros((2, 4), dtype=np.float32) - monkeypatch.setattr(server, "_get_embed_model", lambda tier: fake_model) + monkeypatch.setattr(server, "_get_embed_model", lambda tier, deadline=None: fake_model) class FakeThrottler: def before_batch(self): - pass + return 2, {} def after_batch(self, n, t): pass @@ -204,7 +206,7 @@ def test_expired_deadline_rejected_before_encode(self, monkeypatch): encode_called = {"n": 0} - def boom(tier): + def boom(tier, deadline=None): encode_called["n"] += 1 raise AssertionError("encode must not run for expired request") @@ -227,7 +229,7 @@ def test_future_deadline_runs_normally(self, monkeypatch): server = ModelServer() fake_model = MagicMock() fake_model.encode.return_value = np.zeros((1, 4), dtype=np.float32) - monkeypatch.setattr(server, "_get_embed_model", lambda tier: fake_model) + monkeypatch.setattr(server, "_get_embed_model", lambda tier, deadline=None: fake_model) resp = server.handle_request({ "op": "embed", diff --git a/tests/test_issue_647_vector_rebuild.py b/tests/test_issue_647_vector_rebuild.py index 3a168421..bdc1db80 100644 --- a/tests/test_issue_647_vector_rebuild.py +++ b/tests/test_issue_647_vector_rebuild.py @@ -81,6 +81,9 @@ def _make_server(monkeypatch, tmp_path): def _seed_messages(ms, n: int) -> None: m = ms._get_memory() + # These focused tests own explicit rebuild maintenance. + m._engine._has_consolidation = False + m._engine._has_style_vec = False for i in range(n): m.add(f"memory number {i}", user_id="test") diff --git a/tests/test_issue_648_vec_compat.py b/tests/test_issue_648_vec_compat.py index ee09b694..f7df0748 100644 --- a/tests/test_issue_648_vec_compat.py +++ b/tests/test_issue_648_vec_compat.py @@ -224,6 +224,8 @@ def __getattr__(self, name): ) return getattr(self._real, name) + from truememory.storage import create_db + create_db(db).close() real = sqlite3.connect(str(db)) with patch("sqlite3.connect", return_value=_NoExtConn(real)): with pytest.raises(TierSwitchUnsupportedError) as exc: @@ -236,13 +238,22 @@ def __getattr__(self, name): class TestM51StartRebuildNotBricked: """A pre-thread failure in start_rebuild must not brick later rebuilds.""" - def test_pre_thread_failure_does_not_brick(self): + def test_pre_thread_failure_does_not_brick(self, tmp_path, monkeypatch): from truememory.tier_switch.manager import ( RebuildManager, TierSwitchUnsupportedError, ) mgr = RebuildManager() + from truememory.storage import create_db + from truememory.tier_switch import manager as manager_module + db_path = tmp_path / "synthetic-pre-thread.db" + conn = create_db(db_path) + conn.execute("INSERT INTO messages(content, timestamp) VALUES ('synthetic source', '2026-01-01')") + conn.commit() + conn.close() + monkeypatch.setattr(manager_module, "_TRUEMEMORY_DIR", tmp_path) + monkeypatch.setattr(manager_module, "_get_db_path", lambda: db_path) # First call: _open_db fails before the worker thread starts (M-23). with patch( diff --git a/tests/test_l0_style_vector.py b/tests/test_l0_style_vector.py index 72aae705..540f9da6 100644 --- a/tests/test_l0_style_vector.py +++ b/tests/test_l0_style_vector.py @@ -182,7 +182,7 @@ def test_build_for_multiple_entities(self): class TestIncrementalUpdate: def test_incremental_converges(self): - """Add messages one at a time, verify vector converges toward batch-built vector.""" + """Incremental accumulation must equal the batch mean within float64 tolerance.""" conn = _make_test_db() messages = [ "I love hiking in the mountains", @@ -205,10 +205,9 @@ def test_incremental_converges(self): inc_vec = get_entity_style_vector(conn2, "alice") assert inc_vec is not None - # Cosine should be reasonably high (incremental weighted average - # differs slightly from batch mean-pool due to normalization order) - sim = cosine_similarity(batch_vec, inc_vec) - assert sim > 0.8, f"Expected cosine > 0.8, got {sim}" + assert len(inc_vec) == len(batch_vec) == DIM + assert all(math.isclose(expected, actual, abs_tol=1e-12, rel_tol=1e-12) + for expected, actual in zip(batch_vec, inc_vec)) conn.close() conn2.close() diff --git a/tests/test_l4_entity_sheets_disabled.py b/tests/test_l4_entity_sheets_disabled.py index 425269da..ac500e7a 100644 --- a/tests/test_l4_entity_sheets_disabled.py +++ b/tests/test_l4_entity_sheets_disabled.py @@ -283,7 +283,10 @@ def test_migration_idempotent_across_opens(tmp_path, monkeypatch, caplog): # First open: should purge. with caplog.at_level(logging.INFO): - eng1 = TrueMemoryEngine(db_path).open(rebuild_vectors=False) + eng1 = TrueMemoryEngine(db_path) + eng1._has_consolidation = False + eng1._has_style_vec = False + eng1.open(rebuild_vectors=False) eng1.close() first_text = caplog.text assert "purged 4" in first_text @@ -302,7 +305,10 @@ def test_migration_idempotent_across_opens(tmp_path, monkeypatch, caplog): # Second open: must NOT re-emit the purge log. caplog.clear() with caplog.at_level(logging.INFO): - eng2 = TrueMemoryEngine(db_path).open(rebuild_vectors=False) + eng2 = TrueMemoryEngine(db_path) + eng2._has_consolidation = False + eng2._has_style_vec = False + eng2.open(rebuild_vectors=False) eng2.close() assert "MEMORIST-L4 migration: purged" not in caplog.text, ( f"Idempotent open should skip the migration log; got: {caplog.text!r}" diff --git a/tests/test_l5_surprise_rerank.py b/tests/test_l5_surprise_rerank.py index 10f72552..5635ab1a 100644 --- a/tests/test_l5_surprise_rerank.py +++ b/tests/test_l5_surprise_rerank.py @@ -60,8 +60,20 @@ def engine_with_surprise(tmp_path, monkeypatch): conn.close() eng = TrueMemoryEngine(db_path) - eng.open(rebuild_vectors=False) - return eng + # These tests own their surprise rows and do not exercise maintenance. + eng._has_consolidation = False + eng._has_style_vec = False + try: + eng.open(rebuild_vectors=False) + yield eng + finally: + coordinator = eng._maintenance_coordinator + try: + if coordinator is not None: + coordinator.cancel() + assert coordinator.wait(10), "Fixture maintenance worker did not stop" + finally: + eng.close() def _fake_results(ids_and_scores, source=None): @@ -316,7 +328,7 @@ def spy(results): ) -def test_alpha_zero_byte_identical_through_pipeline(tmp_path, monkeypatch): +def test_alpha_zero_byte_identical_through_pipeline(tmp_path, monkeypatch, request): """Both explicit α=0 (constructor) and α=0 (env var) produce identical no-op results through _apply_surprise_boost.""" from truememory.engine import TrueMemoryEngine @@ -330,15 +342,23 @@ def test_alpha_zero_byte_identical_through_pipeline(tmp_path, monkeypatch): monkeypatch.delenv("TRUEMEMORY_ALPHA_SURPRISE", raising=False) eng = TrueMemoryEngine(db_path, alpha_surprise=0.0) + request.addfinalizer(eng.close) + eng._has_consolidation = False + eng._has_style_vec = False eng.open(rebuild_vectors=False) r_ctor = eng._apply_surprise_boost([dict(r) for r in sample]) assert [(r["id"], r["score"]) for r in r_ctor] == expected monkeypatch.setenv("TRUEMEMORY_ALPHA_SURPRISE", "0") eng2 = TrueMemoryEngine(db_path) + request.addfinalizer(eng2.close) + eng2._has_consolidation = False + eng2._has_style_vec = False eng2.open(rebuild_vectors=False) r_env = eng2._apply_surprise_boost([dict(r) for r in sample]) assert [(r["id"], r["score"]) for r in r_env] == expected + eng2.close() + eng.close() def test_composite_source_refined_is_excluded(engine_with_surprise): diff --git a/tests/test_mcp_safety.py b/tests/test_mcp_safety.py index 46d57b96..16b040bf 100644 --- a/tests/test_mcp_safety.py +++ b/tests/test_mcp_safety.py @@ -152,6 +152,7 @@ def test_configure_restores_hf_offline_on_success(monkeypatch, tmp_path): monkeypatch.setenv("TRUEMEMORY_DB", str(db_path)) monkeypatch.setattr(ms, "_TRUEMEMORY_DIR", home / ".truememory") monkeypatch.setattr(ms, "_CONFIG_PATH", home / ".truememory" / "config.json") + monkeypatch.setattr(ms, "_CONFIG_LOCK_PATH", ms._CONFIG_PATH.with_name("config.json.lock")) monkeypatch.setattr(ms, "_DB_PATH", str(db_path)) monkeypatch.setattr(ms, "_memory", None) @@ -198,6 +199,7 @@ def test_configure_restores_hf_offline_on_set_embedding_model_raise( monkeypatch.setenv("TRUEMEMORY_DB", str(db_path)) monkeypatch.setattr(ms, "_TRUEMEMORY_DIR", home / ".truememory") monkeypatch.setattr(ms, "_CONFIG_PATH", home / ".truememory" / "config.json") + monkeypatch.setattr(ms, "_CONFIG_LOCK_PATH", ms._CONFIG_PATH.with_name("config.json.lock")) monkeypatch.setattr(ms, "_DB_PATH", str(db_path)) monkeypatch.setattr(ms, "_memory", None) @@ -214,20 +216,30 @@ def fake_import(name, *args, **kwargs): import truememory.vector_search as vs + attempted_tiers = [] + def _boom(tier): + attempted_tiers.append(tier) raise ValueError("simulated: model removed") + import truememory.reranker as rr + monkeypatch.setattr(vs, "_model", None) + monkeypatch.setattr(rr, "_model", None) monkeypatch.setattr(vs, "set_embedding_model", _boom) monkeypatch.delenv("HF_HUB_OFFLINE", raising=False) monkeypatch.delenv("TRANSFORMERS_OFFLINE", raising=False) - # truememory_configure wraps the tier switch; ValueError propagates - # out per Python semantics after finally runs. + # Failed lazy projection is returned as a truthful incomplete activation. import os as _os - import pytest - with pytest.raises(ValueError): - ms.truememory_configure(tier="pro") + result = json.loads(ms.truememory_configure(tier="pro")) + assert attempted_tiers == ["pro", "edge"] + assert result["status"] == "activation_pending" + assert result["rebuild_error"] == "ValueError: simulated: model removed" + assert result["served_tier"] is None + assert result["description"] == "Serving state awaits authoritative readback." + assert "warning" in result + assert not ms._CONFIG_PATH.exists() or json.loads(ms._CONFIG_PATH.read_text(encoding="utf-8")).get("tier", "edge") == "edge" # Critical F31 assertion: offline mode was RESTORED despite the raise assert _os.environ.get("HF_HUB_OFFLINE") == "1", ( diff --git a/tests/test_openclaw_adapter.py b/tests/test_openclaw_adapter.py index bf950d79..cd153b42 100644 --- a/tests/test_openclaw_adapter.py +++ b/tests/test_openclaw_adapter.py @@ -160,6 +160,7 @@ def test_install_hooks_creates_plugin(tmp_path, monkeypatch): from truememory.hooks.adapters import openclaw as oc_mod plugins_dir = tmp_path / "plugins" monkeypatch.setattr(oc_mod, "_PLUGINS_DIR", plugins_dir) + monkeypatch.setattr(oc_mod, "_CONFIG_PATH", tmp_path / "openclaw.json") from truememory.hooks.adapters.openclaw import OpenClawAdapter OpenClawAdapter().install_hooks(python_path="/usr/bin/python3") @@ -186,16 +187,19 @@ def test_install_hooks_uses_register_api(tmp_path, monkeypatch): from truememory.hooks.adapters import openclaw as oc_mod plugins_dir = tmp_path / "plugins" monkeypatch.setattr(oc_mod, "_PLUGINS_DIR", plugins_dir) + monkeypatch.setattr(oc_mod, "_CONFIG_PATH", tmp_path / "openclaw.json") from truememory.hooks.adapters.openclaw import OpenClawAdapter OpenClawAdapter().install_hooks(python_path="/usr/bin/python3") js_content = (plugins_dir / "truememory" / "index.js").read_text(encoding="utf-8") # Should use register(api) pattern assert "register(api)" in js_content - assert 'api.on("session_start"' in js_content - assert 'api.on("session_end"' in js_content - assert 'api.on("before_tool_call"' in js_content - assert 'api.on("before_compaction"' in js_content + assert 'api.on("before_prompt_build"' in js_content + assert "session_start.py" not in js_content + assert "stop.py" not in js_content + assert "compact.py" not in js_content + assert "spawnSync" not in js_content + assert "prependContext" in js_content # Should NOT use old fabricated activate(ctx) pattern assert "activate(ctx)" not in js_content assert "ctx.onSessionStart" not in js_content @@ -210,6 +214,7 @@ def test_install_hooks_package_json_esm(tmp_path, monkeypatch): from truememory.hooks.adapters import openclaw as oc_mod plugins_dir = tmp_path / "plugins" monkeypatch.setattr(oc_mod, "_PLUGINS_DIR", plugins_dir) + monkeypatch.setattr(oc_mod, "_CONFIG_PATH", tmp_path / "openclaw.json") from truememory.hooks.adapters.openclaw import OpenClawAdapter OpenClawAdapter().install_hooks(python_path="/usr/bin/python3") @@ -223,6 +228,7 @@ def test_install_hooks_idempotent(tmp_path, monkeypatch): from truememory.hooks.adapters import openclaw as oc_mod plugins_dir = tmp_path / "plugins" monkeypatch.setattr(oc_mod, "_PLUGINS_DIR", plugins_dir) + monkeypatch.setattr(oc_mod, "_CONFIG_PATH", tmp_path / "openclaw.json") from truememory.hooks.adapters.openclaw import OpenClawAdapter adapter = OpenClawAdapter() adapter.install_hooks(python_path="/usr/bin/python3") diff --git a/tests/test_platform_compat.py b/tests/test_platform_compat.py index 6cdfcd24..de48b509 100644 --- a/tests/test_platform_compat.py +++ b/tests/test_platform_compat.py @@ -85,6 +85,7 @@ def _tmp_config_dir(tmp_path, monkeypatch, ms): (home / ".truememory").mkdir() monkeypatch.setattr(ms, "_TRUEMEMORY_DIR", home / ".truememory") monkeypatch.setattr(ms, "_CONFIG_PATH", home / ".truememory" / "config.json") + monkeypatch.setattr(ms, "_CONFIG_LOCK_PATH", home / ".truememory" / "config.json.lock") return home @@ -158,24 +159,36 @@ def test_ingest_cli_save_warns_on_windows_when_api_key_present( tmp_path, monkeypatch, capsys ): from truememory.ingest import cli as ic - cfg_path = tmp_path / ".truememory" / "config.json" + import truememory.mcp_server as ms + home = _tmp_config_dir(tmp_path, monkeypatch, ms) + cfg_path = home / ".truememory" / "config.json" monkeypatch.setattr(ic, "_TRUEMEMORY_CONFIG_PATH", cfg_path) monkeypatch.setattr(ic.sys, "platform", "win32") + assert ic._TRUEMEMORY_CONFIG_PATH == ms._CONFIG_PATH == cfg_path + assert ms._TRUEMEMORY_DIR == cfg_path.parent + assert ms._CONFIG_LOCK_PATH == cfg_path.parent / "config.json.lock" ic._save_truememory_config({"tier": "pro", "openrouter_api_key": "sk-or-fake"}) captured = capsys.readouterr() assert "windows" in captured.err.lower() assert "environment variable" in captured.err.lower() + assert json.loads(cfg_path.read_text(encoding="utf-8")) == {"tier": "pro", "openrouter_api_key": "sk-or-fake"} def test_ingest_cli_save_silent_on_posix(tmp_path, monkeypatch, capsys): from truememory.ingest import cli as ic - cfg_path = tmp_path / ".truememory" / "config.json" + import truememory.mcp_server as ms + home = _tmp_config_dir(tmp_path, monkeypatch, ms) + cfg_path = home / ".truememory" / "config.json" monkeypatch.setattr(ic, "_TRUEMEMORY_CONFIG_PATH", cfg_path) monkeypatch.setattr(ic.sys, "platform", "linux") + assert ic._TRUEMEMORY_CONFIG_PATH == ms._CONFIG_PATH == cfg_path + assert ms._TRUEMEMORY_DIR == cfg_path.parent + assert ms._CONFIG_LOCK_PATH == cfg_path.parent / "config.json.lock" ic._save_truememory_config({"tier": "pro", "openrouter_api_key": "sk-or-fake"}) captured = capsys.readouterr() assert captured.err == "" + assert json.loads(cfg_path.read_text(encoding="utf-8")) == {"tier": "pro", "openrouter_api_key": "sk-or-fake"} diff --git a/tests/test_reranker_tier.py b/tests/test_reranker_tier.py index 18d351cb..4b2827c6 100644 --- a/tests/test_reranker_tier.py +++ b/tests/test_reranker_tier.py @@ -149,32 +149,57 @@ def test_get_reranker_default_routes_via_tier_resolution(): ) -def test_truememory_configure_propagates_tier_to_reranker_module(): - """Change B.4 regression lock. - - `truememory_configure` must call `reranker.set_active_tier(tier)` AND - pre-load the tier's reranker via `_set_reranker(_current_reranker())`. - Without the first call, changing tier via MCP leaves the reranker module - stuck on the old tier. Without the second, the first post-configure - search pays a cold-start that defeats the whole "configure is the right - moment to warm the new model" design. - - Guarded by source inspection for the same reason as the test above — - calling `truememory_configure` in a unit test would require mocking - sentence_transformers, the Memory singleton, and the re-embed path. Too - much surface for a contract check. - """ - import inspect - - from truememory import mcp_server - - src = inspect.getsource(mcp_server.truememory_configure) - assert "_set_active_tier(tier)" in src, ( - "truememory_configure must call set_active_tier(tier) (imported as " - "_set_active_tier) after saving the new tier to config, so subsequent " - "get_reranker(model_name=None) calls resolve to the new tier's reranker." - ) - assert "_set_reranker(_current_reranker())" in src, ( - "truememory_configure must pre-load the tier's reranker so the first " - "post-configure search does not pay a cold-start (~70ms-3s)." - ) +@pytest.mark.parametrize("tier,expected", [("edge", EDGE_RERANKER), ("base", GTE_RERANKER), ("pro", GTE_RERANKER)]) +def test_truememory_configure_propagates_tier_to_reranker_module(tier, expected, tmp_path, monkeypatch): + """Guarded configuration persists the correct lazy identity without model work.""" + import json + from truememory import mcp_server, model_client, vector_search + from truememory.tier_switch.manager import RebuildManager + + config = tmp_path / "config.json" + monkeypatch.setattr(mcp_server, "_TRUEMEMORY_DIR", tmp_path) + monkeypatch.setattr(mcp_server, "_CONFIG_PATH", config) + monkeypatch.setattr(mcp_server, "_CONFIG_LOCK_PATH", tmp_path / "config.json.lock") + monkeypatch.setattr(mcp_server, "_DB_PATH", str(tmp_path / "synthetic-memories.db")) + monkeypatch.setattr(mcp_server, "_config_cache", None) + monkeypatch.setattr(mcp_server, "_memory", None) + monkeypatch.setattr(RebuildManager, "_instance", RebuildManager()) + monkeypatch.setenv("TRUEMEMORY_EMBED_MODEL", "edge") + monkeypatch.setattr(vector_search, "EMBEDDING_MODEL", "model2vec") + monkeypatch.setattr(vector_search, "_embedding_dim", 256) + monkeypatch.setattr(vector_search, "_model", None) + monkeypatch.setattr(vector_search, "_frozen_embedding_target", None) + monkeypatch.setattr(vector_search, "_runtime_policy_tier", None) + monkeypatch.setattr(reranker, "_model", None) + monkeypatch.setattr(reranker, "_model_name", GTE_RERANKER if expected == EDGE_RERANKER else EDGE_RERANKER) + monkeypatch.setattr(reranker, "_active_tier", "edge") + monkeypatch.setattr(reranker, "_frozen_reranker_id", None) + monkeypatch.setattr(reranker, "_model_certified", False) + calls = [] + embedding_attempts = [] + proxy = object() + + def forbidden_embedding(*args, **kwargs): + embedding_attempts.append((args, kwargs)) + raise AssertionError("Configuration cannot construct or encode embeddings") + + def synthetic_proxy(*, model_name): + calls.append(model_name) + return proxy + + monkeypatch.setattr(vector_search, "get_model", forbidden_embedding) + monkeypatch.setattr(vector_search, "_load_frozen_embedding_target", forbidden_embedding) + monkeypatch.setattr(model_client, "use_model_server", lambda: True) + monkeypatch.setattr(model_client, "get_reranker_proxy", synthetic_proxy) + result = json.loads(mcp_server.truememory_configure(tier=tier)) + assert result["status"] == "configured" + assert result["served_tier"] == tier + assert json.loads(config.read_text(encoding="utf-8"))["tier"] == tier + assert reranker._active_tier == tier + assert get_current_reranker_name() == expected + assert vector_search._model is None + assert reranker._model is None + assert calls == [] + assert embedding_attempts == [] + assert reranker.get_reranker() is proxy + assert calls == [expected] diff --git a/tests/test_sensors.py b/tests/test_sensors.py index 4876abc3..258b5c2d 100644 --- a/tests/test_sensors.py +++ b/tests/test_sensors.py @@ -48,24 +48,24 @@ def test_mps_memory_critical(): def test_mps_memory_no_torch(): with patch.dict("sys.modules", {"torch": None}): result = read_mps_memory(cap_gb=12.0) - assert result["status"] == "ok" - assert result["used_gb"] == 0.0 + assert result["status"] == "unknown" + assert result["used_gb"] is None # ── Channel 2: Growth Rate Tracker ── -def test_growth_rate_first_reading_ok(): +def test_growth_rate_first_reading_needs_another_observation(): tracker = GrowthRateTracker(cap_gb=12.0) result = tracker.update(5.0) - assert result["status"] == "ok" - assert result["slope_gb_per_20s"] == 0.0 + assert result["status"] == "unknown" + assert result["slope_gb_per_20s"] is None def test_growth_rate_stable(): tracker = GrowthRateTracker(cap_gb=12.0) tracker._prev_gb = 5.0 - tracker._prev_time = time.time() - 20 + tracker._prev_time = time.monotonic() - 20 result = tracker.update(5.1) assert result["status"] == "ok" assert result["slope_pct"] < 3.0 @@ -74,7 +74,7 @@ def test_growth_rate_stable(): def test_growth_rate_warning(): tracker = GrowthRateTracker(cap_gb=12.0) tracker._prev_gb = 5.0 - tracker._prev_time = time.time() - 20 + tracker._prev_time = time.monotonic() - 20 result = tracker.update(5.7) assert result["status"] == "warning" assert result["slope_pct"] >= 5.0 @@ -83,7 +83,7 @@ def test_growth_rate_warning(): def test_growth_rate_critical(): tracker = GrowthRateTracker(cap_gb=12.0) tracker._prev_gb = 5.0 - tracker._prev_time = time.time() - 20 + tracker._prev_time = time.monotonic() - 20 result = tracker.update(6.5) assert result["status"] == "critical" assert result["slope_pct"] >= 10.0 @@ -94,7 +94,7 @@ def test_growth_rate_critical(): def test_thermal_ok(): fake_output = "CPU_Scheduler_Limit = 100\n" - with patch("subprocess.run") as mock_run: + with patch("sys.platform", "darwin"), patch("subprocess.run") as mock_run: mock_run.return_value = subprocess.CompletedProcess( args=[], returncode=0, stdout=fake_output, stderr="" ) @@ -105,7 +105,7 @@ def test_thermal_ok(): def test_thermal_warning(): fake_output = "CPU_Scheduler_Limit = 85\n" - with patch("subprocess.run") as mock_run: + with patch("sys.platform", "darwin"), patch("subprocess.run") as mock_run: mock_run.return_value = subprocess.CompletedProcess( args=[], returncode=0, stdout=fake_output, stderr="" ) @@ -116,7 +116,7 @@ def test_thermal_warning(): def test_thermal_critical(): fake_output = "CPU_Scheduler_Limit = 60\n" - with patch("subprocess.run") as mock_run: + with patch("sys.platform", "darwin"), patch("subprocess.run") as mock_run: mock_run.return_value = subprocess.CompletedProcess( args=[], returncode=0, stdout=fake_output, stderr="" ) @@ -126,7 +126,7 @@ def test_thermal_critical(): def test_thermal_command_fails(): - with patch("subprocess.run", side_effect=subprocess.TimeoutExpired("pmset", 5)): + with patch("sys.platform", "darwin"), patch("subprocess.run", side_effect=subprocess.TimeoutExpired("pmset", 0.25)): result = read_thermal_pressure() - assert result["status"] == "ok" - assert result["scheduler_limit"] == 100 + assert result["status"] == "unknown" + assert result["scheduler_limit"] is None diff --git a/tests/test_state_machine.py b/tests/test_state_machine.py index 9e260b37..8132491b 100644 --- a/tests/test_state_machine.py +++ b/tests/test_state_machine.py @@ -78,7 +78,7 @@ def test_backoff_cooldown_returns_to_probing(): sm = ThrottlerStateMachine(start_batch=8, max_batch=12, ramp_step=2) sm.safety_check(_critical_mps()) assert sm.state == ThrottlerStateMachine.BACKOFF - sm.last_backoff_time = time.time() - 121 + sm.last_backoff_time = time.monotonic() - 121 sm.good_streak = 3 assert sm.should_ramp_check() is True assert sm.state == ThrottlerStateMachine.PROBING @@ -86,7 +86,7 @@ def test_backoff_cooldown_returns_to_probing(): def test_ramp_requires_3_good_streaks(): sm = ThrottlerStateMachine(start_batch=4, max_batch=12, ramp_step=2) - sm.last_ramp_time = time.time() - 200 + sm.last_ramp_time = time.monotonic() - 200 sm.good_streak = 2 assert sm.should_ramp_check() is False sm.good_streak = 3 @@ -96,9 +96,9 @@ def test_ramp_requires_3_good_streaks(): def test_ramp_requires_120s_cooldown(): sm = ThrottlerStateMachine(start_batch=4, max_batch=12, ramp_step=2) sm.good_streak = 5 - sm.last_ramp_time = time.time() - 60 + sm.last_ramp_time = time.monotonic() - 60 assert sm.should_ramp_check() is False - sm.last_ramp_time = time.time() - 121 + sm.last_ramp_time = time.monotonic() - 121 assert sm.should_ramp_check() is True diff --git a/truememory/__init__.py b/truememory/__init__.py index b6039ab5..85d107ab 100644 --- a/truememory/__init__.py +++ b/truememory/__init__.py @@ -125,6 +125,7 @@ "consolidation", "predictive", "query_classifier", "reranker", "hyde", "clustering", "tier_config", + "embedding_target", "maintenance", "rebuild_source", "tier_switch", ] diff --git a/truememory/_platform.py b/truememory/_platform.py index 9b3cecbd..ee871186 100644 --- a/truememory/_platform.py +++ b/truememory/_platform.py @@ -4,9 +4,13 @@ creation flags so the two modules stay in sync. """ +import errno +import json +import math import os import socket import sys +from pathlib import Path # --------------------------------------------------------------------------- # Transport detection @@ -18,6 +22,77 @@ _LOOPBACK_HOST: str = "127.0.0.1" """TCP fallback binds/connects here on Windows.""" +_MODEL_SERVER_GENERATION_ENV = "TRUEMEMORY_MODEL_SERVER_GENERATION" + + +def try_file_lock(path: Path) -> int | None: + """Acquire a process-owned, nonblocking lock; closing the fd releases it.""" + fd = os.open(str(path), os.O_CREAT | os.O_RDWR, 0o600) + try: + if sys.platform == "win32": + import msvcrt + os.lseek(fd, 0, os.SEEK_SET) + msvcrt.locking(fd, msvcrt.LK_NBLCK, 1) + else: + import fcntl + fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + except OSError as error: + os.close(fd) + if error.errno in (errno.EACCES, errno.EAGAIN, errno.EDEADLK): + return None + raise + return fd + + +def process_birth(pid: int) -> float | None: + """Identify a process incarnation without trusting a reused PID alone.""" + import psutil + try: + process = psutil.Process(pid) + if process.status() == psutil.STATUS_ZOMBIE: + return None + return process.create_time() + except psutil.NoSuchProcess: + return None + except psutil.AccessDenied as error: + raise OSError("Cannot verify model-server launch ownership") from error + + +def read_start_claim(path: Path) -> dict | None: + """Read a small launch claim. Invalid generations are never authorized.""" + try: + with path.open(encoding="utf-8") as stream: + data = stream.read(4097) + except FileNotFoundError: + return None + except UnicodeError: + return None + if len(data) > 4096: + return None + try: + claim = json.loads(data) + except (ValueError, UnicodeError): + return None + if not isinstance(claim, dict): + return None + generation = claim.get("generation") + if (not isinstance(generation, str) or len(generation) != 32 + or any(char not in "0123456789abcdef" for char in generation)): + return None + for key in ("launcher", "child"): + identity = claim.get(key) + if identity is None and key == "child": + continue + if not isinstance(identity, dict): + return None + pid, born = identity.get("pid"), identity.get("born") + if (isinstance(pid, bool) or not isinstance(pid, int) or pid <= 0 + or isinstance(born, bool) or not isinstance(born, (int, float)) + or not math.isfinite(born) or born <= 0): + return None + claim.setdefault("child", None) + return claim + # --------------------------------------------------------------------------- # Safe environment-variable parsing (issue #639) diff --git a/truememory/client.py b/truememory/client.py index 4fb7c15b..0dccc7fd 100644 --- a/truememory/client.py +++ b/truememory/client.py @@ -176,6 +176,7 @@ def search( query, limit=limit * 3 if user_id else limit, include_directives=include_directives, _skip_reranker=_skip_reranker, + **({"landmark_sender": user_id} if user_id else {}), ) if user_id: @@ -250,6 +251,7 @@ def search_deep( results = self._engine.search_agentic( query, limit=limit * 3 if user_id else limit, llm_fn=llm_fn, include_directives=include_directives, + **({"landmark_sender": user_id} if user_id else {}), ) if user_id: @@ -260,6 +262,23 @@ def search_deep( return results[:limit] + def resolve_temporal_query(self, query: str, user_id: str | None = None) -> dict: + """Inspect date/landmark resolution without retrieving or inventing memories.""" + from truememory.temporal import resolve_temporal_intent, _SOURCE_INTERVAL_ROW_LIMIT, _source_interval_candidates + + self._engine._ensure_connection() + intent = resolve_temporal_intent(self._engine.conn, query, user_id) + if intent.get("after") or intent.get("before"): + _, diagnostic = _source_interval_candidates(self._engine.conn) + if diagnostic is not None: + intent["source_interval_coverage"] = diagnostic + intent["source_interval_lookup_policy"] = { + "candidate_limit": _SOURCE_INTERVAL_ROW_LIMIT, + "visited_row_limit": _SOURCE_INTERVAL_ROW_LIMIT + 1, + "overflow": "incomplete", "complete_archive": False, + } + return intent + def get(self, memory_id: int) -> dict | None: """Retrieve a single memory by ID.""" result = self._engine.get(memory_id) diff --git a/truememory/clustering.py b/truememory/clustering.py index 22120adf..bffe7b82 100644 --- a/truememory/clustering.py +++ b/truememory/clustering.py @@ -24,16 +24,27 @@ from __future__ import annotations +import hashlib +import heapq import logging +import math import sqlite3 import struct from collections import defaultdict +from collections.abc import Callable, Iterator +from contextlib import AbstractContextManager, closing, contextmanager +from itertools import islice +from types import ModuleType import numpy as np logger = logging.getLogger(__name__) +class ClusterModelBusyError(RuntimeError): + """Model ownership is temporarily unavailable; no input version changed.""" + + # --------------------------------------------------------------------------- # Schema # --------------------------------------------------------------------------- @@ -57,10 +68,147 @@ """ -def _init_cluster_tables(conn: sqlite3.Connection): - """Create clustering tables if they don't exist.""" - conn.executescript(_CLUSTER_SCHEMA) - conn.commit() +def _init_cluster_tables(conn: sqlite3.Connection) -> None: + """Create tables inside the publication transaction without committing it.""" + for statement in _CLUSTER_SCHEMA.split(";"): + if statement.strip(): + conn.execute(statement) + + +@contextmanager +def _cluster_transaction(conn: sqlite3.Connection, *, write: bool = False) -> Iterator[None]: + owned = not conn.in_transaction + if owned: + conn.execute("BEGIN IMMEDIATE" if write else "BEGIN") + else: + conn.execute("SAVEPOINT truememory_clusters") + completed = False + try: + if write and not owned: + # Acquire the writer before validating a caller's read snapshot. + # A stale WAL snapshot fails here; no source row is changed. + conn.execute("UPDATE messages SET id = id WHERE 0") + yield + conn.execute("COMMIT" if owned else "RELEASE truememory_clusters") + completed = True + finally: + if not completed and conn.in_transaction: + if owned: + conn.rollback() + else: + conn.execute("ROLLBACK TO truememory_clusters") + conn.execute("RELEASE truememory_clusters") + + +def _cluster_dependency_identity(conn: sqlite3.Connection, vector_search: ModuleType) -> tuple[tuple, ...]: + """Read cheap identity under the model lock and a coherent SQLite snapshot.""" + group = vector_search._active_tier_group() + vec_table = vector_search._active_vec_table(conn) + identity = [("model", vector_search.EMBEDDING_MODEL, vector_search._embedding_dim, group, vec_table)] + definitions = conn.execute( + "SELECT type, name, rootpage, sql FROM sqlite_master " + "WHERE name IN (?, 'messages', 'metadata', 'vector_cache_registry') ORDER BY name", + (vec_table,), + ).fetchall() + identity.append(("schema",)) + identity.extend(tuple(row) for row in definitions) + tables = {row[1] for row in definitions} + identity.append(("registry",)) + if "vector_cache_registry" in tables: + for row in conn.execute("SELECT * FROM vector_cache_registry WHERE tier_group = ?", (group,)): + identity.append(tuple(row)) + identity.append(("metadata",)) + if "metadata" in tables: + build_key = f"vec_build_state:{vec_table}" + for row in conn.execute( + "SELECT key, value, updated_at FROM metadata " + "WHERE key IN ('embed_model', 'embed_dim', ?, ?) ORDER BY key", + (build_key, f"vec_source_v1:{vec_table}"), + ): + if row[0] == build_key and row[1] == "in_progress": + raise RuntimeError("Clustering requires a completed vector index; rebuild is in progress") + identity.append(tuple(row)) + return tuple(identity) + + +def cluster_publication_guard(conn: sqlite3.Connection) -> AbstractContextManager[Callable[[], None]]: + """Capture now; later hold model identity through the runner's terminal write. + + The caller must run cluster_messages in its owned transaction before entering + this guard. Its raw-input fence and retained SQLite writer protect source and + vector bytes; this guard closes the runtime-model gap after nested RELEASE. + """ + if conn.in_transaction: + raise RuntimeError("Cluster publication capture requires a clean transaction boundary") + from truememory import vector_search + + with vector_search._lock: + with _cluster_transaction(conn): + expected = _cluster_dependency_identity(conn, vector_search) + + def validate() -> None: + if _cluster_dependency_identity(conn, vector_search) != expected: + raise RuntimeError("Clustering dependency changed before outer commit") + + @contextmanager + def hold() -> Iterator[Callable[[], None]]: + if not conn.in_transaction: + raise RuntimeError("Cluster publication requires the runner's active transaction") + # Ensure writer admission before taking the nonblocking model lock. + conn.execute("UPDATE messages SET id=id WHERE 0") + acquired = vector_search._lock.acquire(blocking=False) + if not acquired: + raise ClusterModelBusyError("Embedding model is busy before cluster commit") + try: + yield validate + finally: + vector_search._lock.release() + + return hold() + + +def _cluster_source_state( + conn: sqlite3.Connection, vector_search: ModuleType, *, collect_categories: bool = False, +) -> tuple[bytes, dict[int, str | None]]: + """Fingerprint raw inputs under the model lock and a database snapshot.""" + digest = hashlib.sha256() + + def add(row: tuple) -> None: + digest.update(struct.pack(">Q", len(row))) + for value in row: + if value is None: + tag, data = b"n", b"" + elif isinstance(value, bytes): + tag, data = b"b", value + elif isinstance(value, str): + tag, data = b"s", value.encode("utf-8") + elif isinstance(value, int): + tag, data = b"i", str(value).encode("ascii") + elif isinstance(value, float): + tag, data = b"f", struct.pack(">d", value) + else: + raise TypeError("Unsupported SQLite value in clustering source") + digest.update(tag) + digest.update(struct.pack(">Q", len(data))) + digest.update(data) + + identity = _cluster_dependency_identity(conn, vector_search) + for row in identity: + add(row) + vec_table = identity[0][-1] + add(("vectors",)) + quoted_table = '"' + vec_table.replace('"', '""') + '"' + for row in conn.execute(f"SELECT rowid, embedding FROM {quoted_table} ORDER BY rowid"): + add(tuple(row)) + add(("messages",)) + categories = {} + for row in conn.execute( + "SELECT id, content, sender, recipient, timestamp, category, modality FROM messages ORDER BY id" + ): + add(tuple(row)) + if collect_categories: + categories[row[0]] = row[5] + return digest.digest(), categories # --------------------------------------------------------------------------- @@ -108,6 +256,8 @@ def cluster_messages( conn: sqlite3.Connection, min_cluster_size: int = 10, min_samples: int = 5, + *, + cluster_selection_method: str = "eom", ) -> int: """ Cluster messages using HDBSCAN on their embeddings. @@ -119,37 +269,50 @@ def cluster_messages( conn: Open database connection with vec_messages built. min_cluster_size: Minimum cluster size for HDBSCAN. min_samples: Minimum samples for HDBSCAN core points. + cluster_selection_method: Explicit HDBSCAN selection policy, eom or leaf. Returns: Number of clusters found (excluding noise). """ - import hdbscan - - _init_cluster_tables(conn) - - # Clear existing clusters - conn.execute("DELETE FROM message_clusters") - conn.execute("DELETE FROM cluster_centroids") - - # Get embeddings - msg_ids, embeddings = _get_all_embeddings(conn) - if len(msg_ids) == 0: - conn.commit() - return 0 + if type(min_cluster_size) is not int or not 2 <= min_cluster_size <= 2147483647: + raise ValueError("min_cluster_size must be an integer from 2 through 2147483647") + if type(min_samples) is not int or not 1 <= min_samples <= 2147483647: + raise ValueError("min_samples must be an integer from 1 through 2147483647") + if type(cluster_selection_method) is not str or cluster_selection_method not in {"eom", "leaf"}: + raise ValueError("cluster_selection_method must be eom or leaf") - # Normalize for cosine-like clustering - norms = np.linalg.norm(embeddings, axis=1, keepdims=True) - norms[norms == 0] = 1.0 - normed = embeddings / norms + import hdbscan + from truememory import vector_search - # Run HDBSCAN - clusterer = hdbscan.HDBSCAN( - min_cluster_size=min_cluster_size, - min_samples=min_samples, - metric="euclidean", # on normalized vectors ≈ cosine - cluster_selection_method="eom", - ) - labels = clusterer.fit_predict(normed) + # Standalone callers can wait before opening a read transaction. A caller + # transaction may already own SQLite's writer, so never wait inside it. + if not vector_search._lock.acquire(blocking=not conn.in_transaction): + raise ClusterModelBusyError("Embedding model is busy before clustering snapshot") + try: + with _cluster_transaction(conn): + source_state, categories = _cluster_source_state(conn, vector_search, collect_categories=True) + msg_ids, embeddings = _get_all_embeddings(conn) + finally: + vector_search._lock.release() + if any(mid not in categories for mid in msg_ids): + raise RuntimeError("Clustering found vectors without source messages; rebuild the vector index") + + labels = [] + if msg_ids: + # Normalize for cosine-like clustering. + norms = np.linalg.norm(embeddings, axis=1, keepdims=True) + norms[norms == 0] = 1.0 + normed = embeddings / norms + + clusterer = hdbscan.HDBSCAN( + min_cluster_size=min_cluster_size, + min_samples=min_samples, + metric="euclidean", # on normalized vectors ≈ cosine + cluster_selection_method=cluster_selection_method, + ) + labels = clusterer.fit_predict(normed) + if len(labels) != len(msg_ids): + raise ValueError("Clustering returned an incomplete assignment set") # Store cluster assignments rows = [] @@ -157,11 +320,6 @@ def cluster_messages( is_noise = 1 if label == -1 else 0 rows.append((msg_id, int(label), is_noise)) - conn.executemany( - "INSERT INTO message_clusters (message_id, cluster_id, noise) VALUES (?, ?, ?)", - rows, - ) - # Build centroids cluster_vecs = defaultdict(list) cluster_msg_ids = defaultdict(list) @@ -170,25 +328,38 @@ def cluster_messages( cluster_vecs[label].append(emb) cluster_msg_ids[label].append(msg_id) + centroids = [] for cid, vecs in cluster_vecs.items(): centroid = np.mean(vecs, axis=0).astype(np.float32) - # Get session range for this cluster - placeholders = ",".join("?" * len(cluster_msg_ids[cid])) - sessions = conn.execute( - f"SELECT DISTINCT category FROM messages WHERE id IN ({placeholders})", - cluster_msg_ids[cid], - ).fetchall() - session_range = ", ".join(sorted(s[0] for s in sessions if s[0])) + sessions = {categories[mid] for mid in cluster_msg_ids[cid] if categories[mid]} + session_range = ", ".join(sorted(sessions)) + centroids.append((int(cid), _serialize_f32(centroid), len(vecs), session_range)) - conn.execute( - "INSERT INTO cluster_centroids (cluster_id, centroid, message_count, session_range) " - "VALUES (?, ?, ?, ?)", - (int(cid), _serialize_f32(centroid), len(vecs), session_range), - ) - - conn.commit() - n_clusters = len(cluster_vecs) - return n_clusters + model_lock_acquired = False + try: + with _cluster_transaction(conn, write=True): + # Never hold SQLite's writer lock while waiting for native model + # load or a tier change. Keep the model stable through commit. + model_lock_acquired = vector_search._lock.acquire(blocking=False) + if not model_lock_acquired: + raise ClusterModelBusyError("Embedding model is busy; retry clustering after model loading or tier switching") + current_state, _ = _cluster_source_state(conn, vector_search) + if current_state != source_state: + raise RuntimeError("Clustering source changed during computation; retry consolidation") + _init_cluster_tables(conn) + conn.execute("DELETE FROM message_clusters") + conn.execute("DELETE FROM cluster_centroids") + conn.executemany( + "INSERT INTO message_clusters (message_id, cluster_id, noise) VALUES (?, ?, ?)", rows, + ) + conn.executemany( + "INSERT INTO cluster_centroids (cluster_id, centroid, message_count, session_range) " + "VALUES (?, ?, ?, ?)", centroids, + ) + finally: + if model_lock_acquired: + vector_search._lock.release() + return len(cluster_vecs) # --------------------------------------------------------------------------- @@ -223,128 +394,140 @@ def search_clustered( List of result dicts sorted by similarity. """ from truememory.vector_search import get_model + from truememory.mps_utils import encode_with_model_ownership model = get_model() - query_vec = model.encode([query])[0].astype(np.float32) + query_vec = encode_with_model_ownership(model, [query])[0].astype(np.float32) + + # Keep centroids, memberships, vectors and winner content in one snapshot. + # Encoding completes before we open an owned read transaction. + with _cluster_transaction(conn): + return _search_clustered_snapshot(conn, query_vec, limit, top_clusters, include_directives) + + +def _cluster_query_chunk_size(conn: sqlite3.Connection) -> int: + getlimit = getattr(conn, "getlimit", None) + size = min(500, getlimit(sqlite3.SQLITE_LIMIT_VARIABLE_NUMBER)) if getlimit else 500 + if size < 1: + raise ValueError("Clustered search requires SQL query parameters") + return size - # Check if clusters exist + +def _cluster_candidate_ids( + conn: sqlite3.Connection, selected_clusters: list[int], include_directives: bool, chunk_size: int, +) -> Iterator[int]: + directive_filter = "" if include_directives else " AND (m.directive = 0 OR m.directive IS NULL)" + cursors = [] + try: + for start in range(0, len(selected_clusters), chunk_size): + chunk = selected_clusters[start:start + chunk_size] + placeholders = ",".join("?" * len(chunk)) + cursors.append(conn.execute( + f"SELECT DISTINCT m.id FROM messages m " + f"JOIN message_clusters c ON c.message_id = m.id " + f"WHERE c.cluster_id IN ({placeholders}){directive_filter} ORDER BY m.id", chunk, + )) + # The old primary-key IN lookup delivered ascending message IDs. Keep + # that tie order across chunks and deduplicate legacy membership rows. + previous = None + streams = (map(lambda row: row[0], cursor) for cursor in cursors) + for message_id in heapq.merge(*streams): + if message_id != previous: + yield message_id + previous = message_id + finally: + for cursor in cursors: + cursor.close() + + +def _search_clustered_snapshot( + conn: sqlite3.Connection, query_vec: np.ndarray, limit: int, + top_clusters: int, include_directives: bool, +) -> list[dict]: try: - cluster_count = conn.execute( - "SELECT COUNT(*) FROM cluster_centroids" - ).fetchone()[0] + cluster_count = conn.execute("SELECT COUNT(*) FROM cluster_centroids").fetchone()[0] except Exception: return [] - if cluster_count == 0: return [] - # Find top clusters by centroid similarity centroids = conn.execute( "SELECT cluster_id, centroid, message_count FROM cluster_centroids" ).fetchall() - cluster_scores = [] for cid, centroid_blob, msg_count in centroids: dim = len(centroid_blob) // 4 centroid = np.array(struct.unpack(f"{dim}f", centroid_blob), dtype=np.float32) - # Cosine similarity - sim = np.dot(query_vec, centroid) / ( + sim = float(np.dot(query_vec, centroid) / ( np.linalg.norm(query_vec) * np.linalg.norm(centroid) + 1e-9 - ) - cluster_scores.append((cid, float(sim), msg_count)) - - cluster_scores.sort(key=lambda x: x[1], reverse=True) - selected_clusters = [c[0] for c in cluster_scores[:top_clusters]] - + )) + if not math.isfinite(sim): + raise ValueError("Clustered search similarity is nonfinite") + cluster_scores.append((cid, sim, msg_count)) + cluster_scores.sort(key=lambda item: item[1], reverse=True) + selected_clusters = [item[0] for item in cluster_scores[:top_clusters]] if not selected_clusters: return [] - # Get message IDs from selected clusters - placeholders = ",".join("?" * len(selected_clusters)) - cluster_msg_rows = conn.execute( - f"SELECT message_id FROM message_clusters WHERE cluster_id IN ({placeholders})", - selected_clusters, - ).fetchall() - - cluster_msg_ids = {r[0] for r in cluster_msg_rows} - if not cluster_msg_ids: - return [] - - # Get full messages with their embeddings - id_placeholders = ",".join("?" * len(cluster_msg_ids)) - msg_ids_list = list(cluster_msg_ids) - - directive_filter = ( - "" if include_directives - else " AND (m.directive = 0 OR m.directive IS NULL)" - ) - messages = conn.execute( - f"""SELECT m.id, m.content, m.sender, m.recipient, m.timestamp, - m.category, m.modality, m.directive - FROM messages m - WHERE m.id IN ({id_placeholders}){directive_filter}""", - msg_ids_list, - ).fetchall() - - if not messages: + chunk_size = _cluster_query_chunk_size(conn) + keep = limit + if limit < 0: + with closing(_cluster_candidate_ids(conn, selected_clusters, include_directives, chunk_size)) as candidates: + keep = max(0, sum(1 for _ in candidates) + limit) + if keep == 0: return [] - # Resolve vec table once outside the loop (not per-message) from truememory.vector_search import _active_vec_table - _vec_tbl = _active_vec_table(conn) - - # Batch-fetch all embeddings in one query (issue #584) instead of - # one SELECT per message. Chunk into groups of 500 to stay within - # SQLite's parameter limits. - _CHUNK = 500 - emb_map: dict[int, np.ndarray] = {} - all_msg_ids = [msg[0] for msg in messages] - for chunk_start in range(0, len(all_msg_ids), _CHUNK): - chunk = all_msg_ids[chunk_start : chunk_start + _CHUNK] - ph = ",".join("?" * len(chunk)) - try: - rows = conn.execute( - f"SELECT rowid, embedding FROM {_vec_tbl} WHERE rowid IN ({ph})", - chunk, - ).fetchall() - for rid, blob in rows: - dim = len(blob) // 4 - emb_map[rid] = np.array( - struct.unpack(f"{dim}f", blob), dtype=np.float32 - ) - except Exception: - logger.debug("Batch embedding fetch failed for chunk", exc_info=True) - - # Pre-compute query norm once - _query_norm = np.linalg.norm(query_vec) + 1e-9 - - # Score each message by vector similarity to query + vec_table = _active_vec_table(conn) + quoted_table = '"' + vec_table.replace('"', '""') + '"' + query_norm = np.linalg.norm(query_vec) + 1e-9 + winners: list[tuple[float, int, int]] = [] + with closing(_cluster_candidate_ids(conn, selected_clusters, include_directives, chunk_size)) as candidates: + while chunk := list(islice(candidates, chunk_size)): + placeholders = ",".join("?" * len(chunk)) + embeddings = {} + try: + rows = conn.execute( + f"SELECT rowid, embedding FROM {quoted_table} WHERE rowid IN ({placeholders})", chunk, + ).fetchall() + for message_id, blob in rows: + dim = len(blob) // 4 + embeddings[message_id] = np.array(struct.unpack(f"{dim}f", blob), dtype=np.float32) + except Exception: + logger.debug("Batch embedding fetch failed for chunk", exc_info=True) + for message_id in chunk: + vector = embeddings.get(message_id) + sim = 0.0 if vector is None else float(np.dot(query_vec, vector) / ( + query_norm * (np.linalg.norm(vector) + 1e-9) + )) + if not math.isfinite(sim): + raise ValueError("Clustered search similarity is nonfinite") + candidate = (sim, -message_id, message_id) + if len(winners) < keep: + heapq.heappush(winners, candidate) + elif candidate > winners[0]: + heapq.heapreplace(winners, candidate) + + winners.sort(reverse=True) + messages = {} + for start in range(0, len(winners), chunk_size): + ids = [item[2] for item in winners[start:start + chunk_size]] + placeholders = ",".join("?" * len(ids)) + rows = conn.execute( + f"SELECT id, content, sender, recipient, timestamp, category, modality, directive " + f"FROM messages WHERE id IN ({placeholders})", ids, + ) + for row in rows: + messages[row[0]] = row results = [] - for msg in messages: - msg_id = msg[0] - msg_vec = emb_map.get(msg_id) - if msg_vec is not None: - sim = float(np.dot(query_vec, msg_vec) / ( - _query_norm * (np.linalg.norm(msg_vec) + 1e-9) - )) - else: - sim = 0.0 - + for score, _, message_id in winners: + msg = messages[message_id] results.append({ - "id": msg_id, - "content": msg[1], - "sender": msg[2], - "recipient": msg[3], - "timestamp": msg[4], - "category": msg[5], - "modality": msg[6], - "directive": bool(msg[7]), - "score": sim, - "source": "clustered", + "id": message_id, "content": msg[1], "sender": msg[2], "recipient": msg[3], + "timestamp": msg[4], "category": msg[5], "modality": msg[6], + "directive": bool(msg[7]), "score": score, "source": "clustered", }) - - results.sort(key=lambda r: r["score"], reverse=True) - return results[:limit] + return results def get_cluster_info(conn: sqlite3.Connection) -> list[dict]: diff --git a/truememory/consolidation.py b/truememory/consolidation.py index 13115e1c..14d2537d 100644 --- a/truememory/consolidation.py +++ b/truememory/consolidation.py @@ -80,22 +80,17 @@ def _consolidation_write(conn: sqlite3.Connection, name: str): """ sp = f"consolidation_{name}" conn.execute(f"SAVEPOINT {sp}") + completed = False try: yield - except BaseException: - try: + conn.execute(f"RELEASE SAVEPOINT {sp}") + completed = True + finally: + if not completed and conn.in_transaction: + # RELEASE can fail while committing the outermost savepoint. + # Never release failed writes unless their rollback succeeded. conn.execute(f"ROLLBACK TO SAVEPOINT {sp}") - except Exception: - pass - # Always release so we don't leave a dangling savepoint on the - # caller's transaction. - try: conn.execute(f"RELEASE SAVEPOINT {sp}") - except Exception: - pass - raise - else: - conn.execute(f"RELEASE SAVEPOINT {sp}") def _get_all_messages_chrono(conn: sqlite3.Connection) -> list[dict]: @@ -893,8 +888,6 @@ def build_summaries(conn: sqlite3.Connection) -> int: Number of summaries built. """ all_msgs = _get_all_messages_chrono(conn) - if not all_msgs: - return 0 now = datetime.now(timezone.utc).isoformat() # Collect all summary rows first WITHOUT opening a write transaction, so the @@ -1056,7 +1049,7 @@ def build_summaries(conn: sqlite3.Connection) -> int: # table is never left emptied without its replacement), and never touches # isolation_level. with _consolidation_write(conn, "summaries"): - conn.execute("DELETE FROM summaries") + conn.execute("DELETE FROM summaries WHERE period IN ('monthly', 'entity_monthly')") if rows: conn.executemany( "INSERT INTO summaries " diff --git a/truememory/embedding_target.py b/truememory/embedding_target.py new file mode 100644 index 00000000..7ee2be9f --- /dev/null +++ b/truememory/embedding_target.py @@ -0,0 +1,124 @@ +"""Explicit embedding identity for inactive target preparation. + +This module imports no model framework. A descriptor identifies an embedding +space, not resident weights, database readiness, or an active configuration. +""" +from __future__ import annotations + +from dataclasses import dataclass +from collections.abc import Callable +import re +import sys + + +TARGET_PROBE_TEXT = "TrueMemory embedding target validation." + + +class EmbeddingTargetError(ValueError): + """The requested target cannot be certified without changing its identity.""" + + +@dataclass(frozen=True) +class EmbeddingTarget: + tier: str + model_id: str + dimension: int + tier_group: str + + def __post_init__(self) -> None: + builtins = { + "edge": ("model2vec", 256, "edge"), + "base": ("qwen3_256", 256, "basepro"), + "pro": ("qwen3_256", 256, "basepro"), + } + if not isinstance(self.tier, str) or self.tier not in (*builtins, "custom"): + raise EmbeddingTargetError("Prepared targets require an explicit public tier") + if (not isinstance(self.model_id, str) + or re.fullmatch(r"[\w][\w.\-]*(/[\w][\w.\-]*)?", self.model_id) is None): + raise EmbeddingTargetError("Invalid prepared embedding model identity") + if type(self.dimension) is not int or not 1 <= self.dimension <= 4096: + raise EmbeddingTargetError("Prepared embedding dimension must be an integer from 1 to 4096") + if self.tier in builtins: + if self.identity != builtins[self.tier]: + raise EmbeddingTargetError("Prepared target does not match its built-in tier") + elif self.tier_group != "custom": + raise EmbeddingTargetError("Custom prepared targets require the custom table group") + if self.model_id in {"minilm", "bge-small", "qwen3"}: + raise EmbeddingTargetError("This legacy model identity cannot be certified by prepared targets") + if self.model_id in {"model2vec", "qwen3_256"} and self.dimension != 256: + raise EmbeddingTargetError("This built-in embedding model requires 256 dimensions") + + @property + def identity(self) -> tuple[str, int, str]: + return self.model_id, self.dimension, self.tier_group + + @property + def tables(self) -> tuple[str, str]: + return f"vec_messages_{self.tier_group}", f"vec_messages_sep_{self.tier_group}" + + @classmethod + def capture(cls, tier: str) -> EmbeddingTarget: + from truememory.tier_config import get_tier_config + + if not isinstance(tier, str): + raise EmbeddingTargetError("Prepared targets require an explicit public tier") + tier = tier.strip().lower() + config = get_tier_config(tier) + return cls(tier, config["embed_model"], config["embed_dim"], config["tier_group"]) + + def check_configuration(self) -> None: + if self != self.capture(self.tier): + raise EmbeddingTargetError("Prepared target configuration changed; capture a new target") + + def to_wire(self) -> dict: + return {"version": 1, "tier": self.tier, "model_id": self.model_id, + "dimension": self.dimension, "tier_group": self.tier_group} + + @classmethod + def from_wire(cls, value: object) -> EmbeddingTarget: + if (not isinstance(value, dict) + or set(value) != {"version", "tier", "model_id", "dimension", "tier_group"} + or type(value["version"]) is not int or value["version"] != 1): + raise EmbeddingTargetError("Unsupported prepared target descriptor") + return cls(value["tier"], value["model_id"], value["dimension"], value["tier_group"]) + + +def check_target_vectors(target: EmbeddingTarget, vectors: object, count: int) -> None: + if getattr(vectors, "shape", None) != (count, target.dimension): + raise EmbeddingTargetError("Actual embedding output does not match the prepared target dimension") + + +def build_target_model( + target: EmbeddingTarget, device: str | None, *, check: Callable[[], None] | None = None, +) -> object: + """Construct from captured arguments, never the legacy custom fallback.""" + def admit() -> None: + # Revalidate permission after ownership waits and imports, but never + # replace captured constructor arguments with newly read settings. + target.check_configuration() + if check is not None: + check() + + admit() + if target.model_id == "model2vec": + from model2vec import StaticModel + + admit() + return StaticModel.from_pretrained("minishlab/potion-base-8M", force_download=False) + + from truememory.mps_utils import ensure_mps_memory_budget + ensure_mps_memory_budget(device) + from sentence_transformers import SentenceTransformer + + if target.model_id == "qwen3_256": + model_kwargs = {"attn_implementation": "eager"} if sys.platform == "darwin" else None + admit() + return SentenceTransformer( + "Qwen/Qwen3-Embedding-0.6B", truncate_dim=256, + model_kwargs=model_kwargs, device=device, + ) + admit() + return SentenceTransformer( + target.model_id, truncate_dim=target.dimension, + trust_remote_code=False, device=device, + ) diff --git a/truememory/engine.py b/truememory/engine.py index 8f212b54..f1143034 100644 --- a/truememory/engine.py +++ b/truememory/engine.py @@ -31,6 +31,11 @@ import time import sqlite3 from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from collections.abc import Callable + from truememory.maintenance import LayerResult, MaintenanceCoordinator # ─────────────────────────────────────────────────────────────────────────── # Core modules (always available) @@ -43,6 +48,8 @@ from truememory.fts_search import search_fts from truememory._platform import _env_int +from truememory.tier_switch.runtime import engine_handle_operation, engine_operation + logger = logging.getLogger(__name__) # ─────────────────────────────────────────────────────────────────────────── @@ -165,7 +172,11 @@ def get_vectors_load_error() -> str | None: _HAS_TEMPORAL = False try: - from truememory.temporal import detect_temporal_intent, search_temporal, detect_episodes, detect_landmark_events + from truememory.temporal import ( + detect_temporal_intent, search_temporal, detect_episodes, detect_landmark_events, + resolve_temporal_intent, attach_temporal_resolution, + _source_intervals, _temporal_row_matches, _exclusive_upper_bound, + ) _HAS_TEMPORAL = True except (ImportError, ModuleNotFoundError): pass @@ -181,7 +192,6 @@ def get_vectors_load_error() -> str | None: try: from truememory.personality import ( build_entity_profiles, - extract_preferences, search_personality, PERSONALITY_ASPECTS, build_dunbar_hierarchy, @@ -242,7 +252,6 @@ def get_vectors_load_error() -> str | None: _HAS_STYLE_VEC = False try: from truememory.personality_style_vec import ( - build_entity_style_vectors, update_entity_style_vector_incremental as _update_style_vec, ) _HAS_STYLE_VEC = True @@ -254,6 +263,44 @@ def get_vectors_load_error() -> str | None: # Helper # ─────────────────────────────────────────────────────────────────────────── +def _validate_add_content(function: "Callable") -> "Callable": + """Validate pure store arguments before database or runtime admission.""" + from functools import wraps + + @wraps(function) + def validated(engine: object, content: str, *args: object, **kwargs: object) -> dict: + if not isinstance(content, str): + raise TypeError(f"content must be a string, got {type(content).__name__}") + + # M-60: enforce the store size cap at the engine level so ALL entry + # points (client.add, hooks, direct Engine.add) inherit it — not just + # mcp_server. Unbounded content means unbounded embed latency and + # poison-large rows. Mirror the mcp_server limit. + if len(content) > MAX_CONTENT_LENGTH: + raise ValueError( + f"Content too large ({len(content)} chars). " + f"Maximum is {MAX_CONTENT_LENGTH}." + ) + + return function(engine, content, *args, **kwargs) + return validated + + +def _validate_delete_user(function: "Callable") -> "Callable": + """Reject invalid deletion scope without opening or initializing storage.""" + from functools import wraps + + @wraps(function) + def validated(engine: object, user_id: str | None = None, *args: object, **kwargs: object) -> bool: + if user_id is not None and not isinstance(user_id, str): + raise TypeError(f"user_id must be a string or None, got {type(user_id).__name__}") + if isinstance(user_id, str) and not user_id.strip(): + raise ValueError("user_id cannot be an empty string") + + return function(engine, user_id, *args, **kwargs) + return validated + + def _has_personality_intent(query: str) -> bool: """ Return True if the query is genuinely asking about personality, preferences, @@ -335,8 +382,13 @@ def __init__(self, db_path: str | Path = None, self.conn: sqlite3.Connection | None = None self.ready = False self.stats: dict = {} - self._write_lock = threading.Lock() + from truememory.tier_switch.runtime import ConnectionWriteLock + self._write_lock = ConnectionWriteLock(self) self._init_lock = threading.Lock() + self._runtime_initialized = False + self._runtime_vector_generation = None + self._runtime_vector_connection = None + self._reconnect_receipt = None # L5 surprise rerank boost coefficient. None = resolve from env # var / default at call-time via _get_alpha_surprise(). @@ -358,102 +410,412 @@ def __init__(self, db_path: str | Path = None, # Coordination flag for background NaN re-embed thread (#485). self._nan_migration_in_progress = False - # Auto-consolidation: run L5 consolidation every N adds (#498) - self._adds_since_consolidation = 0 self._auto_consolidate_threshold = _env_int( "TRUEMEMORY_AUTO_CONSOLIDATE_EVERY", 25, lo=1 ) - self._consolidation_thread: threading.Thread | None = None + self._maintenance_coordinator = None + self._maintenance_handle = None + self._maintenance_pending_reason = "not_connected" # ────────────────────────────────────────────────────────────────────── # Auto-connect (production API) # ────────────────────────────────────────────────────────────────────── - def _ensure_connection(self) -> None: - """Open (or create) the database and load extensions if needed. + def _open_connection_handle(self) -> None: + """Open a plain handle before serving admission or vector initialization.""" + from truememory.tier_switch.runtime import _connection_read, _open_serving_connection_at_path + sqlite_ioerr = getattr(sqlite3, "SQLITE_IOERR", 10) - Called automatically by :meth:`add`, :meth:`search`, and other - public methods so users never have to call ``ingest()`` or - ``open()`` manually for simple CRUD workflows. - """ - if self.conn is not None: - # SRE-01: a cached handle can be poisoned by a transient disk I/O - # error (e.g. a stray -wal deletion) or closed out from under us. - # Previously the short-circuit returned the dead handle forever, so a - # long-lived MCP server failed every add/search/recall until manual - # restart. Probe the handle; if the probe fails, drop it and fall - # through to reconnect so the server self-heals once the FS recovers. + def replacement_reason() -> str | None: + if self.conn is None: + return "missing" try: self.conn.execute("PRAGMA schema_version") - return - except sqlite3.Error: - logger.warning( - "DB connection probe failed for %s; reconnecting", - self.db_path, exc_info=True, - ) - try: - self.conn.close() - except sqlite3.Error: - pass - self.conn = None - self._has_vectors = False - - with self._init_lock: - if self.conn is not None: - return + return None + except (sqlite3.ProgrammingError, sqlite3.OperationalError) as exc: + if (isinstance(exc, sqlite3.ProgrammingError) + and str(exc) == "Cannot operate on a closed database."): + return "closed" + code = getattr(exc, "sqlite_errorcode", None) + io_error = (isinstance(exc, sqlite3.OperationalError) and ( + str(exc) == "disk I/O error" + or (isinstance(code, int) and code & 0xff == sqlite_ioerr))) + if not io_error or self.conn.in_transaction: + raise + return "io_error" - # Create parent directory if using a real path - db_str = str(self.db_path) - if db_str != ":memory:": - # M-89: the DB dir holds real memories/PII — owner-only (0700). - self.db_path.parent.mkdir(parents=True, exist_ok=True) + with _connection_read(self.conn, self._init_lock, None, None, + transaction_owner=self._write_lock, allow_closed=True): + with _connection_read(self.conn, self._write_lock, None, None, allow_closed=True): + reason = replacement_reason() + if reason is None: + return + path = self.db_path if str(self.db_path) == ":memory:" else self.db_path.resolve() + if reason != "missing" and getattr(self, "_reconnect_receipt", None) is not None: + replacement = self._reopen_initialized_connection(path, receipt=self._reconnect_receipt) + if replacement is not None: + candidate, controlled = replacement + installed = False + try: + with _connection_read(self.conn, self._write_lock, None, None, allow_closed=True): + reason = replacement_reason() + if reason is None: + return + if reason == "io_error": + self.conn.close() + self.conn = candidate + installed = True + self._runtime_vector_generation = None + self._runtime_vector_connection = None + if controlled: + self.ready = self._runtime_initialized = self._has_vectors = False + self._reconnect_receipt = None + return + finally: + if not installed: + candidate.close() + if reason == "missing" and str(path) != ":memory:": + from truememory.maintenance import _active_initialization_receipt + with _active_initialization_receipt(path) as observed: + offered = observed + if offered is not None: + replacement = self._reopen_initialized_connection(path, receipt=offered[2]) + if replacement is not None: + candidate, controlled = replacement + installed = False + try: + with _connection_read(self.conn, self._write_lock, None, None, allow_closed=True): + reason = replacement_reason() + if reason is None: + return + if reason == "missing": + with _active_initialization_receipt(path, expected=offered[:2]) as current: + if current is not None: + self.conn = candidate + self._has_vectors = not controlled and offered[2][7] + self._has_hybrid = _HAS_HYBRID and self._has_vectors + self.ready = self._runtime_initialized = not controlled + self._runtime_vector_generation = None + self._runtime_vector_connection = None + self._reconnect_receipt = None if controlled else offered[2] + installed = True + if installed: + return + finally: + if not installed: + candidate.close() + from truememory.maintenance import MaintenanceBusyError, maintenance_owner + if str(path) != ":memory:": + path.parent.mkdir(parents=True, exist_ok=True) try: - self.db_path.parent.chmod(0o700) + path.parent.chmod(0o700) except OSError: pass + # Keep same-Engine openers serialized, but never claim maintenance + # ownership while holding the connection writer. + poisoned = self.conn if reason in {"closed", "io_error"} else None + try: + with maintenance_owner(None if str(path) == ":memory:" else path), \ + _connection_read(self.conn, self._write_lock, None, None, allow_closed=True): + reason = replacement_reason() + if reason is None: + return + if reason == "io_error": + self.conn.close() + self.conn = None + self._has_vectors = False + self.ready = False + self._runtime_initialized = False + self._runtime_vector_generation = None + self._runtime_vector_connection = None + self._reconnect_receipt = None + self.conn = _open_serving_connection_at_path(path, create_db) + except MaintenanceBusyError as error: + if poisoned is not None and self.conn is poisoned: + # Public opening may retry this unusable handle only after + # validating the exact automatic owner on the same error. + error._reconnect_engine = self + error._reconnect_handle = poisoned + raise - self.conn = create_db(self.db_path) + @staticmethod + def _reconnect_registry(conn: sqlite3.Connection) -> tuple | None: + """Bound the legacy registry identity, including mutations without DDL.""" + if not conn.execute( + "SELECT 1 FROM main.sqlite_master WHERE name='vector_cache_registry' COLLATE NOCASE" + ).fetchone(): + return () + if conn.execute( + "SELECT 1 FROM main.vector_cache_registry WHERE " + "typeof(tier_group)!='text' OR length(CAST(tier_group AS BLOB))>16 OR " + "typeof(vec_table)!='text' OR length(CAST(vec_table AS BLOB))>64 OR " + "typeof(sep_table)!='text' OR length(CAST(sep_table AS BLOB))>64 OR " + "(model_name IS NOT NULL AND (typeof(model_name)!='text' OR length(CAST(model_name AS BLOB))>512)) OR " + "typeof(embedding_dim) NOT IN ('integer','real','null') OR " + "typeof(last_embedded_id) NOT IN ('integer','real','null') OR " + "typeof(vector_count) NOT IN ('integer','real','null') OR " + "typeof(last_updated) NOT IN ('integer','real','null') OR " + "typeof(created) NOT IN ('integer','real','null') LIMIT 1" + ).fetchone(): + return None + rows = tuple(tuple(row) for row in conn.execute( + "SELECT tier_group,vec_table,sep_table,model_name,embedding_dim," + "last_embedded_id,vector_count,last_updated,created " + "FROM main.vector_cache_registry ORDER BY tier_group LIMIT 4" + ).fetchall()) + if len(rows) > 3 or any( + row[1] not in _ALL_VEC_TABLES or row[2] not in _ALL_VEC_TABLES + or any(value is not None and type(value) not in (int, float) for value in row[4:]) + for row in rows): + return None + return rows - # Load sqlite-vec extension - global _vectors_load_error - if _HAS_VECTOR: + def _capture_reconnect_receipt(self, operation: object) -> None: + """Remember only this Engine's completed legacy initialization, never a model.""" + self._reconnect_receipt = None + if self.conn.in_transaction or (not self._has_vectors and _HAS_VECTOR + and callable(getattr(self.conn, "enable_load_extension", None))): + return + from truememory.tier_switch.runtime import _legacy_key, TierRuntimeError + from truememory import vector_search + key = operation.key + if (key != _legacy_key() or key[0] != "legacy" + or any(type(value) is not str or len(value) > 512 for value in key)): + return + path = self.db_path.resolve() + if operation._database != ("file", str(path)): + return + receipt = None + try: + self.conn.execute("BEGIN") + stat = path.stat() + registry = self._reconnect_registry(self.conn) + if registry is None: + return + group = vector_search._active_tier_group() + tables = next((row[1:3] for row in registry if row[0] == group), + ("vec_messages", "vec_messages_sep")) + schema = self.conn.execute("PRAGMA schema_version").fetchone()[0] + if not self.conn.execute( + "SELECT 1 FROM main.metadata WHERE key='l4_entity_profile_migration_done' AND value='1'" + ).fetchone(): + return + receipt = (str(path), stat.st_dev, stat.st_ino, schema, + key, tables, registry, self._has_vectors, _HAS_VECTOR, + self.conn.execute("PRAGMA journal_mode").fetchone()[0]) + except (OSError, sqlite3.Error): + logger.debug("Connection initialization cannot be reused", exc_info=True) + finally: + if self.conn.in_transaction: + self.conn.rollback() + if self.conn.in_transaction: + raise TierRuntimeError("Reconnect receipt requires completed read cleanup") + self._reconnect_receipt = receipt + + def _reopen_initialized_connection(self, path: Path, *, receipt: tuple | None = None) -> tuple[sqlite3.Connection, bool] | None: + """Read-only validation allows same-Engine reconnect without migration ownership.""" + from truememory.storage import DatabaseOpenError, _integrity_message + from truememory.tier_switch.runtime import _legacy_key, _read_selection, _read_policy, TierRuntimeError + if receipt is None: + receipt = self._reconnect_receipt + try: + stat = path.stat() + except OSError: + return None + identity = (str(path), stat.st_dev, stat.st_ino) + if identity != receipt[:3] or _HAS_VECTOR != receipt[8]: + return None + candidate = sqlite3.connect(path.as_uri() + "?mode=rw", uri=True, check_same_thread=False) + retained = False + try: + # A receipt from a connection without extension loading must not + # suppress normal vector initialization on a newly capable handle. + if receipt[8] and not receipt[7] and callable(getattr(candidate, "enable_load_extension", None)): + return None + candidate.execute(f"PRAGMA busy_timeout={DEFAULT_BUSY_TIMEOUT_MS}") + candidate.execute("PRAGMA foreign_keys=ON") + candidate.execute("PRAGMA synchronous=NORMAL") + candidate.execute("PRAGMA cache_size=-64000") + candidate.execute("PRAGMA mmap_size=268435456") + if candidate.execute("PRAGMA journal_mode").fetchone()[0] != receipt[9]: + return None + try: + qc = candidate.execute("PRAGMA quick_check(1)").fetchone() + except sqlite3.DatabaseError as exc: + if "i/o error" in str(exc).lower(): + raise DatabaseOpenError( + "TrueMemory could not open the database due to a disk " + f"I/O error: {path}\n" + "This usually means the SQLite WAL file is " + "inconsistent (e.g. a -wal/-shm file was removed while " + "a connection was still open). Close ALL TrueMemory " + "processes (hooks, MCP server, CLI) and retry." + ) from exc + raise DatabaseOpenError(_integrity_message(path, str(exc))) from exc + if qc is not None and qc[0] != "ok": + raise DatabaseOpenError(_integrity_message(path, str(qc[0]))) + if receipt[7]: + import sqlite_vec + candidate.enable_load_extension(True) try: + sqlite_vec.load(candidate) + finally: + candidate.enable_load_extension(False) + candidate.execute("BEGIN") + if candidate.execute("PRAGMA schema_version").fetchone()[0] != receipt[3]: + return None + selection = _read_selection(candidate) + policy = _read_policy(candidate) if selection is None else None + controlled = selection is not None or policy is not None + if not controlled: + if _legacy_key() != receipt[4] or self._reconnect_registry(candidate) != receipt[6]: + return None + values = candidate.execute( + "SELECT key, CASE WHEN typeof(value)='text' AND length(CAST(value AS BLOB))<=512 " + "THEN value ELSE NULL END FROM main.metadata " + "WHERE key IN ('embed_model','embed_dim') LIMIT 3" + ).fetchall() + expected = {"embed_model": receipt[4][1], "embed_dim": receipt[4][2]} + if values: + if len(values) != 2 or dict(values) != expected: + return None + elif receipt[7]: + # A first add may publish metadata after initialization. Its + # absence is safe only while both initialized vector tables are empty. + for table in receipt[5]: + if candidate.execute(f'SELECT 1 FROM main."{table}" LIMIT 1').fetchone(): + return None + current = path.stat() + if (str(path), current.st_dev, current.st_ino) != identity: + return None + candidate.rollback() + if candidate.in_transaction: + raise TierRuntimeError("Reconnect candidate requires completed read cleanup") + retained = True + return candidate, controlled + finally: + if not retained: + candidate.close() + + def _ensure_connection(self, *, _suppress_maintenance: bool = False) -> None: + """Reconcile committed selection before any vector compatibility work.""" + from truememory.tier_switch.runtime import engine_serving_operation + with engine_serving_operation(self, _suppress_maintenance=_suppress_maintenance): + self._initialize_connection(_suppress_maintenance=_suppress_maintenance) + + def _initialize_connection(self, *, _suppress_maintenance: bool = False, + _preparing: bool = False) -> None: + from truememory.tier_switch.runtime import current_operation, TierRuntimeError, _connection_read + operation = current_operation(self.conn) + _suppress_maintenance = _suppress_maintenance or operation._defer_maintenance + controlled = operation.selection or operation.policy + if controlled is not None: + if (getattr(self, "_runtime_initialized", False) + and getattr(self, "_runtime_vector_connection", None) is self.conn + and getattr(self, "_runtime_vector_generation", None) == controlled.generation): + if not _suppress_maintenance: + self._maybe_auto_consolidate() + return + initialized = False + with _connection_read(self.conn, self._init_lock, None, None, + transaction_owner=self._write_lock), \ + _connection_read(self.conn, self._write_lock, None, None): + if not (getattr(self, "_runtime_initialized", False) + and getattr(self, "_runtime_vector_connection", None) is self.conn + and getattr(self, "_runtime_vector_generation", None) == controlled.generation): + if self.conn.in_transaction: + raise TierRuntimeError("Selected vector initialization requires a clean connection") + if not _HAS_VECTOR: + raise TierRuntimeError("Selected vector dependencies are unavailable") import sqlite_vec self.conn.enable_load_extension(True) - sqlite_vec.load(self.conn) - self.conn.enable_load_extension(False) - # init_vec_table() runs _check_embedder_compatibility(), - # which raises TrueMemoryMigrationError on a dim/model - # mismatch. That must NOT be swallowed as a generic - # FTS-only fallback (M-12/M-46): a silent drop to FTS hides - # the degradation from truememory_stats.health and lets - # add() silently fail vec INSERTs. Surface the migration - # guidance so the user can re-embed instead. - init_vec_table(self.conn) try: - from truememory.vector_search import migrate_legacy_vec_tables - migrate_legacy_vec_tables(self.conn) - except Exception: - logger.debug("Legacy vec table migration skipped", exc_info=True) + sqlite_vec.load(self.conn) + finally: + self.conn.enable_load_extension(False) + init_vec_table(self.conn) self._has_vectors = True - except TrueMemoryMigrationError as exc: - # M-12/M-46: record the degradation in module state so - # health surfaces it, then propagate the actionable - # migration guidance instead of falling back to FTS-only. - _vectors_load_error = f"{type(exc).__name__}: {exc}" - self._has_vectors = False - raise - except Exception as exc: - _vectors_load_error = f"{type(exc).__name__}: {exc}" - logger.warning("Failed to load sqlite-vec — FTS-only mode: %s", exc) - self._has_vectors = False + self._has_hybrid = _HAS_HYBRID + self._purge_legacy_entity_profile_summaries() + self.ready = self._runtime_initialized = True + self._runtime_vector_generation = controlled.generation + self._runtime_vector_connection = self.conn + initialized = True + if not _suppress_maintenance: + if initialized: + self._maybe_startup_consolidate() + else: + self._maybe_auto_consolidate() + return + if getattr(self, "_runtime_initialized", True) or getattr(self, "ready", False): + receipt = getattr(self, "_reconnect_receipt", None) + if receipt is not None and operation.key[1:3] != receipt[4][1:3]: + raise TierRuntimeError("Initialized embedding identity changed before serving admission") + if not _suppress_maintenance: + self._maybe_auto_consolidate() + return + import sys as _sys + defer_legacy_migration = (_preparing and _sys.platform == "darwin" + and operation.tier in ("base", "pro", "qwen3_256") + and str(self.db_path) == ":memory:") + with _connection_read(self.conn, self._init_lock, None, None, + transaction_owner=self._write_lock): + if self._runtime_initialized: + if not _suppress_maintenance: + self._maybe_auto_consolidate() + return + with _connection_read(self.conn, self._write_lock, None, None): + if self.conn.in_transaction: + raise TierRuntimeError("Vector initialization requires a clean connection") + from truememory.tier_switch.runtime import _legacy_initialization_is_local + local_only = False + # Load sqlite-vec extension + global _vectors_load_error + if _HAS_VECTOR: + try: + import sqlite_vec + self.conn.enable_load_extension(True) + sqlite_vec.load(self.conn) + self.conn.enable_load_extension(False) + local_only = not _preparing and _legacy_initialization_is_local(self.conn, operation) + # init_vec_table() runs _check_embedder_compatibility(), + # which raises TrueMemoryMigrationError on a dim/model + # mismatch. That must NOT be swallowed as a generic + # FTS-only fallback (M-12/M-46): a silent drop to FTS hides + # the degradation from truememory_stats.health and lets + # add() silently fail vec INSERTs. Surface the migration + # guidance so the user can re-embed instead. + if not local_only: + init_vec_table(self.conn) + try: + from truememory.vector_search import migrate_legacy_vec_tables + if _preparing and not defer_legacy_migration: + migrate_legacy_vec_tables(self.conn) + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) + logger.debug("Legacy vec table migration skipped", exc_info=True) + self._has_vectors = True + except TrueMemoryMigrationError as exc: + # M-12/M-46: record the degradation in module state so + # health surfaces it, then propagate the actionable + # migration guidance instead of falling back to FTS-only. + _vectors_load_error = f"{type(exc).__name__}: {exc}" + self._has_vectors = False + raise + except Exception as exc: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(exc) + _vectors_load_error = f"{type(exc).__name__}: {exc}" + logger.warning("Failed to load sqlite-vec — FTS-only mode: %s", exc) + self._has_vectors = False - self._has_hybrid = _HAS_HYBRID and self._has_vectors + self._has_hybrid = _HAS_HYBRID and self._has_vectors # Qwen3 NaN fix: macOS SDPA kernel produces NaN embeddings. # Re-embed once for Base/Pro users on macOS. - import sys as _sys - if _sys.platform == "darwin" and self._has_vectors: + if (not local_only and _sys.platform == "darwin" and self._has_vectors + and current_operation(self.conn).selection is None): try: _embed_model = resolve_tier() if _embed_model in ("base", "pro", "qwen3_256"): @@ -470,46 +832,49 @@ def _ensure_connection(self) -> None: ).fetchone()[0] if "messages" in _tables else 0 if _msg_count > 0: def _bg_reembed(db_path): - import sqlite3 as _sql + from truememory.tier_switch.runtime import open_serving_connection, legacy_rebuild_operation _conn = None try: - _conn = _sql.connect(str(db_path), check_same_thread=False) - _conn.execute("PRAGMA journal_mode=WAL") - _conn.execute("PRAGMA busy_timeout=%d" % DEFAULT_BUSY_TIMEOUT_MS) - _conn.execute("PRAGMA synchronous=NORMAL") - _conn.execute("PRAGMA foreign_keys=ON") - # Issue #499: load sqlite-vec on the - # background thread's connection so - # vec virtual tables are available. - import sqlite_vec as _sv - _conn.enable_load_extension(True) - _sv.load(_conn) - _conn.enable_load_extension(False) - from truememory.vector_search import ( - build_vectors as _bv, - build_separation_vectors as _bsv, - init_vec_table as _ivt, - ) - _conn.execute("DROP TABLE IF EXISTS vec_messages") - _conn.execute("DROP TABLE IF EXISTS vec_messages_sep") - _conn.commit() - _ivt(_conn) - _bv(_conn) - _bsv(_conn) - # Issue #485: only set the flag AFTER - # successful re-embed so a failed - # thread allows retry on next init. - _conn.execute( - "INSERT OR REPLACE INTO metadata " - "(key, value) VALUES (?, ?)", - ("qwen3_nan_fix_applied", "1"), - ) - _conn.commit() - logger.warning( - "Qwen3 NaN fix: re-embedded %d vectors " - "in background", _msg_count, - ) - except Exception: + _conn = open_serving_connection(db_path, create_db) + with legacy_rebuild_operation(_conn): + _conn.execute("PRAGMA journal_mode=WAL") + _conn.execute("PRAGMA busy_timeout=%d" % DEFAULT_BUSY_TIMEOUT_MS) + _conn.execute("PRAGMA synchronous=NORMAL") + _conn.execute("PRAGMA foreign_keys=ON") + # Issue #499: load sqlite-vec on the + # background thread's connection so + # vec virtual tables are available. + import sqlite_vec as _sv + _conn.enable_load_extension(True) + _sv.load(_conn) + _conn.enable_load_extension(False) + from truememory.vector_search import ( + build_vectors as _bv, + build_separation_vectors as _bsv, + init_vec_table as _ivt, + ) + _conn.execute("DROP TABLE IF EXISTS vec_messages") + _conn.execute("DROP TABLE IF EXISTS vec_messages_sep") + _conn.commit() + _ivt(_conn) + _bv(_conn) + _bsv(_conn) + # Issue #485: only set the flag AFTER + # successful re-embed so a failed + # thread allows retry on next init. + _conn.execute( + "INSERT OR REPLACE INTO metadata " + "(key, value) VALUES (?, ?)", + ("qwen3_nan_fix_applied", "1"), + ) + _conn.commit() + logger.warning( + "Qwen3 NaN fix: re-embedded %d vectors " + "in background", _msg_count, + ) + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.warning( "Qwen3 NaN background migration failed — " "will retry on next startup", @@ -519,7 +884,9 @@ def _bg_reembed(db_path): if _conn is not None: try: _conn.close() - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) pass _db = self.db_path if str(_db) != ":memory:": @@ -539,36 +906,42 @@ def _bg_wrapper(db_path): "in background thread", _msg_count, ) else: - from truememory.vector_search import ( - build_vectors as _bv, - build_separation_vectors as _bsv, - init_vec_table as _ivt, - ) - self.conn.execute("DROP TABLE IF EXISTS vec_messages") - self.conn.execute("DROP TABLE IF EXISTS vec_messages_sep") - self.conn.commit() - _ivt(self.conn) - _bv(self.conn) - _bsv(self.conn) + from truememory.tier_switch.runtime import legacy_rebuild_operation + with legacy_rebuild_operation(self.conn): + from truememory.vector_search import ( + build_vectors as _bv, + build_separation_vectors as _bsv, + init_vec_table as _ivt, + ) + self.conn.execute("DROP TABLE IF EXISTS vec_messages") + self.conn.execute("DROP TABLE IF EXISTS vec_messages_sep") + self.conn.commit() + _ivt(self.conn) + _bv(self.conn) + _bsv(self.conn) + self.conn.execute( + "INSERT OR REPLACE INTO metadata " + "(key, value) VALUES (?, ?)", + ("qwen3_nan_fix_applied", "1"), + ) + self.conn.commit() + logger.warning( + "Qwen3 NaN fix: re-embedded all vectors", + ) + else: + # No messages — just set the flag to skip + # future checks. + from truememory.tier_switch.runtime import legacy_vector_mutation + with legacy_vector_mutation(self.conn): self.conn.execute( "INSERT OR REPLACE INTO metadata " "(key, value) VALUES (?, ?)", ("qwen3_nan_fix_applied", "1"), ) self.conn.commit() - logger.warning( - "Qwen3 NaN fix: re-embedded all vectors", - ) - else: - # No messages — just set the flag to skip - # future checks. - self.conn.execute( - "INSERT OR REPLACE INTO metadata " - "(key, value) VALUES (?, ?)", - ("qwen3_nan_fix_applied", "1"), - ) - self.conn.commit() - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.warning( "Qwen3 NaN migration failed — run " "'truememory-ingest upgrade-tier base --force' to fix manually", @@ -578,9 +951,22 @@ def _bg_wrapper(db_path): # MEMORIST-L4 migration: purge legacy entity_profile summary rows. # M-84: previously this only ran in the deprecated open() path, so # production (which uses _ensure_connection) never purged them. - self._purge_legacy_entity_profile_summaries() - - self.ready = True + with _connection_read(self.conn, self._write_lock, None, None): + if defer_legacy_migration and self._has_vectors: + try: + from truememory.vector_search import migrate_legacy_vec_tables + migrate_legacy_vec_tables(self.conn) + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) + logger.debug("Legacy vec table migration skipped", exc_info=True) + if not local_only: + self._purge_legacy_entity_profile_summaries() + self.ready = True + self._runtime_initialized = True + if getattr(operation, "_database", (None,))[0] == "file": + self._capture_reconnect_receipt(operation) + if not _suppress_maintenance: self._maybe_startup_consolidate() def _purge_legacy_entity_profile_summaries(self) -> None: @@ -656,30 +1042,15 @@ def _purge_legacy_entity_profile_summaries(self) -> None: ) def _maybe_startup_consolidate(self) -> None: - """Trigger background consolidation on startup if data looks stale.""" - if not self._has_consolidation or self.conn is None: - return - try: - msg_count = self.conn.execute("SELECT COUNT(*) FROM messages").fetchone()[0] - if msg_count < self._auto_consolidate_threshold: - return - cluster_count = self.conn.execute( - "SELECT COUNT(*) FROM message_clusters" - ).fetchone()[0] - if cluster_count == 0: - self._consolidation_thread = threading.Thread( - target=self._bg_consolidate, - daemon=True, - name="startup-consolidate", - ) - self._consolidation_thread.start() - except Exception: - pass + """Use durable per-layer provenance, including successful empty work.""" + self._maybe_auto_consolidate() # ────────────────────────────────────────────────────────────────────── # Production CRUD API # ────────────────────────────────────────────────────────────────────── + @_validate_add_content + @engine_operation def add( self, content: str, @@ -706,33 +1077,29 @@ def add( Returns: Dict with ``id`` and the stored fields. """ - if not isinstance(content, str): - raise TypeError(f"content must be a string, got {type(content).__name__}") - - # M-60: enforce the store size cap at the engine level so ALL entry - # points (client.add, hooks, direct Engine.add) inherit it — not just - # mcp_server. Unbounded content means unbounded embed latency and - # poison-large rows. Mirror the mcp_server limit. - if len(content) > MAX_CONTENT_LENGTH: - raise ValueError( - f"Content too large ({len(content)} chars). " - f"Maximum is {MAX_CONTENT_LENGTH}." - ) - self._ensure_connection() + from truememory.tier_switch.writer import capture_writer_selection, writer_transaction + write_selection = capture_writer_selection(self.conn) + pre_embedding = None pre_sep_embedding = None if self._has_vectors: + from truememory.vector_search import VectorPublicationChanged try: from truememory.vector_search import ( - get_model, _encode_with_mps_fallback, _build_sep_text, + _capture_rebuild_model, _encode_with_mps_fallback, _build_sep_text, ) - model = get_model() + with self._write_lock: + model, pre_identity = _capture_rebuild_model(self.conn) pre_embedding = _encode_with_mps_fallback(model, [content])[0] sep_text = _build_sep_text(sender, recipient, timestamp, content) pre_sep_embedding = _encode_with_mps_fallback(model, [sep_text])[0] - except Exception: + except VectorPublicationChanged: + raise + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.debug("Failed to pre-compute embedding during add()", exc_info=True) pre_style_vec = None @@ -740,10 +1107,26 @@ def add( try: from truememory.personality_style_vec import compute_style_vector pre_style_vec = compute_style_vector(content) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.debug("Failed to pre-compute style vector during add()", exc_info=True) - with self._write_lock: + from contextlib import nullcontext + if pre_embedding is not None: + from truememory.vector_search import _foreground_model_fence + + with self._write_lock, ( + _foreground_model_fence(pre_identity, blocking=not self.conn.in_transaction) + if pre_embedding is not None else nullcontext() + ), writer_transaction( + self.conn, write_selection, + model_id=pre_identity[1] if pre_embedding is not None else None, + dimension=pre_identity[2] if pre_embedding is not None else None, + ): + if self._has_style_vec: + from truememory.maintenance import _capture_style_append, _publish_style_append + style_before = _capture_style_append(self.conn) msg = { "content": content, "sender": sender, @@ -757,24 +1140,30 @@ def add( new_id = insert_message(self.conn, msg) if pre_embedding is not None: + from truememory.vector_search import VectorPublicationChanged, _validate_foreground_vector_target try: from truememory.vector_search import ( - serialize_f32, _active_vec_table, _active_sep_table, - _write_embedder_metadata, + serialize_f32, + _write_foreground_embedder_metadata_no_commit, + ) + vec_tbl, sep_tbl = _validate_foreground_vector_target( + self.conn, pre_identity, new_id, selection=write_selection, ) - vec_tbl = _active_vec_table(self.conn) self.conn.execute( f"INSERT INTO {vec_tbl}(rowid, embedding) VALUES (?, ?)", (new_id, serialize_f32(pre_embedding)), ) if pre_sep_embedding is not None: - sep_tbl = _active_sep_table(self.conn) self.conn.execute( f"INSERT INTO {sep_tbl}(rowid, embedding) VALUES (?, ?)", (new_id, serialize_f32(pre_sep_embedding)), ) - _write_embedder_metadata(self.conn) - except Exception: + _write_foreground_embedder_metadata_no_commit(self.conn, pre_identity, selection=write_selection) + except VectorPublicationChanged: + raise + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.warning("Failed to store embedding for message %s during add()", new_id, exc_info=True) # Incrementally update entity profile. @@ -785,17 +1174,20 @@ def add( try: from truememory.personality import update_entity_profile_incremental update_entity_profile_incremental(self.conn, sender, content, recipient) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.debug("Failed to update entity profile for %s during add()", sender, exc_info=True) - # Store pre-computed style vector (DB write only, computation happened outside lock) - if pre_style_vec is not None and not directive: - try: - _update_style_vec(self.conn, sender, content, _pre_computed_vec=pre_style_vec) - except Exception: - logger.debug("Failed to update style vector for %s during add()", sender, exc_info=True) - - self.conn.commit() + if self._has_style_vec: + style_result = _publish_style_append( + self.conn, style_before, new_id, sender=sender, content=content, directive=directive, + precomputed=pre_style_vec is not None, + publish=lambda: _update_style_vec(self.conn, sender, content, _pre_computed_vec=pre_style_vec), + ) + if style_result.error_category is not None: + logger.debug("Style append %s for message %s: %s", + style_result.outcome, new_id, style_result.error_category) self._maybe_auto_consolidate() @@ -810,40 +1202,91 @@ def add( "metadata": metadata or {}, } + def _get_maintenance_coordinator(self) -> MaintenanceCoordinator: + from truememory.maintenance import get_coordinator + coordinator = self._maintenance_coordinator + if coordinator is not None: + try: + coordinator.status + except RuntimeError: + coordinator = None + if coordinator is None or self._maintenance_handle is not self.conn: + coordinator = get_coordinator(self.db_path) + coordinator.refresh_capabilities() + self._maintenance_coordinator = coordinator + self._maintenance_handle = self.conn + return coordinator + def _maybe_auto_consolidate(self) -> None: - """Trigger background consolidation after threshold adds.""" - self._adds_since_consolidation += 1 - if self._adds_since_consolidation < self._auto_consolidate_threshold: + """Probe fixed metadata without waiting on foreground or model work.""" + from truememory.tier_switch.runtime import current_operation + operation = current_operation(self.conn) if self.conn is not None else None + if operation is not None and getattr(operation, "_defer_maintenance", False): return - if not self._has_consolidation: + if (not self._has_consolidation and not getattr(self, "_has_style_vec", False)) or self.conn is None: return - if (self._consolidation_thread is not None - and self._consolidation_thread.is_alive()): + if not self._write_lock.acquire(blocking=False): + self._maintenance_pending_reason = "foreground_busy" return - self._adds_since_consolidation = 0 - self._consolidation_thread = threading.Thread( - target=self._bg_consolidate, - daemon=True, - name="auto-consolidate", - ) - self._consolidation_thread.start() + coordinator = None + eligible = () + style_eligible = False + try: + if self.conn is None: + return + if self.conn.in_transaction: + self._maintenance_pending_reason = "pending_caller_commit" + return + from truememory.maintenance import engine_layer_specs, observe_style, plan_layers + coordinator = self._get_maintenance_coordinator() + if getattr(self, "_has_style_vec", False): + observation = observe_style(self.conn, threshold=self._auto_consolidate_threshold) + style_eligible = observation.eligible + self._maintenance_pending_reason = observation.health["pending_reason"] + if self._has_consolidation: + eligible = plan_layers(self.conn, engine_layer_specs(self.conn, coordinator), + threshold=self._auto_consolidate_threshold) + if not getattr(self, "_has_style_vec", False): + self._maintenance_pending_reason = "awaiting_eligibility" + if eligible or style_eligible: + self._maintenance_pending_reason = "eligible" + except Exception as error: + self._maintenance_pending_reason = type(error).__name__[:64] + finally: + self._write_lock.release() + if (eligible or style_eligible) and coordinator is not None: + try: + initialization = ({"_initialization_receipt": self._reconnect_receipt} + if getattr(self, "_reconnect_receipt", None) is not None else {}) + if style_eligible: + coordinator.request_layers(threshold=self._auto_consolidate_threshold, + include_layers=bool(eligible), include_style=True, **initialization) + else: + coordinator.request_layers(threshold=self._auto_consolidate_threshold, **initialization) + except Exception as error: + self._maintenance_pending_reason = type(error).__name__[:64] def _bg_consolidate(self) -> None: - """Run consolidation in a background thread with its own connection.""" - try: - self.consolidate() - except Exception: - logger.debug("Auto-consolidation failed", exc_info=True) + """Compatibility entry point; the coordinator owns the worker handle.""" + self._maybe_auto_consolidate() + @engine_operation def delete(self, memory_id: int) -> bool: """Delete a memory by ID. Returns True if deleted, False if not found. """ self._ensure_connection() - with self._write_lock: - return delete_message(self.conn, memory_id) - + from truememory.tier_switch.writer import capture_writer_selection, writer_transaction + write_selection = capture_writer_selection(self.conn) + with self._write_lock, writer_transaction(self.conn, write_selection): + deleted = delete_message(self.conn, memory_id, commit=False) + if deleted: + self._maybe_auto_consolidate() + return deleted + + @_validate_delete_user + @engine_operation def delete_all(self, user_id: str | None = None) -> bool: """Delete all memories, optionally filtered by user. @@ -860,14 +1303,11 @@ def delete_all(self, user_id: str | None = None) -> bool: Returns: True if any rows were deleted from messages. """ - if user_id is not None and not isinstance(user_id, str): - raise TypeError(f"user_id must be a string or None, got {type(user_id).__name__}") - if isinstance(user_id, str) and not user_id.strip(): - raise ValueError("user_id cannot be an empty string") - self._ensure_connection() + from truememory.tier_switch.writer import capture_writer_selection, writer_transaction + write_selection = capture_writer_selection(self.conn) - with self._write_lock: + with self._write_lock, writer_transaction(self.conn, write_selection): if user_id is not None: # Get message IDs and episode IDs for this user before deleting msg_ids = [ @@ -898,7 +1338,9 @@ def delete_all(self, user_id: str | None = None) -> bool: raise ValueError(f"Invalid column name: {col}") try: _delete_in_chunks(self.conn, table, col, msg_ids) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.warning("Failed to clean %s for user %s", table, user_id, exc_info=True) # Clean entity profile for this user (normalize to lowercase #467) @@ -906,7 +1348,9 @@ def delete_all(self, user_id: str | None = None) -> bool: self.conn.execute( "DELETE FROM entity_profiles WHERE entity = ?", (user_id.lower(),) ) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.warning("Failed to clean entity_profiles for user %s", user_id, exc_info=True) # Clean entity style vectors for this user (normalize to lowercase #467) @@ -914,7 +1358,9 @@ def delete_all(self, user_id: str | None = None) -> bool: self.conn.execute( "DELETE FROM entity_style_vectors WHERE entity = ?", (user_id.lower(),) ) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.warning("Failed to clean entity_style_vectors for user %s", user_id, exc_info=True) # Clean entity relationships involving this user (normalize to lowercase #467) @@ -923,7 +1369,9 @@ def delete_all(self, user_id: str | None = None) -> bool: "DELETE FROM entity_relationships WHERE entity_a = ? OR entity_b = ?", (user_id.lower(), user_id.lower()), ) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.warning("Failed to clean entity_relationships for user %s", user_id, exc_info=True) # Clean summaries scoped to this user @@ -931,27 +1379,38 @@ def delete_all(self, user_id: str | None = None) -> bool: self.conn.execute( "DELETE FROM summaries WHERE entity = ?", (user_id.lower(),) ) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.warning("Failed to clean summaries for user %s", user_id, exc_info=True) if episode_ids: try: _delete_in_chunks(self.conn, "episodes", "id", episode_ids) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.warning("Failed to clean episodes for user %s", user_id, exc_info=True) # Clean vector tables for deleted message IDs if msg_ids: - try: - vec_tbl, sep_tbl = _resolve_vec_tables(self.conn) - except Exception: - logger.warning("_resolve_vec_tables failed in delete_all(user_id=%s); falling back to legacy table names", user_id, exc_info=True) - vec_tbl, sep_tbl = "vec_messages", "vec_messages_sep" + if write_selection.selection is not None: + vec_tbl, sep_tbl = write_selection.selection.tables + else: + try: + vec_tbl, sep_tbl = _resolve_vec_tables(self.conn) + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) + logger.warning("_resolve_vec_tables failed in delete_all(user_id=%s); falling back to legacy table names", user_id, exc_info=True) + vec_tbl, sep_tbl = "vec_messages", "vec_messages_sep" # dict.fromkeys deduplicates in case vec_tbl == sep_tbl (shouldn't happen, but defensive) for vec_table in dict.fromkeys((vec_tbl, sep_tbl)): try: _delete_in_chunks(self.conn, vec_table, "rowid", msg_ids) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.warning("Failed to clean %s for user %s", vec_table, user_id, exc_info=True) # Remove orphaned cluster_centroids (clusters with no @@ -961,7 +1420,9 @@ def delete_all(self, user_id: str | None = None) -> bool: "DELETE FROM cluster_centroids WHERE cluster_id NOT IN " "(SELECT DISTINCT cluster_id FROM message_clusters)" ) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.warning("Failed to clean cluster_centroids for user %s", user_id, exc_info=True) # Delete parent rows AFTER all child FK references are gone @@ -989,14 +1450,18 @@ def delete_all(self, user_id: str | None = None) -> bool: raise ValueError(f"Invalid table name: {table}") try: self.conn.execute(f"DELETE FROM {table}") - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.warning("Failed to clear table %s during delete_all", table, exc_info=True) # Clear ALL known vector tables across all tiers. for vec_table in _ALL_VEC_TABLES: try: self.conn.execute(f"DELETE FROM {vec_table}") - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.debug("Failed to clear %s during delete_all (table may not exist)", vec_table, exc_info=True) cursor = self.conn.execute("DELETE FROM messages") @@ -1005,156 +1470,91 @@ def delete_all(self, user_id: str | None = None) -> bool: # Rebuild FTS index try: self.conn.execute("INSERT INTO messages_fts(messages_fts) VALUES('rebuild')") - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.warning("Failed to rebuild FTS index during delete_all", exc_info=True) - self.conn.commit() - return deleted - - def consolidate(self) -> dict[str, str]: - """Run all consolidation layers (L0-L5) under the write lock. - - Returns timing stats for each step. - """ - import time as _time - - self._ensure_connection() - stats: dict[str, str] = {} - - try: - from truememory.consolidation import ( - build_summaries, - detect_contradictions, - build_structured_facts, - ) - except (ImportError, ModuleNotFoundError): - stats["consolidation"] = "SKIPPED (module not available)" - return stats - - try: - from truememory.predictive import build_surprise_index - except (ImportError, ModuleNotFoundError): - build_surprise_index = None - - try: - from truememory.temporal import detect_episodes, detect_landmark_events - except (ImportError, ModuleNotFoundError): - detect_episodes = None - detect_landmark_events = None - - try: - from truememory.personality import build_dunbar_hierarchy - except (ImportError, ModuleNotFoundError): - build_dunbar_hierarchy = None - - _cluster_messages = None - if _HAS_CLUSTERING and self._has_vectors: - try: - from truememory.clustering import cluster_messages as _cm - _cluster_messages = _cm - except (ImportError, ModuleNotFoundError): - pass - - _extract_preferences = None - if _HAS_PERSONALITY: - try: - from truememory.personality import extract_preferences as _ep - _extract_preferences = _ep - except (ImportError, ModuleNotFoundError): - pass - - with self._write_lock: - if _cluster_messages: - try: - t0 = _time.time() - n = _cluster_messages(self.conn) - stats["cluster_messages"] = f"{n} clusters in {_time.time() - t0:.3f}s" - self._has_clustering = True - except Exception as exc: - stats["cluster_messages"] = f"ERROR: {exc}" - - if _extract_preferences: - try: - t0 = _time.time() - _extract_preferences(self.conn) - stats["extract_preferences"] = f"{_time.time() - t0:.3f}s" - except Exception as exc: - stats["extract_preferences"] = f"ERROR: {exc}" - - try: - t0 = _time.time() - build_summaries(self.conn) - stats["build_summaries"] = f"{_time.time() - t0:.3f}s" - except Exception as exc: - stats["build_summaries"] = f"ERROR: {exc}" + if deleted: + self._maybe_auto_consolidate() + return deleted - try: - t0 = _time.time() - detect_contradictions(self.conn) - stats["detect_contradictions"] = f"{_time.time() - t0:.3f}s" - except Exception as exc: - stats["detect_contradictions"] = f"ERROR: {exc}" + def _apply_manual_maintenance_capabilities(self, results: tuple[LayerResult, ...]) -> None: + completed = {result.layer for result in results + if result.outcome in {"success", "success_empty"}} + if "summaries" in completed: + self._has_consolidation = True + if "clusters" in completed: + self._has_clustering = True + def consolidate(self) -> dict[str, str]: + """Force one pass, preserving caller transactions and shared ownership.""" + from truememory.maintenance import ( + MaintenanceBusyError, format_maintenance_report, maintenance_busy_result, + maintenance_observation, observe_style, run_engine_maintenance, _style_unready_result, + ) + from truememory.tier_switch.runtime import _read_selection, _read_policy, _connection_read + self._open_connection_handle() + with _connection_read(self.conn, self._write_lock, None, None): + controlled = _read_selection(self.conn) is not None or _read_policy(self.conn) is not None + if controlled: + self._has_style_vec = self.conn.execute( + "SELECT 1 FROM main.sqlite_master WHERE type='table' AND name='entity_style_vectors' COLLATE NOCASE" + ).fetchone() is not None + if not controlled: + self._ensure_connection(_suppress_maintenance=True) + with _connection_read(self.conn, self._write_lock, None, None): + coordinator = self._get_maintenance_coordinator() + coordinator.refresh_capabilities() + borrowed = self.conn.in_transaction + foreground_style = None + if not borrowed and coordinator.path is not None and getattr(self, "_has_style_vec", False): + observation = observe_style(self.conn, threshold=self._auto_consolidate_threshold) + if observation.state is None: + foreground_style = _style_unready_result(observation.health) + if borrowed or coordinator.path is None: try: - t0 = _time.time() - n = build_structured_facts(self.conn) - stats["structured_facts"] = f"{n} facts in {_time.time() - t0:.3f}s" - except Exception as exc: - stats["structured_facts"] = f"ERROR: {exc}" - - if build_surprise_index: - try: - t0 = _time.time() - build_surprise_index(self.conn) - stats["build_surprise_index"] = f"{_time.time() - t0:.3f}s" - except Exception as exc: - stats["build_surprise_index"] = f"ERROR: {exc}" - - if detect_episodes: - try: - t0 = _time.time() - ep = detect_episodes(self.conn) - stats["detect_episodes"] = f"{ep} episodes in {_time.time() - t0:.3f}s" - except Exception as exc: - stats["detect_episodes"] = f"ERROR: {exc}" - - if detect_landmark_events: - try: - t0 = _time.time() - lm = detect_landmark_events(self.conn) - stats["detect_landmarks"] = f"{lm} events in {_time.time() - t0:.3f}s" - except Exception as exc: - stats["detect_landmarks"] = f"ERROR: {exc}" - - if build_dunbar_hierarchy: + with maintenance_observation(coordinator, borrowed=borrowed) as observed: + report = run_engine_maintenance(self.conn, coordinator, force=True, + threshold=self._auto_consolidate_threshold, prepare_extensions=False, + _connection_lock=self._write_lock, allow_caller_transaction=borrowed, include_style=getattr(self, "_has_style_vec", False)) + observed.append(report) + except MaintenanceBusyError: + return maintenance_busy_result() + self._apply_manual_maintenance_capabilities(report.results) + return format_maintenance_report(report) + try: + with maintenance_observation(coordinator) as observed: + from truememory.tier_switch.runtime import open_serving_connection + connection = open_serving_connection(coordinator.path, create_db) try: - t0 = _time.time() - primary = None - try: - row = self.conn.execute( - "SELECT sender, COUNT(*) as cnt FROM messages " - "WHERE sender != '' AND sender IS NOT NULL " - "GROUP BY sender ORDER BY cnt DESC LIMIT 1" - ).fetchone() - if row and row[0] and row[0].strip(): - primary = row[0] - except Exception: - pass - result = build_dunbar_hierarchy(self.conn, primary_entity=primary) - n_rel = len(result) if isinstance(result, dict) else result - stats["dunbar_hierarchy"] = f"{n_rel} relationships in {_time.time() - t0:.3f}s" - except Exception as exc: - stats["dunbar_hierarchy"] = f"ERROR: {exc}" - - self.conn.commit() - - self._has_consolidation = True - return stats - - def update(self, memory_id: int, content: str | None = None, **fields) -> dict | None: + report = run_engine_maintenance(connection, coordinator, force=True, + threshold=self._auto_consolidate_threshold, + include_style=getattr(self, "_has_style_vec", False) and foreground_style is None, + connection_owned=True) + if foreground_style is not None: + report = report._replace(style_result=foreground_style) + if connection.in_transaction: + connection.rollback() + raise RuntimeError("Maintenance work left an unfinished transaction") + observed.append(report) + finally: + connection.close() + except MaintenanceBusyError: + return maintenance_busy_result() + with self._write_lock: + self._apply_manual_maintenance_capabilities(report.results) + return format_maintenance_report(report) + + @engine_operation + def update( + self, memory_id: int, content: str | None = None, *, + _expected_source: dict | None = None, **fields, + ) -> dict | None: """Update a memory. Re-embeds the message if ``content`` changes. + Sender, recipient and timestamp changes refresh only the SEP vector. Args: memory_id: The memory to update. @@ -1166,73 +1566,184 @@ def update(self, memory_id: int, content: str | None = None, **fields) -> dict | Updated memory dict, or None if not found. """ self._ensure_connection() + from truememory.tier_switch.writer import capture_writer_selection, writer_transaction + write_selection = capture_writer_selection(self.conn) + + def read_snapshot() -> dict | None: + import hashlib + import json + from truememory.storage import _row_to_dict, select_message_cols + row = self.conn.execute( + f"SELECT {select_message_cols(self.conn)} FROM messages WHERE id = ?", + (memory_id,), + ).fetchone() + if row is None: + return None + try: + raw = json.dumps(list(row), ensure_ascii=True, allow_nan=False, + separators=(",", ":")).encode("utf-8") + except (TypeError, ValueError, UnicodeError) as exc: + raise sqlite3.OperationalError("Source snapshot contains unsupported stored values") from exc + current = _row_to_dict(row) + current["_source_snapshot_sha256"] = hashlib.sha256(raw).hexdigest() + return current + + def require_source(expected: dict) -> None: + import json + current = read_snapshot() + names = ("id", "content", "sender", "recipient", "timestamp", "category", + "modality", "directive", "metadata", "_source_snapshot_sha256") + try: + same = (type(expected) is dict and current is not None + and all(name in expected for name in names) + and json.dumps([expected[name] for name in names], sort_keys=True, allow_nan=False) + == json.dumps([current[name] for name in names], sort_keys=True, allow_nan=False)) + except (TypeError, ValueError, KeyError) as exc: + raise sqlite3.OperationalError("Invalid expected source snapshot") from exc + if not same: + raise sqlite3.OperationalError("Source changed before update; retry from a fresh snapshot") + + pre_source = None + sep_fields_changed = False + if content is None and self._has_vectors and any( + name in fields for name in ("sender", "recipient", "timestamp") + ): + with self._write_lock: + pre_source = read_snapshot() + if pre_source is not None: + sep_fields_changed = any( + name in fields and fields[name] != pre_source[name] + for name in ("sender", "recipient", "timestamp") + ) pre_embedding = None pre_sep_embedding = None - if content is not None and self._has_vectors: + if self._has_vectors and (content is not None or sep_fields_changed): + from truememory.vector_search import VectorPublicationChanged try: from truememory.vector_search import ( - get_model, _encode_with_mps_fallback, _build_sep_text, + _capture_rebuild_model, _encode_with_mps_fallback, _build_sep_text, ) - model = get_model() - pre_embedding = _encode_with_mps_fallback(model, [content])[0] - row = self.conn.execute( - "SELECT sender, recipient, timestamp FROM messages WHERE id = ?", - (memory_id,), - ).fetchone() + with self._write_lock: + model, pre_identity = _capture_rebuild_model(self.conn) + if content is not None: + pre_embedding = _encode_with_mps_fallback(model, [content])[0] + row = self.conn.execute( + "SELECT sender, recipient, timestamp FROM messages WHERE id = ?", + (memory_id,), + ).fetchone() + else: + row = (pre_source["sender"], pre_source["recipient"], pre_source["timestamp"]) if row: sender_val = fields.get("sender", row[0]) recipient_val = fields.get("recipient", row[1]) timestamp_val = fields.get("timestamp", row[2]) - sep_text = _build_sep_text(sender_val, recipient_val, timestamp_val, content) + sep_text = _build_sep_text(sender_val, recipient_val, timestamp_val, + content if content is not None else pre_source["content"]) pre_sep_embedding = _encode_with_mps_fallback(model, [sep_text])[0] - except Exception: + except VectorPublicationChanged: + raise + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) + if sep_fields_changed: + raise sqlite3.OperationalError("SEP pre-computation failed; source update not committed") from _serving_error logger.debug("Failed to pre-compute embedding during update()", exc_info=True) - with self._write_lock: - if content is not None: - fields["content"] = content - - ok = update_message(self.conn, memory_id, **fields) - if not ok: - return None - - if pre_embedding is not None: - try: - from truememory.vector_search import ( - serialize_f32, _active_vec_table, _active_sep_table, - _write_embedder_metadata, - ) - vec_tbl = _active_vec_table(self.conn) - try: - self.conn.execute(f"DELETE FROM {vec_tbl} WHERE rowid = ?", (memory_id,)) - except Exception: - logger.debug("Failed to delete old vector embedding for message %d", memory_id, exc_info=True) - self.conn.execute( - f"INSERT INTO {vec_tbl}(rowid, embedding) VALUES (?, ?)", - (memory_id, serialize_f32(pre_embedding)), - ) - if pre_sep_embedding is not None: - sep_tbl = _active_sep_table(self.conn) - try: + source_committed = False + try: + with self._write_lock: + if content is not None: + fields["content"] = content + + with writer_transaction( + self.conn, write_selection, + model_id=pre_identity[1] if pre_embedding is not None or pre_sep_embedding is not None else None, + dimension=pre_identity[2] if pre_embedding is not None or pre_sep_embedding is not None else None, + ): + if _expected_source is not None: + require_source(_expected_source) + if sep_fields_changed: + require_source(pre_source) + ok = update_message(self.conn, memory_id, commit=False, **fields) + if ok and sep_fields_changed: + from truememory.vector_search import _foreground_vector_publication, serialize_f32 + # Keep the source and field-only SEP correction in one + # transaction; a failed publication retains both old rows. + with _foreground_vector_publication( + self.conn, pre_identity, memory_id, selection=write_selection, + ) as (_, sep_tbl): self.conn.execute(f"DELETE FROM {sep_tbl} WHERE rowid = ?", (memory_id,)) - except Exception: - logger.debug("Failed to delete old sep vector for message %d", memory_id, exc_info=True) - self.conn.execute( - f"INSERT INTO {sep_tbl}(rowid, embedding) VALUES (?, ?)", - (memory_id, serialize_f32(pre_sep_embedding)), - ) - _write_embedder_metadata(self.conn) - except Exception: - logger.warning("Vector embedding failed for message %d", memory_id, exc_info=True) + self.conn.execute( + f"INSERT INTO {sep_tbl}(rowid, embedding) VALUES (?, ?)", + (memory_id, serialize_f32(pre_sep_embedding)), + ) + source_committed = ok and not self.conn.in_transaction + if not ok: + return None - self.conn.commit() + if pre_embedding is not None: + from truememory.vector_search import VectorPublicationChanged, _foreground_vector_publication + try: + from truememory.vector_search import serialize_f32, _write_foreground_embedder_metadata_no_commit + with _foreground_vector_publication( + self.conn, pre_identity, memory_id, selection=write_selection, + ) as (vec_tbl, sep_tbl): + try: + self.conn.execute(f"DELETE FROM {vec_tbl} WHERE rowid = ?", (memory_id,)) + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) + logger.debug("Failed to delete old vector embedding for message %d", memory_id, exc_info=True) + self.conn.execute( + f"INSERT INTO {vec_tbl}(rowid, embedding) VALUES (?, ?)", + (memory_id, serialize_f32(pre_embedding)), + ) + if pre_sep_embedding is not None: + try: + self.conn.execute(f"DELETE FROM {sep_tbl} WHERE rowid = ?", (memory_id,)) + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) + logger.debug("Failed to delete old sep vector for message %d", memory_id, exc_info=True) + self.conn.execute( + f"INSERT INTO {sep_tbl}(rowid, embedding) VALUES (?, ?)", + (memory_id, serialize_f32(pre_sep_embedding)), + ) + _write_foreground_embedder_metadata_no_commit(self.conn, pre_identity, selection=write_selection) + except VectorPublicationChanged as exc: + raise VectorPublicationChanged("Source update committed; retry update to publish vectors") from exc + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) + logger.warning("Vector embedding failed for message %d", memory_id, exc_info=True) + finally: + if source_committed: + self._maybe_auto_consolidate() return self.get(memory_id) - def get(self, memory_id: int) -> dict | None: + def get(self, memory_id: int, *, _source_snapshot: bool = False) -> dict | None: """Retrieve a single memory by ID.""" self._ensure_connection() + if _source_snapshot: + import hashlib + import json + from truememory.storage import _row_to_dict, select_message_cols + row = self.conn.execute( + f"SELECT {select_message_cols(self.conn)} FROM messages WHERE id = ?", + (memory_id,), + ).fetchone() + if row is None: + return None + result = _row_to_dict(row) + try: + raw_snapshot = json.dumps(list(row), ensure_ascii=True, allow_nan=False, + separators=(",", ":")).encode("utf-8") + except (TypeError, ValueError, UnicodeError) as exc: + raise sqlite3.OperationalError("Source snapshot contains unsupported stored values") from exc + result["_source_snapshot_sha256"] = hashlib.sha256(raw_snapshot).hexdigest() + return result return get_message(self.conn, memory_id) def get_all(self, limit: int = 100, offset: int = 0, user_id: str | None = None) -> list[dict]: @@ -1285,140 +1796,141 @@ def open(self, rebuild_vectors: bool = True) -> "TrueMemoryEngine": if not self.db_path.exists(): raise FileNotFoundError(f"Database not found: {self.db_path}") - from truememory.storage import _validate_db_path - self.conn = sqlite3.connect( - _validate_db_path(self.db_path), check_same_thread=False - ) - self.conn.row_factory = None # Use default tuple rows - self.conn.execute("PRAGMA journal_mode=WAL") - self.conn.execute(f"PRAGMA busy_timeout={DEFAULT_BUSY_TIMEOUT_MS}") - self.conn.execute("PRAGMA foreign_keys=ON") - self.conn.execute("PRAGMA synchronous=NORMAL") - self.conn.execute("PRAGMA cache_size=-64000") - self.conn.execute("PRAGMA mmap_size=268435456") - - # Detect available tables - tables = { - row[0] - for row in self.conn.execute( - "SELECT name FROM sqlite_master WHERE type='table'" - ).fetchall() - } - - # ── MEMORIST-L4 migration: purge legacy entity_profile summary rows ─ - # Shared with _ensure_connection (M-84): the purge logic lives in one - # idempotent helper so both the production path and this deprecated - # open() path behave identically. - self._purge_legacy_entity_profile_summaries() + from truememory.tier_switch.runtime import open_serving_connection - # Style vector hash migration: Python's hash() was replaced with a - # stable hashlib-based hash in v0.6.3. Existing style vectors were - # computed with the old non-deterministic hash and must be rebuilt. - if "entity_style_vectors" in tables and "metadata" in tables: - try: - row = self.conn.execute( - "SELECT value FROM metadata WHERE key = 'style_vec_hash_version'" - ).fetchone() - if row is None or row[0] != "2": - if _HAS_STYLE_VEC: - from truememory.personality_style_vec import build_entity_style_vectors - build_entity_style_vectors(self.conn) - logger.info("Style vectors rebuilt with stable hash (one-time migration)") - self.conn.execute( - "INSERT OR REPLACE INTO metadata (key, value) VALUES (?, ?)", - ("style_vec_hash_version", "2"), - ) - self.conn.commit() - except Exception: - logger.debug("Style vector migration failed", exc_info=True) - - # Load sqlite-vec extension if available. - # upgrade DEBUG → WARNING and track failure in a - # module-level state so ``truememory_stats.health`` can report - # "search is FTS-only because sqlite-vec failed to load". - global _vectors_load_error - if _HAS_VECTOR: + def open_existing(path: Path) -> sqlite3.Connection: + from truememory.storage import _validate_db_path + uri = Path(_validate_db_path(path)).as_uri() + "?mode=rw" + candidate = sqlite3.connect(uri, uri=True, check_same_thread=False) try: - import sqlite_vec - self.conn.enable_load_extension(True) - sqlite_vec.load(self.conn) - _vectors_load_error = None - except Exception as e: - _vectors_load_error = f"{type(e).__name__}: {e}" - logger.warning( - "sqlite-vec unavailable (%s); falling back to FTS-only " - "search for this process. See " - "https://github.com/buildingjoshbetter/TrueMemory for " - "platform notes.", - _vectors_load_error, - ) + candidate.row_factory = None + candidate.execute("PRAGMA journal_mode=WAL") + candidate.execute(f"PRAGMA busy_timeout={DEFAULT_BUSY_TIMEOUT_MS}") + candidate.execute("PRAGMA foreign_keys=ON") + candidate.execute("PRAGMA synchronous=NORMAL") + candidate.execute("PRAGMA cache_size=-64000") + candidate.execute("PRAGMA mmap_size=268435456") + return candidate + except BaseException: + candidate.close() + raise + + self.conn = open_serving_connection(self.db_path, open_existing) + + from truememory.tier_switch.runtime import serving_operation + with serving_operation(self.conn, connection_lock=self._write_lock) as operation: + if operation.selection is not None or operation.policy is not None: + self._initialize_connection() + self.stats["message_count"] = get_message_count(self.conn) + return self + # Detect available tables + tables = { + row[0] + for row in self.conn.execute( + "SELECT name FROM sqlite_master WHERE type='table'" + ).fetchall() + } - # Check for vector tables — rebuild if missing. - # if metadata names a different embedder, refuse silent - # rebuild; route the user through truememory_configure() instead. - self._has_vectors = False - if _HAS_VECTOR: - from truememory.vector_search import ( - _active_vec_table, - _check_embedder_compatibility, - vectors_are_built, - ) - vec_tbl = _active_vec_table(self.conn) - # vectors_are_built() returns False for a table left ``in_progress`` - # by an interrupted build (issue #647) — so a partial/empty table is - # rebuilt rather than trusted, not just one that's missing. - if vectors_are_built(self.conn, vec_tbl): - # M-12: the exists-path previously trusted the table on a bare - # SELECT 1 and never checked embedder/dim compatibility. A dim - # mismatch then surfaced only as a per-query OperationalError - # caught at DEBUG → permanent silent FTS-only with - # _vectors_load_error=None (health reports vectors healthy) and - # silent vec-INSERT failures in add(). Run the compatibility - # check here so a mismatch surfaces as actionable migration - # guidance instead. + # ── MEMORIST-L4 migration: purge legacy entity_profile summary rows ─ + # Shared with _ensure_connection (M-84): the purge logic lives in one + # idempotent helper so both the production path and this deprecated + # open() path behave identically. + self._purge_legacy_entity_profile_summaries() + + # Load sqlite-vec extension if available. + # upgrade DEBUG → WARNING and track failure in a + # module-level state so ``truememory_stats.health`` can report + # "search is FTS-only because sqlite-vec failed to load". + global _vectors_load_error + if _HAS_VECTOR: try: - _check_embedder_compatibility(self.conn) - self._has_vectors = True - except TrueMemoryMigrationError as exc: - _vectors_load_error = f"{type(exc).__name__}: {exc}" - self._has_vectors = False - raise - else: - logger.warning( - "Vector table %r missing or unreadable; attempting rebuild " - "with current model=%s", - vec_tbl, - resolve_tier(), + import sqlite_vec + self.conn.enable_load_extension(True) + sqlite_vec.load(self.conn) + _vectors_load_error = None + except Exception as e: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(e) + _vectors_load_error = f"{type(e).__name__}: {e}" + logger.warning( + "sqlite-vec unavailable (%s); falling back to FTS-only " + "search for this process. See " + "https://github.com/buildingjoshbetter/TrueMemory for " + "platform notes.", + _vectors_load_error, + ) + + # Check for vector tables — rebuild if missing. + # if metadata names a different embedder, refuse silent + # rebuild; route the user through truememory_configure() instead. + self._has_vectors = False + if _HAS_VECTOR: + from truememory.vector_search import ( + _active_vec_table, + _check_embedder_compatibility, + vectors_are_built, ) - if rebuild_vectors: - _check_rebuild_allowed(self.conn) # raises on model drift + vec_tbl = _active_vec_table(self.conn) + # vectors_are_built() returns False for a table left ``in_progress`` + # by an interrupted build (issue #647) — so a partial/empty table is + # rebuilt rather than trusted, not just one that's missing. + if vectors_are_built(self.conn, vec_tbl): + # M-12: the exists-path previously trusted the table on a bare + # SELECT 1 and never checked embedder/dim compatibility. A dim + # mismatch then surfaced only as a per-query OperationalError + # caught at DEBUG → permanent silent FTS-only with + # _vectors_load_error=None (health reports vectors healthy) and + # silent vec-INSERT failures in add(). Run the compatibility + # check here so a mismatch surfaces as actionable migration + # guidance instead. try: - init_vec_table(self.conn) - n = build_vectors(self.conn) - self._has_vectors = n > 0 - logger.info( - "Vector table %r rebuilt with %d vectors (model=%s)", - vec_tbl, n, resolve_tier(), - ) - except TrueMemoryMigrationError: - raise - except Exception: - logger.exception("Vector table rebuild failed") + _check_embedder_compatibility(self.conn) + self._has_vectors = True + except TrueMemoryMigrationError as exc: + _vectors_load_error = f"{type(exc).__name__}: {exc}" self._has_vectors = False + raise + else: + logger.warning( + "Vector table %r missing or unreadable; attempting rebuild " + "with current model=%s", + vec_tbl, + resolve_tier(), + ) + if rebuild_vectors: + _check_rebuild_allowed(self.conn) # raises on model drift + try: + init_vec_table(self.conn) + n = build_vectors(self.conn) + self._has_vectors = n > 0 + logger.info( + "Vector table %r rebuilt with %d vectors (model=%s)", + vec_tbl, n, resolve_tier(), + ) + except TrueMemoryMigrationError: + raise + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) + logger.exception("Vector table rebuild failed") + self._has_vectors = False - self._has_hybrid = _HAS_HYBRID and self._has_vectors - self._has_clustering = _HAS_CLUSTERING and "message_clusters" in tables + self._has_hybrid = _HAS_HYBRID and self._has_vectors + self._has_clustering = _HAS_CLUSTERING and "message_clusters" in tables - # Count messages for stats - try: - count = self.conn.execute("SELECT COUNT(*) FROM messages").fetchone()[0] - self.stats["message_count"] = count - except Exception: - logger.debug("Failed to count messages in open()", exc_info=True) - self.stats["message_count"] = 0 + # Count messages for stats + try: + count = self.conn.execute("SELECT COUNT(*) FROM messages").fetchone()[0] + self.stats["message_count"] = count + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) + logger.debug("Failed to count messages in open()", exc_info=True) + self.stats["message_count"] = 0 - self.ready = True - return self + self.ready = True + self._maybe_auto_consolidate() + return self # ────────────────────────────────────────────────────────────────────── # Ingestion @@ -1433,7 +1945,7 @@ def ingest(self, data_path: str | Path) -> dict: 2. Load messages from JSON file 3. Build vector embeddings (Model2Vec -> sqlite-vec) 4. Build entity profiles (L0) - 5. Extract preferences (L0) + 5. Report unavailable scheduled preference refresh (L0) 6. Build summaries (L5) 7. Detect contradictions (L5) 8. Build surprise index (predictive coding) @@ -1443,273 +1955,322 @@ def ingest(self, data_path: str | Path) -> dict: engine still works with FTS5 only. Returns: - Dict mapping step names to timing strings (or error messages). + Dict mapping step names to timing strings or explicit status messages. """ - data_path = Path(data_path) - stats: dict = {} + from truememory.tier_switch.runtime import destructive_ingest_operation + with destructive_ingest_operation(self): + data_path = Path(data_path) + stats: dict = {} - # ── 1. Create database ──────────────────────────────────────────── - try: - t0 = time.time() - # Remove stale database so every run is clean. - if self.db_path.exists() and str(self.db_path) != ":memory:": - self.db_path.unlink() - self.conn = create_db(self.db_path) - stats["create_db"] = f"{time.time() - t0:.3f}s" - except Exception as exc: - stats["create_db"] = f"ERROR: {exc}" - logger.warning("create_db failed", exc_info=True) - return stats # Can't continue without a database. - - # ── 2. Load messages ────────────────────────────────────────────── - try: - t0 = time.time() - msg_count = load_messages_from_file(self.conn, data_path) - elapsed = time.time() - t0 - stats["load_messages"] = f"{msg_count} messages in {elapsed:.3f}s" - self.stats["message_count"] = msg_count - except Exception as exc: - stats["load_messages"] = f"ERROR: {exc}" - logger.warning("load_messages failed", exc_info=True) - self.stats["message_count"] = 0 - - # ── 3. Build vector embeddings ──────────────────────────────────── - if _HAS_VECTOR: + # ── 1. Create database ──────────────────────────────────────────── try: t0 = time.time() - init_vec_table(self.conn) - vec_count = build_vectors(self.conn) - elapsed = time.time() - t0 - stats["build_vectors"] = f"{vec_count} vectors in {elapsed:.3f}s" - self._has_vectors = True + # Remove stale database so every run is clean. + if self.db_path.exists() and str(self.db_path) != ":memory:": + self.db_path.unlink() + self.conn = create_db(self.db_path) + stats["create_db"] = f"{time.time() - t0:.3f}s" except Exception as exc: - stats["build_vectors"] = f"ERROR: {exc}" - logger.debug("build_vectors failed", exc_info=True) - self._has_vectors = False - else: - stats["build_vectors"] = "SKIPPED (sqlite-vec or model2vec not installed)" - self._has_vectors = False - - # Update hybrid capability — needs vectors to be useful. - self._has_hybrid = _HAS_HYBRID and self._has_vectors + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(exc) + stats["create_db"] = f"ERROR: {exc}" + logger.warning("create_db failed", exc_info=True) + return stats # Can't continue without a database. - # ── 3b. Build scene clusters ─────────────────────────────────────── - if _HAS_CLUSTERING and self._has_vectors: + # ── 2. Load messages ────────────────────────────────────────────── try: t0 = time.time() - n_clusters = cluster_messages(self.conn) - stats["build_clusters"] = f"{n_clusters} clusters in {time.time() - t0:.3f}s" - self._has_clustering = True + msg_count = load_messages_from_file(self.conn, data_path) + elapsed = time.time() - t0 + stats["load_messages"] = f"{msg_count} messages in {elapsed:.3f}s" + self.stats["message_count"] = msg_count except Exception as exc: - stats["build_clusters"] = f"ERROR: {exc}" - logger.debug("build_clusters failed", exc_info=True) + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(exc) + stats["load_messages"] = f"ERROR: {exc}" + logger.warning("load_messages failed", exc_info=True) + self.stats["message_count"] = 0 + + # ── 3. Build vector embeddings ──────────────────────────────────── + if _HAS_VECTOR: + try: + t0 = time.time() + init_vec_table(self.conn) + vec_count = build_vectors(self.conn) + elapsed = time.time() - t0 + stats["build_vectors"] = f"{vec_count} vectors in {elapsed:.3f}s" + self._has_vectors = True + except Exception as exc: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(exc) + stats["build_vectors"] = f"ERROR: {exc}" + logger.debug("build_vectors failed", exc_info=True) + self._has_vectors = False + else: + stats["build_vectors"] = "SKIPPED (sqlite-vec or model2vec not installed)" + self._has_vectors = False + + # Update hybrid capability — needs vectors to be useful. + self._has_hybrid = _HAS_HYBRID and self._has_vectors + + # ── 3b. Build scene clusters ─────────────────────────────────────── + if _HAS_CLUSTERING and self._has_vectors: + try: + t0 = time.time() + n_clusters = cluster_messages(self.conn) + stats["build_clusters"] = f"{n_clusters} clusters in {time.time() - t0:.3f}s" + self._has_clustering = True + if not self.conn.in_transaction: + self._get_maintenance_coordinator().observe_clustering_outcome( + "success_empty" if n_clusters == 0 else "success") + except Exception as exc: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(exc) + stats["build_clusters"] = f"ERROR: {exc}" + self._get_maintenance_coordinator().observe_clustering_outcome( + "unavailable" if isinstance(exc, ImportError) else "failed", type(exc).__name__) + self._has_clustering = False + else: + stats["build_clusters"] = "SKIPPED (clustering or vectors not available)" + self._get_maintenance_coordinator().observe_clustering_outcome( + "unavailable", "ClusteringRuntimeUnavailable" if not _HAS_CLUSTERING else "VectorRuntimeUnavailable") self._has_clustering = False - else: - stats["build_clusters"] = "SKIPPED (clustering or vectors not available)" - self._has_clustering = False - # ── 4. Build entity profiles (L0) ───────────────────────────────── - if _HAS_PERSONALITY: - try: - t0 = time.time() - build_entity_profiles(self.conn) - stats["build_profiles"] = f"{time.time() - t0:.3f}s" - except Exception as exc: - stats["build_profiles"] = f"ERROR: {exc}" - logger.debug("build_profiles failed", exc_info=True) - self._has_personality = False - else: - stats["build_profiles"] = "SKIPPED (personality module not available)" + # ── 4. Build entity profiles (L0) ───────────────────────────────── + if _HAS_PERSONALITY: + try: + t0 = time.time() + build_entity_profiles(self.conn) + stats["build_profiles"] = f"{time.time() - t0:.3f}s" + except Exception as exc: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(exc) + stats["build_profiles"] = f"ERROR: {exc}" + logger.debug("build_profiles failed", exc_info=True) + self._has_personality = False + else: + stats["build_profiles"] = "SKIPPED (personality module not available)" - # ── 4.5. Build entity style vectors (L0 char-n-gram) ───────────── - if _HAS_STYLE_VEC: - try: - t0 = time.time() - style_vecs = build_entity_style_vectors(self.conn) - stats["build_style_vectors"] = f"{len(style_vecs)} vectors in {time.time() - t0:.3f}s" - self._has_style_vec = True - except Exception as exc: - stats["build_style_vectors"] = f"ERROR: {exc}" - logger.debug("build_style_vectors failed", exc_info=True) - self._has_style_vec = False - else: - stats["build_style_vectors"] = "SKIPPED (personality_style_vec module not available)" + # ── 4.5. Build entity style vectors (L0 char-n-gram) ───────────── + if _HAS_STYLE_VEC: + try: + from truememory.maintenance import maintenance_observation, run_engine_maintenance + coordinator = self._get_maintenance_coordinator() + borrowed = self.conn.in_transaction + with maintenance_observation(coordinator, borrowed=borrowed) as observed: + report = run_engine_maintenance(self.conn, coordinator, force=True, prepare_extensions=False, + include_layers=False, include_style=True, allow_caller_transaction=borrowed) + if not borrowed and self.conn.in_transaction: + self.conn.rollback() + raise RuntimeError("Style work left an unfinished transaction") + observed.append(report) + result = report.style_result + if result.outcome in {"success", "success_empty"}: + stats["build_style_vectors"] = f"{result.output_count} vectors in {result.elapsed_seconds:.3f}s" + else: + stats["build_style_vectors"] = result.outcome.upper() + " (" + (result.error_category or "Pending") + ")" + if result.pending_caller_commit: + stats["build_style_vectors"] += " (pending caller commit)" + self._has_style_vec = True + except Exception as exc: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(exc) + stats["build_style_vectors"] = "ERROR (" + type(exc).__name__[:64] + ")" + logger.debug("build_style_vectors failed", exc_info=True) + else: + stats["build_style_vectors"] = "SKIPPED (personality_style_vec module not available)" - # ── 5. Extract preferences (L0) ─────────────────────────────────── - if _HAS_PERSONALITY: - try: - t0 = time.time() - extract_preferences(self.conn) - stats["extract_preferences"] = f"{time.time() - t0:.3f}s" - except Exception as exc: - stats["extract_preferences"] = f"ERROR: {exc}" - logger.debug("extract_preferences failed", exc_info=True) - else: - stats["extract_preferences"] = "SKIPPED (personality module not available)" + # Scheduled preference maps have no persistence/refresh implementation. + if _HAS_PERSONALITY: + from truememory.maintenance import SCHEDULED_PREFERENCES_UNAVAILABLE + stats["extract_preferences"] = SCHEDULED_PREFERENCES_UNAVAILABLE + else: + stats["extract_preferences"] = "SKIPPED (personality module not available)" - # ── 6. Build summaries (L5) ─────────────────────────────────────── - if _HAS_CONSOLIDATION: - try: - t0 = time.time() - build_summaries(self.conn) - stats["build_summaries"] = f"{time.time() - t0:.3f}s" - except Exception as exc: - stats["build_summaries"] = f"ERROR: {exc}" - logger.debug("build_summaries failed", exc_info=True) - self._has_consolidation = False - else: - stats["build_summaries"] = "SKIPPED (consolidation module not available)" + # ── 6. Build summaries (L5) ─────────────────────────────────────── + if _HAS_CONSOLIDATION: + try: + t0 = time.time() + build_summaries(self.conn) + stats["build_summaries"] = f"{time.time() - t0:.3f}s" + except Exception as exc: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(exc) + stats["build_summaries"] = f"ERROR: {exc}" + logger.debug("build_summaries failed", exc_info=True) + self._has_consolidation = False + else: + stats["build_summaries"] = "SKIPPED (consolidation module not available)" - # ── 7. Detect contradictions (L5) ───────────────────────────────── - if _HAS_CONSOLIDATION: - try: - t0 = time.time() - detect_contradictions(self.conn) - stats["detect_contradictions"] = f"{time.time() - t0:.3f}s" - except Exception as exc: - stats["detect_contradictions"] = f"ERROR: {exc}" - logger.debug("detect_contradictions failed", exc_info=True) - else: - stats["detect_contradictions"] = "SKIPPED (consolidation module not available)" + # ── 7. Detect contradictions (L5) ───────────────────────────────── + if _HAS_CONSOLIDATION: + try: + t0 = time.time() + detect_contradictions(self.conn) + stats["detect_contradictions"] = f"{time.time() - t0:.3f}s" + except Exception as exc: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(exc) + stats["detect_contradictions"] = f"ERROR: {exc}" + logger.debug("detect_contradictions failed", exc_info=True) + else: + stats["detect_contradictions"] = "SKIPPED (consolidation module not available)" - # ── 8. Build surprise index (predictive coding) ─────────────────── - if _HAS_PREDICTIVE: - try: - t0 = time.time() - build_surprise_index(self.conn) - stats["build_surprise_index"] = f"{time.time() - t0:.3f}s" - except Exception as exc: - stats["build_surprise_index"] = f"ERROR: {exc}" - logger.debug("build_surprise_index failed", exc_info=True) - self._has_predictive = False - else: - stats["build_surprise_index"] = "SKIPPED (predictive module not available)" + # ── 8. Build surprise index (predictive coding) ─────────────────── + if _HAS_PREDICTIVE: + try: + t0 = time.time() + build_surprise_index(self.conn) + stats["build_surprise_index"] = f"{time.time() - t0:.3f}s" + except Exception as exc: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(exc) + stats["build_surprise_index"] = f"ERROR: {exc}" + logger.debug("build_surprise_index failed", exc_info=True) + self._has_predictive = False + else: + stats["build_surprise_index"] = "SKIPPED (predictive module not available)" - # ── 9. Build separation vectors (B2) ───────────────────────────── - if _HAS_VECTOR: - try: - t0 = time.time() - sep_count = build_separation_vectors(self.conn) - stats["build_separation_vectors"] = f"{sep_count} vectors in {time.time() - t0:.3f}s" - except Exception as exc: - stats["build_separation_vectors"] = f"ERROR: {exc}" - logger.debug("build_separation_vectors failed", exc_info=True) - else: - stats["build_separation_vectors"] = "SKIPPED (vector module not available)" + # ── 9. Build separation vectors (B2) ───────────────────────────── + if _HAS_VECTOR: + try: + t0 = time.time() + sep_count = build_separation_vectors(self.conn) + stats["build_separation_vectors"] = f"{sep_count} vectors in {time.time() - t0:.3f}s" + except Exception as exc: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(exc) + stats["build_separation_vectors"] = f"ERROR: {exc}" + logger.debug("build_separation_vectors failed", exc_info=True) + else: + stats["build_separation_vectors"] = "SKIPPED (vector module not available)" - # ── 10. Detect episodes (B1) ───────────────────────────────────── - if _HAS_TEMPORAL: - try: - t0 = time.time() - ep_count = detect_episodes(self.conn) - stats["detect_episodes"] = f"{ep_count} episodes in {time.time() - t0:.3f}s" - except Exception as exc: - stats["detect_episodes"] = f"ERROR: {exc}" - logger.debug("detect_episodes failed", exc_info=True) - else: - stats["detect_episodes"] = "SKIPPED (temporal module not available)" + # ── 10. Detect episodes (B1) ───────────────────────────────────── + if _HAS_TEMPORAL: + try: + t0 = time.time() + ep_count = detect_episodes(self.conn) + stats["detect_episodes"] = f"{ep_count} episodes in {time.time() - t0:.3f}s" + except Exception as exc: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(exc) + stats["detect_episodes"] = f"ERROR: {exc}" + logger.debug("detect_episodes failed", exc_info=True) + else: + stats["detect_episodes"] = "SKIPPED (temporal module not available)" - # ── 11. Detect landmark events (E3) ────────────────────────────── - if _HAS_TEMPORAL: - try: - t0 = time.time() - lm_count = detect_landmark_events(self.conn) - stats["detect_landmarks"] = f"{lm_count} events in {time.time() - t0:.3f}s" - except Exception as exc: - stats["detect_landmarks"] = f"ERROR: {exc}" - logger.debug("detect_landmarks failed", exc_info=True) - else: - stats["detect_landmarks"] = "SKIPPED (temporal module not available)" - - # ── 12. Build entity summary sheets (B3) ───────────────────────── - # Disabled 2026-04-24 per MEMORIST-L4 research. - # See CHANGELOG v0.6.0 for rationale. - # The function wrote `summaries` rows with period='entity_profile' - # that saturated top-1 retrieval by keyword match and leaked - # superseded facts into contradiction scoring. Disabling produced - # +5.3 pts on the L4 composite probe metric (Pareto-dominant, - # see REPORT.md §3 Table "D1 vs C1" and §10.7 Ablation 2). - # - # Escape hatch: set TRUEMEMORY_ENTITY_SHEETS=1 to re-enable this - # function. Users who regress on real-world workloads can revert - # without a code patch. Intended to be removed in a future release - # once long-horizon production telemetry confirms the disable. - _entity_sheets_enabled = ( - os.environ.get("TRUEMEMORY_ENTITY_SHEETS", "") - .strip().lower() in {"1", "true", "yes", "on"} - ) - if _HAS_CONSOLIDATION and _entity_sheets_enabled: - try: - t0 = time.time() - sheet_count = build_entity_summary_sheets(self.conn) - stats["entity_summary_sheets"] = f"{sheet_count} sheets in {time.time() - t0:.3f}s (re-enabled via TRUEMEMORY_ENTITY_SHEETS=1)" - except Exception as exc: - stats["entity_summary_sheets"] = f"ERROR: {exc}" - logger.debug("entity_summary_sheets failed", exc_info=True) - elif _HAS_CONSOLIDATION: - stats["entity_summary_sheets"] = "DISABLED (MEMORIST-L4; set TRUEMEMORY_ENTITY_SHEETS=1 to re-enable)" - else: - stats["entity_summary_sheets"] = "SKIPPED (consolidation module not available)" + # ── 11. Detect landmark events (E3) ────────────────────────────── + if _HAS_TEMPORAL: + try: + t0 = time.time() + lm_count = detect_landmark_events(self.conn) + stats["detect_landmarks"] = f"{lm_count} events in {time.time() - t0:.3f}s" + except Exception as exc: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(exc) + stats["detect_landmarks"] = f"ERROR: {exc}" + logger.debug("detect_landmarks failed", exc_info=True) + else: + stats["detect_landmarks"] = "SKIPPED (temporal module not available)" + + # ── 12. Build entity summary sheets (B3) ───────────────────────── + # Disabled 2026-04-24 per MEMORIST-L4 research. + # See CHANGELOG v0.6.0 for rationale. + # The function wrote `summaries` rows with period='entity_profile' + # that saturated top-1 retrieval by keyword match and leaked + # superseded facts into contradiction scoring. Disabling produced + # +5.3 pts on the L4 composite probe metric (Pareto-dominant, + # see REPORT.md §3 Table "D1 vs C1" and §10.7 Ablation 2). + # + # Escape hatch: set TRUEMEMORY_ENTITY_SHEETS=1 to re-enable this + # function. Users who regress on real-world workloads can revert + # without a code patch. Intended to be removed in a future release + # once long-horizon production telemetry confirms the disable. + _entity_sheets_enabled = ( + os.environ.get("TRUEMEMORY_ENTITY_SHEETS", "") + .strip().lower() in {"1", "true", "yes", "on"} + ) + if _HAS_CONSOLIDATION and _entity_sheets_enabled: + try: + t0 = time.time() + sheet_count = build_entity_summary_sheets(self.conn) + stats["entity_summary_sheets"] = f"{sheet_count} sheets in {time.time() - t0:.3f}s (re-enabled via TRUEMEMORY_ENTITY_SHEETS=1)" + except Exception as exc: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(exc) + stats["entity_summary_sheets"] = f"ERROR: {exc}" + logger.debug("entity_summary_sheets failed", exc_info=True) + elif _HAS_CONSOLIDATION: + stats["entity_summary_sheets"] = "DISABLED (MEMORIST-L4; set TRUEMEMORY_ENTITY_SHEETS=1 to re-enable)" + else: + stats["entity_summary_sheets"] = "SKIPPED (consolidation module not available)" - # ── 13. Build structured facts (B4) ────────────────────────────── - if _HAS_CONSOLIDATION: - try: - t0 = time.time() - fact_count = build_structured_facts(self.conn) - stats["structured_facts"] = f"{fact_count} facts in {time.time() - t0:.3f}s" - except Exception as exc: - stats["structured_facts"] = f"ERROR: {exc}" - logger.debug("structured_facts failed", exc_info=True) - else: - stats["structured_facts"] = "SKIPPED (consolidation module not available)" + # ── 13. Build structured facts (B4) ────────────────────────────── + if _HAS_CONSOLIDATION: + try: + t0 = time.time() + fact_count = build_structured_facts(self.conn) + stats["structured_facts"] = f"{fact_count} facts in {time.time() - t0:.3f}s" + except Exception as exc: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(exc) + stats["structured_facts"] = f"ERROR: {exc}" + logger.debug("structured_facts failed", exc_info=True) + else: + stats["structured_facts"] = "SKIPPED (consolidation module not available)" - # ── 14. Build Dunbar hierarchy (E2) ────────────────────────────── - if _HAS_PERSONALITY: - try: - t0 = time.time() - primary = None + # ── 14. Build Dunbar hierarchy (E2) ────────────────────────────── + if _HAS_PERSONALITY: try: - row = self.conn.execute( - "SELECT sender, COUNT(*) as cnt FROM messages " - "WHERE sender != '' AND sender IS NOT NULL " - "GROUP BY sender ORDER BY cnt DESC LIMIT 1" - ).fetchone() - if row and row[0] and row[0].strip(): - primary = row[0] + t0 = time.time() + primary = None + try: + row = self.conn.execute( + "SELECT sender, COUNT(*) as cnt FROM messages " + "WHERE sender != '' AND sender IS NOT NULL " + "GROUP BY sender ORDER BY cnt DESC LIMIT 1" + ).fetchone() + if row and row[0] and row[0].strip(): + primary = row[0] + except Exception as exc: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(exc) + logger.debug("Failed to detect primary entity for Dunbar: %s", exc) + dunbar_result = build_dunbar_hierarchy(self.conn, primary_entity=primary) + dunbar_count = len(dunbar_result) if isinstance(dunbar_result, dict) else dunbar_result + stats["dunbar_hierarchy"] = f"{dunbar_count} relationships in {time.time() - t0:.3f}s" except Exception as exc: - logger.debug("Failed to detect primary entity for Dunbar: %s", exc) - dunbar_result = build_dunbar_hierarchy(self.conn, primary_entity=primary) - dunbar_count = len(dunbar_result) if isinstance(dunbar_result, dict) else dunbar_result - stats["dunbar_hierarchy"] = f"{dunbar_count} relationships in {time.time() - t0:.3f}s" - except Exception as exc: - stats["dunbar_hierarchy"] = f"ERROR: {exc}" - logger.debug("dunbar_hierarchy failed", exc_info=True) - else: - stats["dunbar_hierarchy"] = "SKIPPED (personality module not available)" - - # ── Record capabilities ─────────────────────────────────────────── - self.stats["capabilities"] = { - "fts5": True, - "vector_search": self._has_vectors, - "hybrid_rrf": self._has_hybrid, - "temporal": self._has_temporal, - "salience": self._has_salience, - "personality": self._has_personality, - "style_vec": self._has_style_vec, - "consolidation": self._has_consolidation, - "predictive": self._has_predictive, - "reranker": self._has_reranker, - "hyde": self._has_hyde, - "clustering": self._has_clustering, - } + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(exc) + stats["dunbar_hierarchy"] = f"ERROR: {exc}" + logger.debug("dunbar_hierarchy failed", exc_info=True) + else: + stats["dunbar_hierarchy"] = "SKIPPED (personality module not available)" + + # ── Record capabilities ─────────────────────────────────────────── + self.stats["capabilities"] = { + "fts5": True, + "vector_search": self._has_vectors, + "hybrid_rrf": self._has_hybrid, + "temporal": self._has_temporal, + "salience": self._has_salience, + "personality": self._has_personality, + "style_vec": self._has_style_vec, + "consolidation": self._has_consolidation, + "predictive": self._has_predictive, + "reranker": self._has_reranker, + "hyde": self._has_hyde, + "clustering": self._has_clustering, + } - self.ready = True - return stats + self.ready = True + return stats # ────────────────────────────────────────────────────────────────────── # Search — full 6-layer pipeline # ────────────────────────────────────────────────────────────────────── + @engine_operation def search_vectors_raw(self, query: str, limit: int = 5) -> list[dict] | None: """Pure vector cosine similarity search, or None if unavailable. @@ -1723,10 +2284,13 @@ def search_vectors_raw(self, query: str, limit: int = 5) -> list[dict] | None: try: from truememory.vector_search import search_vector_raw return search_vector_raw(self.conn, query, limit=limit) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) return None - def search(self, query: str, limit: int = 10, _skip_surprise_boost: bool = False, _skip_reranker: bool = False, _skip_salience_guard: bool = False, include_directives: bool = False) -> list[dict]: + @engine_operation + def search(self, query: str, limit: int = 10, _skip_surprise_boost: bool = False, _skip_reranker: bool = False, _skip_salience_guard: bool = False, include_directives: bool = False, landmark_sender: str | None = None) -> list[dict]: """ Main search pipeline. @@ -1757,6 +2321,7 @@ def search(self, query: str, limit: int = 10, _skip_surprise_boost: bool = False results: list[dict] = [] source_label = "fts" + temporal_intent: dict = {} # ── 0. Classify query (A2/A5) ──────────────────────────────────── query_info = { @@ -1769,7 +2334,9 @@ def search(self, query: str, limit: int = 10, _skip_surprise_boost: bool = False try: query_info = classify_query(query) search_mode = get_search_mode(query) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.debug("Query classification failed in search()", exc_info=True) fts_w = query_info["weights"].get("fts", 1.0) @@ -1784,7 +2351,9 @@ def search(self, query: str, limit: int = 10, _skip_surprise_boost: bool = False include_directives=include_directives, ) source_label = "hybrid" - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.debug("Hybrid search failed, falling back to FTS5", exc_info=True) results = [] @@ -1796,7 +2365,9 @@ def search(self, query: str, limit: int = 10, _skip_surprise_boost: bool = False for r in results: if "source" not in r: r["source"] = "fts" - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.debug("FTS search failed in search()", exc_info=True) results = [] @@ -1813,20 +2384,25 @@ def search(self, query: str, limit: int = 10, _skip_surprise_boost: bool = False if _high_diversity and len(results) >= 3: try: results = self._scent_trail(query, results, limit) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.debug("Scent trail failed in search()", exc_info=True) # ── 3. Retrieval quality self-check (A4) — only with high diversity if _high_diversity: try: results = self._quality_self_check(query, results, limit) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.debug("Quality self-check failed in search()", exc_info=True) # ── 4. Temporal filtering with cross-source feedback (A1) ───────── if self._has_temporal: try: - intent = detect_temporal_intent(query) + intent = resolve_temporal_intent(self.conn, query, landmark_sender) + temporal_intent = intent if intent.get("has_temporal"): # search_temporal takes the query + existing results and # filters/re-ranks them using the detected time window. @@ -1836,6 +2412,7 @@ def search(self, query: str, limit: int = 10, _skip_surprise_boost: bool = False hybrid_results=results, limit=limit * 2, include_directives=include_directives, + resolved_intent=intent, ) else: temporal_results = search_temporal( @@ -1843,6 +2420,7 @@ def search(self, query: str, limit: int = 10, _skip_surprise_boost: bool = False fts_results=results, limit=limit * 2, include_directives=include_directives, + resolved_intent=intent, ) if temporal_results: # Tag new results with temporal source and merge. @@ -1891,6 +2469,10 @@ def search(self, query: str, limit: int = 10, _skip_surprise_boost: bool = False default=0.05, ) for rr in range_results: + if _source_intervals(rr.get("metadata")) is not None and not _temporal_row_matches( + rr, intent["after"], _exclusive_upper_bound(intent["before"]), + ): + continue if rr.get("id") and rr["id"] not in existing_ids: rr["source"] = "temporal_rescoped" _rs = rr.get("score", 1.0) @@ -1899,10 +2481,14 @@ def search(self, query: str, limit: int = 10, _skip_surprise_boost: bool = False rr["score"] = _resc_max * 0.8 * max(0.0, min(_rs, 1.0)) results.append(rr) existing_ids.add(rr["id"]) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.debug("Temporal cross-source rescope failed in search()", exc_info=True) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.debug("Temporal filtering failed in search()", exc_info=True) # ── 5. Personality supplementation ──────────────────────────────── @@ -1941,7 +2527,9 @@ def search(self, query: str, limit: int = 10, _skip_surprise_boost: bool = False results.append(pr) if pr_id: existing_ids.add(pr_id) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.debug("Personality supplementation failed in search()", exc_info=True) # ── 6. Contradiction / fact timeline ────────────────────────────── @@ -1977,7 +2565,9 @@ def search(self, query: str, limit: int = 10, _skip_surprise_boost: bool = False if cr.get("id") and cr["id"] not in existing_ids: results.append(cr) existing_ids.add(cr["id"]) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.debug("Contradiction search failed in search()", exc_info=True) try: @@ -2010,7 +2600,9 @@ def search(self, query: str, limit: int = 10, _skip_surprise_boost: bool = False if sr.get("id") and sr["id"] not in existing_ids: results.append(sr) existing_ids.add(sr["id"]) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.debug("Consolidated search failed in search()", exc_info=True) # ── 7. Salience guard with mode-aware threshold (A5) ────────────── @@ -2030,7 +2622,9 @@ def search(self, query: str, limit: int = 10, _skip_surprise_boost: bool = False results = apply_salience_guard( results, query, conn=self.conn, min_salience=min_sal, ) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.debug("Salience guard failed in search()", exc_info=True) # ── 7.5 L5 surprise rerank boost (MEMORIST-L5) ── @@ -2062,7 +2656,9 @@ def search(self, query: str, limit: int = 10, _skip_surprise_boost: bool = False rrf_weight=0.4, rerank_weight=0.6, ) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.debug("Cross-encoder rerank failed in search()", exc_info=True) # ── 9. Ensure all results have required fields and trim ─────────── @@ -2117,12 +2713,14 @@ def search(self, query: str, limit: int = 10, _skip_surprise_boost: bool = False # str() is type-stable even when ids mix int message ids with # "summary_N" / None supplement ids (#630 M-01). cleaned.sort(key=lambda d: (-d["score"], str(d.get("id", "")))) - return cleaned[:limit] + selected = cleaned[:limit] + return attach_temporal_resolution(selected, temporal_intent) if temporal_intent else selected # ────────────────────────────────────────────────────────────────────── # Search — agentic multi-round retrieval # ────────────────────────────────────────────────────────────────────── + @engine_operation def search_agentic( self, query: str, @@ -2136,6 +2734,7 @@ def search_agentic( max_per_session: int = 0, use_llm_reranker: bool = False, include_directives: bool = False, + landmark_sender: str | None = None, ) -> list[dict]: """ Agentic retrieval with sufficiency checking and multi-query generation. @@ -2184,7 +2783,19 @@ def search_agentic( candidate_pool = max(limit * 8, 100) # Large pool for reranking else: candidate_pool = limit * 3 - primary_results = self.search(query, limit=candidate_pool, _skip_surprise_boost=True, _skip_reranker=True, _skip_salience_guard=True, include_directives=include_directives) + primary_results = self.search(query, limit=candidate_pool, _skip_surprise_boost=True, _skip_reranker=True, _skip_salience_guard=True, include_directives=include_directives, landmark_sender=landmark_sender) + query_intent: dict = {} + if self._has_temporal: + try: + query_intent = resolve_temporal_intent(self.conn, query, landmark_sender) + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) + logger.debug("Agentic temporal resolution failed", exc_info=True) + for row in primary_results: + if row.get("temporal_scope_coverage"): + query_intent["source_interval_coverage"] = row["temporal_scope_coverage"] + break # If HyDE available, run a parallel search and fuse with RRF if use_hyde and self._has_hyde and self._has_hybrid and llm_fn: @@ -2198,7 +2809,9 @@ def search_agentic( primary_results = reciprocal_rank_fusion( [primary_results, hyde_results] ) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.debug("HyDE search failed in search_agentic()", exc_info=True) # ── Score normalization (issue #584) ───────────────────────────── @@ -2239,7 +2852,9 @@ def search_agentic( cr["source"] = cr.get("source", "") + "+cluster_supp" primary_results.append(cr) existing_ids.add(cid) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.debug("Cluster search failed in search_agentic()", exc_info=True) # ── Entity-focused search ─────────────────────────────────────── @@ -2265,29 +2880,24 @@ def search_agentic( _ent_after = _ent_before = None try: from truememory.temporal import ( - detect_temporal_intent, _validate_iso_date, _exclusive_upper_bound, + _temporal_row_matches, ) - _ent_intent = detect_temporal_intent(query) + _ent_intent = query_intent _ent_after = _validate_iso_date(_ent_intent.get("after")) _eb = _validate_iso_date(_ent_intent.get("before")) _ent_before = _exclusive_upper_bound(_eb) if _eb else None - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.debug("Entity temporal-window detection failed", exc_info=True) def _in_window(row: dict) -> bool: # No window detected -> every row is "in window". if _ent_after is None and _ent_before is None: return True - ts = row.get("timestamp") or "" - if not ts: - return False - if _ent_after is not None and ts < _ent_after: - return False - if _ent_before is not None and ts >= _ent_before: - return False - return True + return _temporal_row_matches(row, _ent_after, _ent_before) # Normalize entity scores to [0, 1] independently normalize_scores(entity_results) @@ -2331,7 +2941,9 @@ def _in_window(row: dict) -> bool: primary_results.sort( key=lambda d: (-d.get("score", 0), d.get("id", 0)) ) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.debug("Entity-focused search failed in search_agentic()", exc_info=True) # ── Salience guard (deferred from search() for #582) ───────────── @@ -2357,7 +2969,9 @@ def _in_window(row: dict) -> bool: if r.get("_entity_boosted") and r.get("id") ), ) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.debug("Salience guard failed in search_agentic()", exc_info=True) # ── Sufficiency check ───────────────────────────────────────────── @@ -2372,7 +2986,7 @@ def _in_window(row: dict) -> bool: existing_ids = {r.get("id") for r in primary_results if r.get("id")} for rq in refined_queries: try: - rq_results = self.search(rq, limit=limit, _skip_surprise_boost=True, _skip_reranker=True, include_directives=include_directives) + rq_results = self.search(rq, limit=limit, _skip_surprise_boost=True, _skip_reranker=True, include_directives=include_directives, landmark_sender=landmark_sender) normalize_scores(rq_results) for rr in rq_results: rid = rr.get("id") @@ -2387,7 +3001,9 @@ def _in_window(row: dict) -> bool: if pr.get("id") == rid: pr["score"] = pr.get("score", 0) * 1.15 break - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.debug("Refined query search failed in search_agentic()", exc_info=True) # Re-sort after adding refined results @@ -2423,11 +3039,16 @@ def _in_window(row: dict) -> bool: query, final_results[:_llm_cap], llm_fn=llm_fn, top_k=limit, ) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.debug("LLM reranking failed in search_agentic()", exc_info=True) - return self._clean_results(final_results, limit, max_per_session=max_per_session, - include_directives=include_directives) - except Exception: + cleaned = self._clean_results(final_results, limit, max_per_session=max_per_session, + include_directives=include_directives) + return attach_temporal_resolution(cleaned, query_intent) if query_intent else cleaned + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.warning("Cross-encoder rerank failed in search_agentic()", exc_info=True) # ── LLM reranking without cross-encoder ────────────────────────── @@ -2439,11 +3060,14 @@ def _in_window(row: dict) -> bool: query, primary_results[:_llm_cap], llm_fn=llm_fn, top_k=limit, ) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) logger.debug("LLM reranking (standalone) failed in search_agentic()", exc_info=True) - return self._clean_results(primary_results, limit, max_per_session=max_per_session, - include_directives=include_directives) + cleaned = self._clean_results(primary_results, limit, max_per_session=max_per_session, + include_directives=include_directives) + return attach_temporal_resolution(cleaned, query_intent) if query_intent else cleaned # ── L5 surprise rerank boost (MEMORIST-L5 wiring, 2026-04-24) ────── # Multiplies the reranked `score` field by (1 + α · surprise) for @@ -2513,6 +3137,7 @@ def _quality_self_check(self, query: str, results: list[dict], limit: int) -> li # Search — simple FTS5-only (for benchmarking) # ────────────────────────────────────────────────────────────────────── + @engine_handle_operation def search_simple(self, query: str, limit: int = 10) -> list[dict]: """ Simple FTS5-only search (no vector, no layers). @@ -2525,7 +3150,9 @@ def search_simple(self, query: str, limit: int = 10) -> list[dict]: try: results = search_fts(self.conn, query, limit=limit) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) return [] cleaned: list[dict] = [] @@ -2550,10 +3177,57 @@ def search_simple(self, query: str, limit: int = 10) -> list[dict]: # Stats / teardown # ────────────────────────────────────────────────────────────────────── + def get_clustering_health(self) -> dict: + """Read existing state without connecting or scheduling maintenance.""" + from truememory.maintenance import clustering_health + if not self._write_lock.acquire(blocking=False): + return clustering_health(pending_reason="foreground_busy") + try: + coordinator = self._maintenance_coordinator if self._maintenance_handle is self.conn else None + return clustering_health(self.conn, coordinator) + finally: + self._write_lock.release() + + def get_style_health(self) -> dict: + """Observe existing style coverage without connecting or scheduling.""" + from truememory.maintenance import style_health + if not self._write_lock.acquire(blocking=False): + return style_health(pending_reason="foreground_busy") + try: + coordinator = self._maintenance_coordinator if self._maintenance_handle is self.conn else None + return style_health(self.conn, coordinator) + finally: + self._write_lock.release() + def get_stats(self) -> dict: """Return ingestion and search statistics.""" self._ensure_connection() stats = dict(self.stats) + if self._write_lock.acquire(blocking=False): + try: + from truememory.maintenance import engine_layer_specs, layer_freshness, read_layer_states + coordinator = self._get_maintenance_coordinator() + maintenance = coordinator.snapshot() + maintenance["last_results"] = [result._asdict() for result in maintenance["last_results"]] + maintenance["pending_reason"] = self._maintenance_pending_reason + maintenance["pending_caller_commit"] = self.conn.in_transaction + states = read_layer_states(self.conn, engine_layer_specs(self.conn, coordinator)) + maintenance["layers"] = {name: { + "outcome": state.outcome, "freshness": layer_freshness(state.source, state, state.dependency), + "coverage": state.successful_coverage, "error_category": state.error_category, + "dependency_error": state.dependency.error_category, "output_count": state.output_count, + "pending_inserts": state.source.insert_count - (state.attempted_insert_count or 0), + } for name, state in states.items()} + stats["maintenance"] = maintenance + except Exception as error: + stats["maintenance"] = {"status": "unavailable", "error_category": type(error).__name__[:64]} + finally: + self._write_lock.release() + else: + stats["maintenance"] = {"status": "pending", "pending_reason": "foreground_busy"} + + stats["clustering"] = self.get_clustering_health() + stats["maintenance"]["style"] = self.get_style_health() # Add live DB stats if connected. if self.conn: @@ -2576,19 +3250,22 @@ def get_stats(self) -> dict: return stats def close(self): - """Close database connection and join background threads.""" - if self._consolidation_thread is not None and self._consolidation_thread.is_alive(): - try: - self._consolidation_thread.join(timeout=5) - except Exception: - pass - if self.conn: - try: - self.conn.close() - except Exception: - logger.debug("Failed to close database connection", exc_info=True) - self.conn = None - self.ready = False + """Detach this engine; a shared worker retains its own connection.""" + with self._init_lock, self._write_lock: + self._maintenance_coordinator = None + self._maintenance_handle = None + self._maintenance_pending_reason = "not_connected" + if self.conn: + try: + self.conn.close() + except sqlite3.Error: + logger.debug("Failed to close database connection", exc_info=True) + self.conn = None + self.ready = False + self._runtime_initialized = False + self._runtime_vector_generation = None + self._runtime_vector_connection = None + self._reconnect_receipt = None def __enter__(self): return self diff --git a/truememory/hooks/adapters/base.py b/truememory/hooks/adapters/base.py index fd8d3f71..964d1935 100644 --- a/truememory/hooks/adapters/base.py +++ b/truememory/hooks/adapters/base.py @@ -8,10 +8,53 @@ import json import os +import re +import shlex +import sys from abc import ABC, abstractmethod from pathlib import Path +def is_owned_hook_command(command: object) -> bool: + """Recognize a simple Python invocation of an installed TrueMemory hook.""" + if not isinstance(command, str) or not command.strip() or any(char in command for char in ("\0", "\n", "\r")): + return False + try: + lexer = shlex.shlex(command, posix=False, punctuation_chars=";&|<>()`") + lexer.whitespace_split = True + for token in lexer: + if token and all(char in ";&|<>()`" for char in token): + return False + if not token.startswith("'") and any(part in token for part in ("$(", "${", "`")): + return False + tokens = shlex.split(command, comments=True, posix=sys.platform != "win32") + except ValueError: + return False + if sys.platform == "win32": + tokens = [token[1:-1] if token.startswith('"') and token.endswith('"') else token for token in tokens] + if len(tokens) < 2: + return False + executable = tokens[0].replace("\\", "/").rsplit("/", 1)[-1] + if re.fullmatch(r"python(?:\d+(?:\.\d+)*)?(?:\.exe)?", executable, re.IGNORECASE) is None: + return False + index = 1 + while index < len(tokens) and tokens[index] in ("-B", "-u", "-E", "-I", "-s", "-S", "-O", "-OO"): + index += 1 + if index >= len(tokens): + return False + stems = ("session_start", "user_prompt_submit", "stop", "compact") + if tokens[index] == "-m": + return index + 1 < len(tokens) and tokens[index + 1] in ( + f"truememory.ingest.hooks.{stem}" for stem in stems + ) + script = tokens[index].replace("\\", "/").split("/") + return ( + len(script) >= 4 + and script[-4:-1] == ["truememory", "ingest", "hooks"] + and script[-1] in (f"{stem}.py" for stem in stems) + ) + + def atomic_write_text(path: Path, text: str) -> None: """Write *text* to *path* atomically (tmp-in-same-dir + os.replace). diff --git a/truememory/hooks/adapters/claude.py b/truememory/hooks/adapters/claude.py index 7e113d17..f33b43c0 100644 --- a/truememory/hooks/adapters/claude.py +++ b/truememory/hooks/adapters/claude.py @@ -14,6 +14,75 @@ from truememory.hooks.adapters.base import CLIAdapter +_HOOK_MODULES = { + "SessionStart": "truememory.ingest.hooks.session_start", + "UserPromptSubmit": "truememory.ingest.hooks.user_prompt_submit", + "SessionEnd": "truememory.ingest.hooks.stop", + "Stop": "truememory.ingest.hooks.stop", + "PreCompact": "truememory.ingest.hooks.compact", +} + + +def claude_hook_settings(python_path: str, user_id: str = "", db_path: str = "", *, + preserve_stop: bool = False) -> dict: + import shlex + import subprocess + events = ["SessionStart", "UserPromptSubmit", "SessionEnd", "PreCompact"] + if preserve_stop or os.environ.get("TRUEMEMORY_CLAUDE_STOP_FALLBACK") == "1": + events.append("Stop") + settings = {"hooks": {}} + for event in events: + parts = [python_path, "-m", _HOOK_MODULES[event], "--host", "claude", "--event", event] + if user_id: + parts.extend(["--user", user_id]) + if db_path: + parts.extend(["--db", db_path]) + command = subprocess.list2cmdline(parts) if sys.platform == "win32" else " ".join(shlex.quote(part) for part in parts) + settings["hooks"][event] = [{"matcher": "", "hooks": [{"type": "command", "command": command}]}] + return settings + + +def _owned_hook_module(command: object, hooks_dir: Path, python_path: str) -> str | None: + import re + import shlex + if type(command) is not str: + return None + try: + parts = shlex.split(command, posix=sys.platform != "win32") + if sys.platform == "win32": + parts = [part[1:-1] if len(part) >= 2 and part[0] == part[-1] == '"' else part for part in parts] + except ValueError: + return None + if len(parts) < 2: + return None + executable = Path(parts[0]).name.lower() + if parts[0] != python_path and re.fullmatch(r"(?:python(?:[0-9]+(?:\.[0-9]+)*)?|py)(?:\.exe)?", executable) is None: + return None + modules = set(_HOOK_MODULES.values()) + if len(parts) >= 3 and parts[1] == "-m" and parts[2] in modules: + module, tail = parts[2], parts[3:] + else: + scripts = {str((hooks_dir / (module.rsplit(".", 1)[1] + ".py")).resolve()): module for module in modules} + try: + module = scripts.get(str(Path(parts[1]).resolve())) + except (OSError, ValueError): + return None + tail = parts[2:] + if module is None: + return None + if len(tail) % 2 or any(tail[index] not in ("--user", "--db", "--host", "--event") for index in range(0, len(tail), 2)): + return None + return module + + +def _group_has_module(group: object, module: str, hooks_dir: Path, python_path: str) -> bool: + if type(group) is not dict: + return False + commands = group.get("hooks", [group]) + return type(commands) is list and any(type(hook) is dict and hook.get("type") == "command" + and _owned_hook_module(hook.get("command"), hooks_dir, python_path) == module for hook in commands) + + class ClaudeAdapter(CLIAdapter): """Adapter for Claude Code CLI.""" @@ -147,43 +216,8 @@ def install_hooks( if missing: raise FileNotFoundError(f"Missing hook files: {', '.join(missing)}") - import shlex py = python_path or sys.executable - - _HOOK_MODULES = { - "session_start.py": "truememory.ingest.hooks.session_start", - "user_prompt_submit.py": "truememory.ingest.hooks.user_prompt_submit", - "stop.py": "truememory.ingest.hooks.stop", - "compact.py": "truememory.ingest.hooks.compact", - } - - def _build_command(hook_path: Path) -> str: - module = _HOOK_MODULES.get(hook_path.name) - if module: - parts: list[str] = [py, "-m", module] - else: - parts = [py, str(hook_path)] - if user_id: - parts.extend(["--user", user_id]) - if db_path: - parts.extend(["--db", db_path]) - if sys.platform == "win32": - import subprocess as _sp - return _sp.list2cmdline(parts) - return " ".join(shlex.quote(p) for p in parts) - - settings = { - "hooks": { - event: [{ - "matcher": "", - "hooks": [{ - "type": "command", - "command": _build_command(path), - }], - }] - for event, path in hook_files.items() - } - } + settings = claude_hook_settings(py, user_id, db_path) # Merge into existing settings (preserves other config) settings_path = self.config_path @@ -203,54 +237,34 @@ def _build_command(hook_path: Path) -> str: if not isinstance(existing["hooks"], dict): existing["hooks"] = {} - # Migration: earlier versions of this installer registered the compact - # hook under the event name "compact", but Claude Code's canonical event - # name is "PreCompact". Any hook registered under "compact" is silently - # ignored by the runtime. Strip stale "compact" entries that point at - # our hook file so users upgrading from 0.1.0 don't end up with dead - # config alongside the correct PreCompact entry. - _legacy_compact = existing["hooks"].get("compact") - if isinstance(_legacy_compact, list): - _cleaned = [ - h for h in _legacy_compact - if not ( - isinstance(h, dict) - and "truememory" in str(h.get("command", "")).lower() - ) - ] - if _cleaned: - existing["hooks"]["compact"] = _cleaned + # Only exact owned commands migrate from the unsupported event. Foreign + # hooks remain untouched, including commands mentioning TrueMemory. + legacy = existing["hooks"].get("compact") + if isinstance(legacy, list): + cleaned = [] + for group in legacy: + if not isinstance(group, dict): + cleaned.append(group) + continue + if "hooks" in group and isinstance(group["hooks"], list): + inner = [hook for hook in group["hooks"] if not ( + isinstance(hook, dict) and hook.get("type") == "command" + and _owned_hook_module(hook.get("command"), hooks_dir, py) == _HOOK_MODULES["PreCompact"])] + if inner: + cleaned.append({**group, "hooks": inner}) + elif not _group_has_module(group, _HOOK_MODULES["PreCompact"], hooks_dir, py): + cleaned.append(group) + if cleaned: + existing["hooks"]["compact"] = cleaned else: - del existing["hooks"]["compact"] - if len(_cleaned) != len(_legacy_compact): - print( - " Migrated legacy 'compact' hook entry to 'PreCompact' " - "(earlier versions registered the wrong event name)." - ) + existing["hooks"].pop("compact", None) - # Migration: earlier versions wired the transcript extraction hook to - # Claude Code's "Stop" event, but "Stop" fires after every assistant - # response (per-turn), not once at session end. The correct event is - # "SessionEnd". Strip stale "Stop" entries that point at our hook file - # so upgrading users don't get double-fires. - _legacy_stop = existing["hooks"].get("Stop") - if isinstance(_legacy_stop, list): - _cleaned = [ - h for h in _legacy_stop - if not ( - isinstance(h, dict) - and "truememory" in str(h.get("command", "")).lower() - ) - ] - if _cleaned: - existing["hooks"]["Stop"] = _cleaned - else: - del existing["hooks"]["Stop"] - if len(_cleaned) != len(_legacy_stop): - print( - " Migrated truememory extraction hook from 'Stop' (per-turn) " - "to 'SessionEnd' (per-session)." - ) + # A supported Stop fallback may already be the only firing trigger on + # a host. Preserve it while always installing SessionEnd as default. + prior_stop = existing["hooks"].get("Stop", []) + viable_stop = isinstance(prior_stop, list) and any( + _group_has_module(group, _HOOK_MODULES["Stop"], hooks_dir, py) for group in prior_stop) + settings = claude_hook_settings(py, user_id, db_path, preserve_stop=viable_stop) # Migration: earlier versions wrote hooks in the flat format # {type, command} instead of the required {matcher, hooks: [{type, command}]}. @@ -278,34 +292,33 @@ def _build_command(hook_path: Path) -> str: existing["hooks"][event] = migrated print(f" Migrated '{event}' hook entries from old format to new {{matcher, hooks}} schema.") - for event, hooks in settings["hooks"].items(): - existing["hooks"].setdefault(event, []) - if not isinstance(existing["hooks"][event], list): - existing["hooks"][event] = [] - # Don't add duplicates. Match on the "truememory" substring rather - # than the absolute .py file path: _build_command emits module-form - # commands ("-m truememory.ingest.hooks.X"), so the .py path never - # appears in the command and the old check matched nothing — every - # re-install appended another entry. The "truememory" needle is the - # same one the migration/uninstall code already uses, keeping - # install idempotent across upgrades. - for hook in hooks: - already_present = False - for h in existing["hooks"][event]: - if not isinstance(h, dict): - continue - inner_hooks = h.get("hooks", []) - if isinstance(inner_hooks, list): - for ih in inner_hooks: - if isinstance(ih, dict) and "truememory" in str(ih.get("command", "")).lower(): - already_present = True - break - if "truememory" in str(h.get("command", "")).lower(): - already_present = True - if already_present: - break - if not already_present: - existing["hooks"][event].append(hook) + for event, desired_groups in settings["hooks"].items(): + entries = existing["hooks"].get(event, []) + if not isinstance(entries, list): + entries = [] + module = _HOOK_MODULES[event] + desired = desired_groups[0]["hooks"][0] + merged = [] + replaced = False + for group in entries: + if not isinstance(group, dict) or not isinstance(group.get("hooks"), list): + merged.append(group) + continue + inner = [] + for hook in group["hooks"]: + owned = (isinstance(hook, dict) and hook.get("type") == "command" + and _owned_hook_module(hook.get("command"), hooks_dir, py) == module) + if owned: + if not replaced: + inner.append({**hook, "command": desired["command"]}) + replaced = True + else: + inner.append(hook) + if inner: + merged.append({**group, "hooks": inner}) + if not replaced: + merged.extend(desired_groups) + existing["hooks"][event] = merged settings_tmp = settings_path.with_suffix(".json.tmp") settings_tmp.write_text(json.dumps(existing, indent=2), encoding="utf-8") diff --git a/truememory/hooks/adapters/codex.py b/truememory/hooks/adapters/codex.py index b0bc603d..a8813b60 100644 --- a/truememory/hooks/adapters/codex.py +++ b/truememory/hooks/adapters/codex.py @@ -10,13 +10,14 @@ from __future__ import annotations import logging +import json import re import shlex import shutil import sys from pathlib import Path -from truememory.hooks.adapters.base import CLIAdapter, atomic_write_text +from truememory.hooks.adapters.base import CLIAdapter, atomic_write_text, is_owned_hook_command log = logging.getLogger(__name__) @@ -158,6 +159,7 @@ def install_hooks( # Migrate any legacy [[hooks]] blocks before appending new ones existing_text = self._remove_legacy_hook_blocks(existing_text) + existing_text = self._refresh_hook_commands(existing_text, py, hooks_dir, user_id, db_path) existing_hooks = self._parse_existing_hooks(existing_text) @@ -167,7 +169,7 @@ def install_hooks( continue script_path = hooks_dir / info["script"] - cmd = self._build_command(py, script_path, user_id, db_path) + cmd = self._build_command(py, script_path, user_id, db_path, event=event) lines_to_append.append("") lines_to_append.append(f"[[hooks.{event}]]") @@ -223,8 +225,11 @@ def _build_command( script_path: Path, user_id: str = "", db_path: str = "", + event: str = "", ) -> str: - parts: list[str] = [python_path, str(script_path)] + parts: list[str] = [python_path, str(script_path), "--host", "codex"] + if event: + parts.extend(["--event", event]) if user_id: parts.extend(["--user", user_id]) if db_path: @@ -234,6 +239,52 @@ def _build_command( return _sp.list2cmdline(parts) return " ".join(shlex.quote(p) for p in parts) + def _refresh_hook_commands( + self, text: str, python_path: str, hooks_dir: Path, user_id: str, db_path: str, + ) -> str: + """Upgrade owned command lines while retaining other TOML fields/hooks.""" + event = "" + lines: list[str] = [] + for line in text.splitlines(keepends=True): + header = re.match(r'^\s*\[\[hooks\.(\w+)\.hooks\]\]\s*$', line) + if header: + event = header.group(1) + elif line.lstrip().startswith("["): + event = "" + if ( + event in _HOOK_EVENTS + and re.match(r'^\s*command\s*=', line) + and is_owned_hook_command(self._hook_command_value(line)) + ): + cmd = self._build_command( + python_path, hooks_dir / _HOOK_EVENTS[event]["script"], + user_id, db_path, event=event, + ) + indent = line[:len(line) - len(line.lstrip())] + ending = "\n" if line.endswith("\n") else "" + line = f'{indent}command = "{_toml_escape(cmd)}"{ending}' + lines.append(line) + return "".join(lines) + + @staticmethod + def _hook_command_value(line: str) -> str | None: + """Decode the generated one-line TOML string without reading comments.""" + match = re.match(r'^\s*command\s*=\s*(.*)$', line) + if match is None: + return None + value = match.group(1).strip() + if value.startswith("'"): + literal = re.fullmatch(r"'([^']*)'\s*(?:#.*)?", value) + return literal.group(1) if literal else None + try: + command, end = json.JSONDecoder().raw_decode(value) + except (json.JSONDecodeError, ValueError): + return None + tail = value[end:].strip() + if isinstance(command, str) and (not tail or tail.startswith("#")): + return command + return None + @staticmethod def _parse_existing_hooks(toml_text: str) -> dict: """Parse hooks from TOML text, returning ``{event: [matcher_groups]}``.""" @@ -315,7 +366,7 @@ def _event_already_registered(hooks: dict, event: str) -> bool: for hook_entry in mg.get("hooks", []): handler = hook_entry if isinstance(hook_entry, dict) else {} cmd = handler.get("command", "") - if _TRUEMEMORY_MARKER in cmd.lower(): + if is_owned_hook_command(cmd): return True return False @@ -340,7 +391,7 @@ def _has_hook_entries_in_text(self, text: str) -> bool: for mg in matcher_groups: for hook_entry in mg.get("hooks", []): handler = hook_entry if isinstance(hook_entry, dict) else {} - if _TRUEMEMORY_MARKER in handler.get("command", "").lower(): + if is_owned_hook_command(handler.get("command", "")): return True return False @@ -410,8 +461,7 @@ def _remove_hook_blocks(text: str) -> str: block_lines.append(lines[i]) i += 1 - block_text = "".join(block_lines) - if _TRUEMEMORY_MARKER not in block_text.lower(): + if not any(is_owned_hook_command(CodexAdapter._hook_command_value(line)) for line in block_lines): cleaned.extend(block_lines) continue @@ -441,8 +491,7 @@ def _remove_legacy_hook_blocks(text: str) -> str: block_lines.append(lines[i]) i += 1 - block_text = "".join(block_lines) - if _TRUEMEMORY_MARKER not in block_text.lower(): + if not any(is_owned_hook_command(CodexAdapter._hook_command_value(line)) for line in block_lines): cleaned.extend(block_lines) continue diff --git a/truememory/hooks/adapters/cursor.py b/truememory/hooks/adapters/cursor.py index 06ba89d5..a61b1d29 100644 --- a/truememory/hooks/adapters/cursor.py +++ b/truememory/hooks/adapters/cursor.py @@ -13,7 +13,7 @@ import sys from pathlib import Path -from truememory.hooks.adapters.base import CLIAdapter, atomic_write_text +from truememory.hooks.adapters.base import CLIAdapter, atomic_write_text, is_owned_hook_command _CURSOR_DIR = Path.home() / ".cursor" _MCP_CONFIG = _CURSOR_DIR / "mcp.json" @@ -172,7 +172,7 @@ def install_hooks( h for h in stale_list if not ( isinstance(h, dict) - and _TRUEMEMORY_MARKER in h.get("command", "").lower() + and is_owned_hook_command(h.get("command")) ) ] if cleaned: @@ -180,7 +180,8 @@ def install_hooks( else: hooks.pop(stale, None) - # --- Migration: update existing TrueMemory hook timeouts --- + # Upgrade owned commands so existing installations select the native + # event contract instead of retaining the legacy Claude response. for event, info in _HOOK_EVENTS.items(): event_list = hooks.get(event) if not isinstance(event_list, list): @@ -188,10 +189,12 @@ def install_hooks( for entry in event_list: if ( isinstance(entry, dict) - and _TRUEMEMORY_MARKER in entry.get("command", "").lower() - and entry.get("timeout") != info["timeout"] + and is_owned_hook_command(entry.get("command")) ): entry["timeout"] = info["timeout"] + entry["command"] = self._build_command( + py, hooks_dir / info["script"], user_id, db_path, event=event, + ) for event, info in _HOOK_EVENTS.items(): event_list = hooks.setdefault(event, []) @@ -203,7 +206,7 @@ def install_hooks( continue script_path = hooks_dir / info["script"] - cmd = self._build_command(py, script_path, user_id, db_path) + cmd = self._build_command(py, script_path, user_id, db_path, event=event) event_list.append({ "command": cmd, "timeout": info["timeout"], @@ -232,12 +235,16 @@ def _build_command( script_path: Path, user_id: str = "", db_path: str = "", + event: str = "", ) -> str: parts: list[str] = [python_path, str(script_path)] if user_id: parts.extend(["--user", user_id]) if db_path: parts.extend(["--db", db_path]) + parts.extend(["--host", "cursor"]) + if event: + parts.extend(["--event", event]) if sys.platform == "win32": import subprocess as _sp return _sp.list2cmdline(parts) @@ -273,7 +280,7 @@ def _has_hook_entries(self) -> bool: def _event_has_truememory(entries: list) -> bool: return any( isinstance(h, dict) - and _TRUEMEMORY_MARKER in h.get("command", "").lower() + and is_owned_hook_command(h.get("command")) for h in entries ) @@ -309,7 +316,7 @@ def _remove_hook_entries(self) -> None: h for h in entries if not ( isinstance(h, dict) - and _TRUEMEMORY_MARKER in h.get("command", "").lower() + and is_owned_hook_command(h.get("command")) ) ] if cleaned: diff --git a/truememory/hooks/adapters/gemini.py b/truememory/hooks/adapters/gemini.py index 938b6b96..52c7af68 100644 --- a/truememory/hooks/adapters/gemini.py +++ b/truememory/hooks/adapters/gemini.py @@ -18,7 +18,7 @@ import sys from pathlib import Path -from truememory.hooks.adapters.base import CLIAdapter, atomic_write_text +from truememory.hooks.adapters.base import CLIAdapter, atomic_write_text, is_owned_hook_command _GEMINI_DIR = Path.home() / ".gemini" _CONFIG_PATH = _GEMINI_DIR / "settings.json" @@ -147,11 +147,30 @@ def install_hooks( event_list = [] hooks[event] = event_list + script_path = hooks_dir / info["script"] + cmd = self._build_command(py, script_path, user_id, db_path, event=event) + for definition in event_list: + if not isinstance(definition, dict): + continue + inner_hooks = definition.get("hooks", []) + if isinstance(inner_hooks, list): + for hook in inner_hooks: + if ( + isinstance(hook, dict) + and isinstance(hook.get("command"), str) + and is_owned_hook_command(hook["command"]) + ): + hook["command"] = cmd + legacy_command = definition.get("command", "") + if is_owned_hook_command(legacy_command): + definition.pop("command", None) + definition.pop("timeout", None) + definition["hooks"] = [{ + "type": "command", "command": cmd, + "name": f"truememory-{event.lower()}", "timeout": info["timeout"], + }] if self._event_has_truememory(event_list): continue - - script_path = hooks_dir / info["script"] - cmd = self._build_command(py, script_path, user_id, db_path) event_list.append({ "hooks": [{ "type": "command", @@ -208,8 +227,11 @@ def _build_command( script_path: Path, user_id: str = "", db_path: str = "", + event: str = "", ) -> str: - parts: list[str] = [python_path, str(script_path)] + parts: list[str] = [python_path, str(script_path), "--host", "gemini"] + if event: + parts.extend(["--event", event]) if user_id: parts.extend(["--user", user_id]) if db_path: @@ -277,11 +299,11 @@ def _definition_has_truememory(hook_def: object) -> bool: for hc in inner_hooks: if ( isinstance(hc, dict) - and _TRUEMEMORY_MARKER in hc.get("command", "").lower() + and is_owned_hook_command(hc.get("command", "")) ): return True # Legacy flat format fallback - if _TRUEMEMORY_MARKER in hook_def.get("command", "").lower(): + if is_owned_hook_command(hook_def.get("command", "")): return True return False diff --git a/truememory/hooks/adapters/hermes.py b/truememory/hooks/adapters/hermes.py index b9affe95..36784caa 100644 --- a/truememory/hooks/adapters/hermes.py +++ b/truememory/hooks/adapters/hermes.py @@ -4,7 +4,7 @@ - ~/.hermes/config.yaml for both MCP server registration (mcp_servers: key) and shell hook registration (hooks: key) - Shell hooks are list-of-dicts under each event name with command/matcher/timeout -- Same JSON stdin/stdout wire protocol as Claude Code for shell hooks +- Native session_id plus extra content on stdin; top-level context on stdout - 19 valid hook events defined in hermes_cli/plugins.py VALID_HOOKS Hook config format (from hermes-agent source — agent/shell_hooks.py): @@ -24,6 +24,7 @@ from __future__ import annotations import logging +import re import shlex import shutil import sys @@ -36,39 +37,89 @@ _HERMES_DIR = Path.home() / ".hermes" _CONFIG = _HERMES_DIR / "config.yaml" -# Map TrueMemory hook scripts to Hermes hook events. -# Hermes uses a single config.yaml for both mcp_servers and hooks. -# -# NOTE on pre_llm_call -> user_prompt_submit.py: -# Hermes fires pre_llm_call on every LLM API call, including continuation -# and retry calls — not just on new user prompts. user_prompt_submit.py is -# designed to be idempotent (buffers are append-only, recall is read-only, -# extraction is guarded by should_extract_session), so repeated invocations -# are harmless but wasteful. Hermes does not expose a dedicated -# "user_prompt_submitted" event; pre_llm_call is the closest approximation. -# The script exits early if stdin provides no prompt or a prompt < 3 chars, -# which naturally filters most continuation/retry calls where the prompt -# field is empty. +# Pinned Hermes main and v0.21.6 emit pre/post once per user turn. +# The fixed wrapper translates native content without transcript admission. _HOOK_ENTRIES = { "on_session_start": { - "script": "session_start.py", + "script": "hermes.py", "timeout": 30, }, "on_session_end": { - "script": "stop.py", + "script": "hermes.py", "timeout": 30, }, "pre_llm_call": { - "script": "user_prompt_submit.py", + "script": "hermes.py", "timeout": 10, }, + "post_llm_call": { + "script": "hermes.py", + "timeout": 30, + }, "on_session_finalize": { - "script": "compact.py", + "script": "hermes.py", "timeout": 15, }, } -_TRUEMEMORY_MARKER = "truememory" +_LEGACY_SCRIPTS = {"on_session_start": "session_start.py", "on_session_end": "stop.py", + "pre_llm_call": "user_prompt_submit.py", "on_session_finalize": "compact.py"} + + +def _owned_command(command: object, event: str) -> bool: + """Only replace a simple Python invocation of this installed event hook.""" + if not isinstance(command, str) or any(char in command for char in ("\0", "\n", "\r")): + return False + try: + lexer = shlex.shlex(command, posix=False, punctuation_chars=";&|<>()`") + lexer.whitespace_split = True + for token in lexer: + if token and all(char in ";&|<>()`" for char in token): + return False + if not token.startswith("'") and any(part in token for part in ("$(", "${", "`")): + return False + tokens = shlex.split(command, comments=False, posix=sys.platform != "win32") + except ValueError: + return False + if sys.platform == "win32": + tokens = [token[1:-1] if token.startswith('"') and token.endswith('"') else token + for token in tokens] + if len(tokens) < 2 or re.fullmatch(r"python(?:\d+(?:\.\d+)*)?(?:\.exe)?", + tokens[0].replace("\\", "/").rsplit("/", 1)[-1], re.I) is None: + return False + index = 1 + while index < len(tokens) and tokens[index] in {"-B", "-u", "-E", "-I", "-s", "-S", "-O", "-OO"}: + index += 1 + if index >= len(tokens): + return False + script = tokens[index] + index += 1 + if script == "-m": + if index >= len(tokens): + return False + module = tokens[index] + index += 1 + prefix = "truememory.ingest.hooks." + script_name = module.removeprefix(prefix) + ".py" if module.startswith(prefix) else "" + else: + hook_dir = Path(__file__).parent.parent.parent / "ingest" / "hooks" + candidates = {str(hook_dir / name): name for name in + {"hermes.py", *_LEGACY_SCRIPTS.values()}} + script_name = candidates.get(script, "") + native = script_name == "hermes.py" + if not native and script_name != _LEGACY_SCRIPTS.get(event): + return False + flags: dict[str, str] = {} + rest = tokens[index:] + if len(rest) % 2: + return False + for flag, value in zip(rest[::2], rest[1::2]): + if flag not in {"--user", "--db", "--host", "--event"} or flag in flags: + return False + flags[flag] = value + if native: + return event in _HOOK_ENTRIES and flags.get("--event") == event and "--host" not in flags + return flags.get("--host", "claude") == "claude" and "--event" not in flags def _yaml_safe_load(text: str) -> dict: @@ -95,6 +146,11 @@ class HermesAdapter(CLIAdapter): def has_hooks(self) -> bool: return True + @property + def has_session_start(self) -> bool: + # Hermes discards observer output; first-turn recall uses pre_llm_call. + return False + @property def name(self) -> str: return "Hermes Agent" @@ -168,22 +224,14 @@ def install_hooks( for event, info in _HOOK_ENTRIES.items(): script_path = hooks_dir / info["script"] - cmd = self._build_command(py, script_path, user_id, db_path) + cmd = self._build_command(py, script_path, user_id, db_path, event=event) # Ensure the event key is a list event_list = hooks.setdefault(event, []) if not isinstance(event_list, list): - event_list = [] - hooks[event] = event_list - - # Skip if a truememory command is already registered for this event - already = any( - isinstance(entry, dict) - and _TRUEMEMORY_MARKER in entry.get("command", "").lower() - for entry in event_list - ) - if already: - continue + raise ValueError(f"Hermes {event} hooks must be a list") + event_list[:] = [entry for entry in event_list + if not (isinstance(entry, dict) and _owned_command(entry.get("command"), event))] entry: dict = {"command": cmd} if info.get("timeout"): @@ -217,8 +265,12 @@ def _build_command( script_path: Path, user_id: str = "", db_path: str = "", + *, + event: str = "", ) -> str: parts: list[str] = [python_path, str(script_path)] + if event: + parts.extend(["--event", event]) if user_id: parts.extend(["--user", user_id]) if db_path: @@ -252,7 +304,7 @@ def _has_hook_entries(self) -> bool: for entry in entries: if ( isinstance(entry, dict) - and _TRUEMEMORY_MARKER in entry.get("command", "").lower() + and _owned_command(entry.get("command"), _event) ): return True return False @@ -289,7 +341,7 @@ def _remove_hook_entries(self) -> None: entry for entry in entries if not ( isinstance(entry, dict) - and _TRUEMEMORY_MARKER in entry.get("command", "").lower() + and _owned_command(entry.get("command"), event) ) ] if len(cleaned) != len(entries): diff --git a/truememory/hooks/adapters/kimi.py b/truememory/hooks/adapters/kimi.py index 1b25a0c5..7470cf48 100644 --- a/truememory/hooks/adapters/kimi.py +++ b/truememory/hooks/adapters/kimi.py @@ -124,11 +124,15 @@ def install_hooks( script_path = hooks_dir / info["script"] cmd = self._build_command(py, script_path, user_id, db_path) + toml_command = json.dumps(cmd, ensure_ascii=False) + # TOML forbids DEL; splitlines also treats these Unicode characters as boundaries. + for codepoint in (0x7F, 0x85, 0x2028, 0x2029): + toml_command = toml_command.replace(chr(codepoint), f"\\u{codepoint:04x}") lines_to_append.append("") lines_to_append.append("[[hooks]]") lines_to_append.append(f'event = "{event}"') - lines_to_append.append(f'command = "{cmd}"') + lines_to_append.append(f"command = {toml_command}") lines_to_append.append(f'timeout = {info["timeout"]}') if lines_to_append: @@ -190,10 +194,15 @@ def _parse_existing_hooks(toml_text: str) -> list[dict]: line = line.strip() if not line or line.startswith("["): break - m = re.match(r'(\w+)\s*=\s*"([^"]*)"', line) + m = re.match(r'(\w+)\s*=\s*("(?:\\.|[^"\\])*")\s*(?:#.*)?$', line) if m: - entry[m.group(1)] = m.group(2) + try: + entry[m.group(1)] = json.loads(m.group(2)) + except json.JSONDecodeError: + return [] else: + if re.match(r'\w+\s*=\s*"', line): + return [] m = re.match(r'(\w+)\s*=\s*(\d+)', line) if m: entry[m.group(1)] = m.group(2) diff --git a/truememory/hooks/adapters/openclaw.py b/truememory/hooks/adapters/openclaw.py index 618d11c1..66669b61 100644 --- a/truememory/hooks/adapters/openclaw.py +++ b/truememory/hooks/adapters/openclaw.py @@ -6,12 +6,10 @@ The runtime reads ``sourceConfig.mcp?.servers`` in mcp-config.ts and all write-back functions (setConfiguredMcpServer, etc.) write to ``next.mcp = { ...next.mcp, servers }``. -- ~/.openclaw/plugins// for plugins (openclaw.plugin.json + index.js) +- ~/.openclaw/extensions// for plugins (openclaw.plugin.json + index.js) using ``export default { id, name, register(api) }`` — the plugin loader calls the ``register(api)`` function at startup. -- Plugin lifecycle hooks: session_start, session_end, before_tool_call, - before_compaction (registered via ``api.on("event", handler)`` inside - ``register(api)`` callback) +- A recall-only before_prompt_build hook registered via ``api.on``. - Container-based skills can be enabled/disabled in the ``skills:`` section of the main config @@ -38,7 +36,7 @@ _OPENCLAW_DIR = Path.home() / ".openclaw" _CONFIG_PATH = _OPENCLAW_DIR / "openclaw.json" -_PLUGINS_DIR = _OPENCLAW_DIR / "plugins" +_PLUGINS_DIR = _OPENCLAW_DIR / "extensions" _PLUGIN_NAME = "truememory" _TEMPLATE_DIR = Path(__file__).parent.parent / "templates" / "openclaw" @@ -114,6 +112,10 @@ class OpenClawAdapter(CLIAdapter): def has_hooks(self) -> bool: return True + @property + def has_session_start(self) -> bool: + return False + @property def name(self) -> str: return "OpenClaw" @@ -179,12 +181,42 @@ def install_hooks( dst = plugin_dir / template_file if src.exists(): content = src.read_text(encoding="utf-8") - if python_path: + if template_file == "index.js": content = content.replace( - 'process.env.TRUEMEMORY_PYTHON || "python3"', - f'process.env.TRUEMEMORY_PYTHON || "{python_path}"', + 'const INSTALLED_CONFIG = {"pythonPath": "python3", "userId": "", "dbPath": ""};', + "const INSTALLED_CONFIG = " + json.dumps({ + "pythonPath": python_path or sys.executable, + "userId": user_id, + "dbPath": db_path, + }) + ";", ) atomic_write_text(dst, content) + self._configure_hook_permission() + + def _configure_hook_permission(self) -> None: + try: + if _CONFIG_PATH.exists(): + text = _CONFIG_PATH.read_text(encoding="utf-8") + config = json.loads(_strip_json5_comments(text)) + else: + config = {} + except (OSError, json.JSONDecodeError): + log.warning("Cannot read OpenClaw hook policy in %s; preserving config", _CONFIG_PATH) + return + if not isinstance(config, dict): + return + current = config + for key in ("plugins", "entries", _PLUGIN_NAME, "hooks"): + if key not in current: + current[key] = {} + child = current[key] + if not isinstance(child, dict): + return + current = child + # Existing denials and all global activation policy retain authority. + if "allowConversationAccess" not in current: + current["allowConversationAccess"] = True + atomic_write_text(_CONFIG_PATH, json.dumps(config, indent=2)) def uninstall(self) -> None: self._remove_mcp_entry() @@ -199,7 +231,8 @@ def get_system_prompt_path(self) -> Path | None: def get_system_prompt_content(self) -> str: from truememory.hooks.adapters.base import get_generic_system_prompt return get_generic_system_prompt( - has_hooks=self.has_hooks, + # The generic hook template promises capture and session autoload. + has_hooks=False, has_session_start=self.has_session_start, ) diff --git a/truememory/hooks/templates/openclaw/index.js b/truememory/hooks/templates/openclaw/index.js index 3680bf0b..65a4f897 100644 --- a/truememory/hooks/templates/openclaw/index.js +++ b/truememory/hooks/templates/openclaw/index.js @@ -1,148 +1,148 @@ -/** - * TrueMemory plugin for OpenClaw. - * - * Hooks into the agent lifecycle to recall memories before each run - * and trigger extraction after each run. Uses the TrueMemory MCP server - * for memory operations. - * - * OpenClaw plugin API: - * export default { id, name, register(api) } - * api.on("session_start", handler) — before agent starts processing - * api.on("session_end", handler) — after agent finishes - * api.on("before_tool_call", handler) — before each tool invocation - * api.on("before_compaction", handler) — before context compaction - */ -import { spawnSync, spawn } from "child_process"; -import { join } from "path"; +/** Native OpenClaw recall transport. Run IDs correlate work, not admitted turns. */ +import { spawn } from "node:child_process"; +import { createHash } from "node:crypto"; -const PYTHON_PATH = process.env.TRUEMEMORY_PYTHON || "python3"; -const HOOKS_DIR = process.env.TRUEMEMORY_HOOKS_DIR || ""; +const INSTALLED_CONFIG = {"pythonPath": "python3", "userId": "", "dbPath": ""}; +const WORKER_TIMEOUT_MS = 4000; +const MAX_PROMPT_BYTES = 128 * 1024; +const MAX_OUTPUT_BYTES = 64 * 1024; +const MAX_WORKERS = 4; -function getHooksDir() { - if (HOOKS_DIR) return HOOKS_DIR; - try { - const result = spawnSync( - PYTHON_PATH, - ["-c", "from pathlib import Path; import truememory; print(Path(truememory.__file__).parent / 'ingest' / 'hooks')"], - { encoding: "utf-8", timeout: 10000 } - ); - return (result.stdout || "").trim(); - } catch { - return ""; - } +function validIdentity(value) { + return typeof value === "string" && value.trim().length > 0 && + value.length <= 512 && !/[\x00-\x1f\x7f]/.test(value); } -function runHookSync(hooksDir, script, input, timeoutMs) { - try { - const result = spawnSync( - PYTHON_PATH, - [join(hooksDir, script)], - { input, encoding: "utf-8", timeout: timeoutMs } - ); - return (result.stdout || "").trim(); - } catch { - return ""; +function assertInvocationCurrent(ctx) { + if (ctx.hookInvocation !== undefined) { + if (typeof ctx.hookInvocation?.assertActive !== "function") throw new Error("Invalid invocation"); + ctx.hookInvocation.assertActive(); + } + if (ctx.assertMemoryAudienceCurrent !== undefined) { + if (typeof ctx.assertMemoryAudienceCurrent !== "function") throw new Error("Invalid audience"); + ctx.assertMemoryAudienceCurrent(); } } +function recallWorker(config, input, workers) { + return new Promise((resolve) => { + let child; + let timer; + let settled = false; + let outputBytes = 0; + const chunks = []; + const finish = (result) => { + if (settled) return; + settled = true; + clearTimeout(timer); + resolve(result); + }; + const rejectWorker = () => { + finish(undefined); + try { + if (child) child.kill("SIGKILL"); + } catch { + return; + } + }; + try { + child = spawn(config.pythonPath, [ + "-m", "truememory.ingest.hooks.openclaw", "--event", "before_prompt_build", + "--user", config.userId, "--db", config.dbPath, + ], { stdio: ["pipe", "pipe", "ignore"], shell: false }); + workers.add(child); + timer = setTimeout(rejectWorker, WORKER_TIMEOUT_MS); + child.on("error", rejectWorker); + child.stdin.on("error", rejectWorker); + child.stdout.on("error", rejectWorker); + child.stdout.on("data", (chunk) => { + if (settled) return; + const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); + outputBytes += bytes.length; + if (outputBytes > MAX_OUTPUT_BYTES) { + rejectWorker(); + return; + } + chunks.push(bytes); + }); + child.on("close", (code, signal) => { + workers.delete(child); + if (settled) return; + if (code !== 0 || signal) { + finish(undefined); + return; + } + try { + const result = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks))); + if (!result || Array.isArray(result) || Object.keys(result).length !== 1 || + typeof result.context !== "string") { + finish(undefined); + return; + } + finish(result.context); + } catch { + finish(undefined); + } + }); + child.stdin.end(JSON.stringify(input)); + } catch { + rejectWorker(); + } + }); +} + export default { id: "truememory", name: "TrueMemory", - description: "TrueMemory persistent memory integration for OpenClaw", + description: "TrueMemory recall-only integration for OpenClaw", register(api) { - const hooksDir = getHooksDir(); - if (!hooksDir) { - console.error("[truememory] Could not locate hook scripts"); + const supplied = api.pluginConfig ?? {}; + const config = { + pythonPath: supplied.pythonPath ?? INSTALLED_CONFIG.pythonPath, + userId: supplied.userId ?? INSTALLED_CONFIG.userId, + dbPath: supplied.dbPath ?? INSTALLED_CONFIG.dbPath, + }; + if (!config.pythonPath || Object.values(config).some((value) => + typeof value !== "string" || value.length > 8192 || value.includes("\0"))) { + api.logger?.warn("TrueMemory recall configuration is invalid"); return; } - - let lastProcessedPrompt = null; - let toolCallsSinceLastPrompt = 0; - - api.on("session_start", async (event) => { - lastProcessedPrompt = null; - toolCallsSinceLastPrompt = 0; + const pending = new Map(); + const workers = new Set(); + api.on("before_prompt_build", async (event, ctx) => { try { - const input = JSON.stringify({ - session_id: event.sessionId || "openclaw", - cwd: process.cwd(), - transcript_path: event.transcriptPath || "", - }); - const result = runHookSync(hooksDir, "session_start.py", input, 10000); - if (result) { - const parsed = JSON.parse(result); - if (parsed.additionalContext) { - event.additionalContext = (event.additionalContext || "") + "\n" + parsed.additionalContext; - } + if (!event || !ctx || ctx.trigger !== "user" || + !validIdentity(ctx.sessionId) || !validIdentity(ctx.runId)) return; + if ([ctx.agentId, ctx.sessionKey].some((value) => + value !== undefined && !validIdentity(value))) return; + if (ctx.inputProvenance !== undefined && ctx.inputProvenance?.kind !== "external_user") return; + const prompt = Object.hasOwn(event, "currentUserMessage") ? event.currentUserMessage : event.prompt; + const hasAdmissionId = Object.hasOwn(event, "currentUserMessageId"); + const admissionId = hasAdmissionId ? event.currentUserMessageId : null; + if (hasAdmissionId && !validIdentity(admissionId)) return; + if (typeof prompt !== "string" || prompt.trim().length < 3 || + Buffer.byteLength(prompt, "utf8") > MAX_PROMPT_BYTES || prompt.includes("\0")) return; + assertInvocationCurrent(ctx); + const sessionId = "openclaw_" + createHash("sha256") + .update(JSON.stringify([ctx.agentId ?? "", ctx.sessionKey ?? "", ctx.sessionId])) + .digest("hex").slice(0, 48); + const runId = createHash("sha256").update(ctx.runId).digest("hex"); + const key = createHash("sha256").update(JSON.stringify([sessionId, runId, admissionId, prompt])).digest("hex"); + let work = pending.get(key); + if (!work) { + if (workers.size >= MAX_WORKERS) return; + work = recallWorker(config, { session_id: sessionId, run_id: runId, prompt }, workers); + pending.set(key, work); + work.finally(() => { + if (pending.get(key) === work) pending.delete(key); + }); } - } catch (err) { - // Never block the agent run - } - }); - - api.on("session_end", async (event) => { - try { - const input = JSON.stringify({ - session_id: event.sessionId || "openclaw", - transcript_path: event.transcriptPath || "", - }); - const child = spawn(PYTHON_PATH, [join(hooksDir, "stop.py")], { - stdio: ["pipe", "ignore", "ignore"], - detached: true, - }); - child.on("error", () => {}); - if (child.stdin && !child.stdin.destroyed) { - child.stdin.on("error", () => {}); - child.stdin.write(input); - child.stdin.end(); - } - child.unref(); - } catch (err) { - // Never block agent shutdown - } - }); - - api.on("before_tool_call", async (event) => { - const prompt = event.lastUserPrompt ?? event.userPrompt ?? null; - - if (prompt !== null) { - if (!prompt || prompt === lastProcessedPrompt) return; - lastProcessedPrompt = prompt; - toolCallsSinceLastPrompt = 0; - } else { - // Field not present — fall back to counter-based dedup. If the - // prompt field is never provided by this OpenClaw version, the - // hook fires once per session (known limitation — no turn-boundary - // event available to reset the counter). - if (toolCallsSinceLastPrompt === 0) { - console.debug("[truememory] before_tool_call has no prompt field; using counter-based dedup"); - } - toolCallsSinceLastPrompt++; - if (toolCallsSinceLastPrompt > 1) return; - } - - try { - const input = JSON.stringify({ - session_id: event.sessionId || "openclaw", - cwd: process.cwd(), - user_prompt: prompt || "", - }); - runHookSync(hooksDir, "user_prompt_submit.py", input, 5000); - } catch (err) { - // Never block tool call processing - } - }); - - api.on("before_compaction", async (event) => { - try { - const input = JSON.stringify({ - session_id: event.sessionId || "openclaw", - transcript_path: event.transcriptPath || "", - }); - runHookSync(hooksDir, "compact.py", input, 5000); - } catch (err) { - // Never block compression + const context = await work; + assertInvocationCurrent(ctx); + if (typeof context === "string" && context.trim()) return { prependContext: context }; + } catch { + return; } }); }, diff --git a/truememory/hooks/templates/openclaw/openclaw.plugin.json b/truememory/hooks/templates/openclaw/openclaw.plugin.json index b6f2a124..35fbc354 100644 --- a/truememory/hooks/templates/openclaw/openclaw.plugin.json +++ b/truememory/hooks/templates/openclaw/openclaw.plugin.json @@ -9,6 +9,14 @@ "pythonPath": { "type": "string", "description": "Path to the Python interpreter with truememory installed" + }, + "userId": { + "type": "string", + "description": "Configured TrueMemory user identity for recall" + }, + "dbPath": { + "type": "string", + "description": "Configured TrueMemory database path for recall" } }, "additionalProperties": false diff --git a/truememory/hybrid.py b/truememory/hybrid.py index 8ee56ae4..ea3ac220 100644 --- a/truememory/hybrid.py +++ b/truememory/hybrid.py @@ -41,6 +41,8 @@ import sqlite3 from collections import defaultdict +from truememory.tier_switch.runtime import database_operation + log = logging.getLogger(__name__) @@ -123,6 +125,7 @@ def reciprocal_rank_fusion( _CANDIDATE_POOL = 200 +@database_operation def search_hybrid( conn: sqlite3.Connection, query: str, @@ -176,7 +179,9 @@ def search_hybrid( sep_tbl = _active_sep_table(conn) conn.execute(f"SELECT rowid FROM {sep_tbl} LIMIT 1").fetchone() _has_sep = True - except Exception: + except Exception as exc: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(exc) pass except ImportError: pass @@ -206,7 +211,9 @@ def search_hybrid( sender_count = unique_senders_row[0] if unique_senders_row else 0 if sender_count > 5: sep_results = search_vector_separation(conn, query, limit=_CANDIDATE_POOL, _query_blob=_q_blob, include_directives=include_directives) - except Exception: + except Exception as exc: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(exc) pass # ------------------------------------------------------------------ diff --git a/truememory/ingest/__init__.py b/truememory/ingest/__init__.py index ff853f37..836d077e 100644 --- a/truememory/ingest/__init__.py +++ b/truememory/ingest/__init__.py @@ -50,6 +50,8 @@ def ingest( gate_threshold: float = 0.30, llm_config: LLMConfig | None = None, session_id: str = "", + *, + require_complete: bool = False, ) -> IngestionResult: """ Ingest a conversation transcript into TrueMemory. @@ -76,6 +78,8 @@ def ingest( gate_threshold=gate_threshold, llm_config=llm_config, ) + if require_complete: + return pipeline.ingest_transcript(transcript_path, session_id=session_id, require_complete=True) return pipeline.ingest_transcript(transcript_path, session_id=session_id) diff --git a/truememory/ingest/cli.py b/truememory/ingest/cli.py index 68cbca71..d62384d5 100644 --- a/truememory/ingest/cli.py +++ b/truememory/ingest/cli.py @@ -12,6 +12,7 @@ import json import logging import os +import re import sys from pathlib import Path @@ -43,6 +44,11 @@ def main(): p_ingest.add_argument("--provider", default="auto", help="LLM provider (auto/ollama/claude_cli/openrouter/anthropic)") p_ingest.add_argument("--model", default="", help="LLM model name") p_ingest.add_argument("--session", default="", help="Session identifier to tag this ingestion trace with") + p_ingest.add_argument("--transcript-host", choices=("claude", "codex", "gemini", "cursor"), + default="claude", help=argparse.SUPPRESS) + p_ingest.add_argument("--automatic", action="store_true", help=argparse.SUPPRESS) + p_ingest.add_argument("--completion-key", default="", help=argparse.SUPPRESS) + p_ingest.add_argument("--completion-attempt", type=int, default=0, help=argparse.SUPPRESS) p_ingest.add_argument("-v", "--verbose", action="store_true", help="Verbose logging") # --- install command --- @@ -89,6 +95,17 @@ def main(): p_upgrade.add_argument("tier", choices=["edge", "base", "pro"], help="Target tier") p_upgrade.add_argument("--force", action="store_true", help="Re-embed even if tier is unchanged") + p_cancel = sub.add_parser( + "cancel-rebuild", help="Cancel one explicitly identified interrupted tier rebuild", + description=("Read truememory_status(status_id) for the matching rebuild.id and rebuild.job_id. " + "This command cancels only that marker. Retry upgrade-tier explicitly afterward."), + ) + p_cancel.add_argument("--status-id", required=True, type=_positive_rebuild_status_id, + help="Exact positive rebuild.id returned by truememory_status") + p_cancel.add_argument("--job-id", required=True, type=_rebuild_job_id, + help="Exact matching 32-character lowercase hexadecimal rebuild.job_id") + p_cancel.add_argument("--db", default=None, help="Path to the same truememory database") + # --- migrate-memory command --- p_migrate = sub.add_parser("migrate-memory", help="Migrate MEMORY.md facts into TrueMemory") p_migrate.add_argument("--path", default=None, help="Path to MEMORY.md (auto-detected if not specified)") @@ -119,13 +136,83 @@ def main(): _run_setup(args) elif args.command == "upgrade-tier": _run_upgrade_tier(args) + elif args.command == "cancel-rebuild": + _run_cancel_rebuild(args) elif args.command == "migrate-memory": _run_migrate_memory(args) else: parser.print_help() +def _positive_rebuild_status_id(value: str) -> int: + if (type(value) is not str or re.fullmatch(r"[1-9][0-9]{0,18}", value) is None + or int(value) > 9223372036854775807): + raise argparse.ArgumentTypeError("status ID must be an exact positive SQLite integer") + return int(value) + + +def _rebuild_job_id(value: str) -> str: + if type(value) is not str or re.fullmatch(r"[0-9a-f]{32}", value) is None: + raise argparse.ArgumentTypeError("job ID must be exactly 32 lowercase hexadecimal characters") + return value + + +def _run_cancel_rebuild(args: argparse.Namespace) -> None: + """Cancel one explicit marker; do not discover, retry, or rebuild anything.""" + try: + if type(args.status_id) is not int or not 1 <= args.status_id <= 9223372036854775807: + raise argparse.ArgumentTypeError("status ID must be an exact positive SQLite integer") + job_id = _rebuild_job_id(args.job_id) + except argparse.ArgumentTypeError as error: + print(f"Invalid rebuild identity: {error}", file=sys.stderr) + raise SystemExit(2) from error + + from truememory.tier_switch.manager import RebuildManager + manager = RebuildManager() + try: + result = manager.cancel(args.status_id, expected_job_id=job_id, + db_path=Path(args.db) if args.db is not None else None) + except Exception as error: + print(f"Rebuild cancellation refused: {error}", file=sys.stderr) + print("Read truememory_status(status_id) again and use its matching rebuild.id and rebuild.job_id.", file=sys.stderr) + raise SystemExit(1) from error + if result.get("status") != "cancelled" or result.get("job_id") != job_id: + print("No matching interrupted rebuild was cancelled. Read truememory_status(status_id) again.", file=sys.stderr) + raise SystemExit(1) + print(f"Rebuild {args.status_id} cancelled (job_id={job_id}).") + print("Retry the original upgrade-tier request explicitly to resume the eligible inactive prefix.") + + def _run_ingest(args): + if not getattr(args, "automatic", False): + return _execute_ingest(args) + from truememory.ingest.completion import ( + CompletionError, admit_capture, load_capture, reserve_inline_attempt, worker_claim, + ) + host = getattr(args, "transcript_host", "claude") + try: + key = getattr(args, "completion_key", "") + if key: + capture = load_capture(key, args.user, args.db, host) + if (str(Path(args.transcript).expanduser().resolve()) != str(capture.path.resolve()) + or args.session != capture.metadata["session_id"]): + raise CompletionError("worker_capture_mismatch") + attempt = getattr(args, "completion_attempt", 0) + else: + capture = admit_capture(args.transcript, args.session, args.user, args.db, host) + attempt = reserve_inline_attempt(capture) + if not attempt: + return + args.transcript = str(capture.path) + with worker_claim(capture, attempt) as allowed: + if allowed: + return _execute_ingest(args, capture) + except OSError as error: + logging.getLogger(__name__).warning("Automatic ingestion retained nonterminal work (%s)", type(error).__name__) + raise SystemExit(5) from error + + +def _execute_ingest(args, capture=None): level = logging.DEBUG if args.verbose else logging.INFO logging.basicConfig(level=level, format="%(levelname)s %(message)s") @@ -213,6 +300,7 @@ def _run_ingest(args): gate_threshold=args.threshold, llm_config=config, session_id=args.session, + require_complete=True, ) except RuntimeError as e: # auto_detect raises RuntimeError when no LLM backend is available @@ -224,34 +312,29 @@ def _run_ingest(args): if args.trace: save_trace(result, args.trace) - try: - from truememory.ingest.hooks._shared import mark_session_extracted - mark_session_extracted(args.session, args.transcript) - except Exception: - pass + from truememory.ingest.completion import strict_complete, record_receipt, drain + if not strict_complete(result, capture): + if capture is not None: + record_receipt(capture, result) + logging.getLogger(__name__).warning("Ingestion coverage remains unproven; completion and claims retained") + raise SystemExit(5) + if capture is None: + return + if not record_receipt(capture, result): + raise SystemExit(5) + host = getattr(args, "transcript_host", "claude") + drain(args.user, args.db, host) + _cascade_next(host, automatic=True, user_id=args.user, db_path=args.db) - # issue #422: this point is only reached on a confirmed-successful ingest - # (all error paths above sys.exit before here). Remove the backlog - # .processing claim now that the work is done. Crashed workers never reach - # this line, so their claim is left for the stale watcher to re-queue. - try: - from truememory.ingest.hooks._shared import clear_backlog_processing - clear_backlog_processing(args.session) - except Exception as e: - # Non-fatal: the ingest already succeeded. But if we can't drop the - # claim, the stale watcher will (eventually) restore the .processing - # marker to .json and re-queue this already-done session, wasting an - # extraction. Log it so that wasted re-runs are diagnosable instead of - # silently swallowed. - logging.getLogger(__name__).warning( - "failed to clear backlog .processing claim for session %r: %s", - args.session, e, - ) - _cascade_next() +def _clear_legacy_processing(args) -> bool: + """Explicit legacy utility; automatic completion never uses this path.""" + from truememory.ingest.hooks._shared import clear_backlog_processing + return clear_backlog_processing(args.session) -def _cascade_next() -> None: +def _cascade_next(host: str = "claude", *, automatic: bool = False, + user_id: str = "", db_path: str | None = None) -> None: """Spawn the next backlog ingest process if work remains and slots are free. This creates a self-sustaining chain: each ingest process spawns the @@ -262,6 +345,10 @@ def _cascade_next() -> None: Uses atomic rename (.json → .processing) to prevent TOCTOU races where multiple drainers read the same marker before either acquires the flock. """ + if automatic: + from truememory.ingest.completion import convert_legacy_pointers + convert_legacy_pointers(user_id, db_path, host) + return import subprocess as _sp backlog_dir = Path.home() / ".truememory" / "backlog" @@ -275,7 +362,12 @@ def _cascade_next() -> None: record_stale_processing_pid, mark_session_extracted, _quarantine_marker, + backlog_transcript_source, + normalize_transcript_host, ) + host = normalize_transcript_host(host) + if not host: + return cleanup_stale_processing(backlog_dir) try: @@ -291,7 +383,7 @@ def _cascade_next() -> None: except ImportError: return - for marker_path in markers[:1]: + for marker_path in markers: claimed_path = marker_path.with_suffix(".processing") try: marker_path.rename(claimed_path) @@ -315,9 +407,20 @@ def _cascade_next() -> None: continue try: - transcript = data.get("transcript_path", "") - if not transcript or not Path(transcript).exists(): - claimed_path.unlink(missing_ok=True) + if ( + isinstance(data, dict) + and normalize_transcript_host(data.get("transcript_host", "claude")) + and data.get("transcript_host", "claude") != host + ): + claimed_path.rename(marker_path) + continue + source = backlog_transcript_source(data, host=host) + if source is None: + _quarantine_marker(claimed_path) + continue + transcript, session_id = source + if not Path(transcript).is_file(): + _quarantine_marker(claimed_path) continue if not check_extraction_budget(): @@ -340,8 +443,9 @@ def _cascade_next() -> None: cmd = [ sys.executable, "-m", "truememory.ingest.cli", "ingest", transcript, + "--transcript-host", host, + "--automatic", ] - session_id = data.get("session_id", "") if session_id: cmd.extend(["--session", session_id]) if data.get("user_id"): @@ -369,8 +473,9 @@ def _cascade_next() -> None: # the session if the spawned worker exits non-zero. logging.getLogger(__name__).info( "Cascade: spawned next ingest for session %s (PID %d)", - data.get("session_id", "?"), proc.pid, + session_id, proc.pid, ) + return except Exception: try: claimed_path.rename(marker_path) @@ -492,12 +597,13 @@ def _save_truememory_config(config: dict) -> None: _save_config(config) -def _setup_cli_integrations(args, config): - """Step 3 of setup: detect installed CLIs and configure TrueMemory.""" +def _setup_cli_integrations(args: argparse.Namespace, config: dict) -> list[str]: + """Configure selected CLIs and return IDs whose setup failed.""" from truememory.hooks.registry import detect_installed, get_adapter from truememory.hooks.cli import install_cli cli_arg = getattr(args, "cli", "") + failures: list[str] = [] if cli_arg: requested_ids = [c.strip() for c in cli_arg.split(",") if c.strip()] @@ -505,20 +611,22 @@ def _setup_cli_integrations(args, config): adapter = get_adapter(cli_id) if adapter is None: print(f" \033[33m⚠ Unknown CLI: {cli_id}\033[0m") + failures.append(cli_id) continue print(f" Setting up {adapter.name}...") if install_cli(cli_id, user_id=config.get("user_id", "")): print(f" \033[32m✓ {adapter.name} — configured\033[0m") else: print(f" \033[31m✗ {adapter.name} — failed\033[0m") - return + failures.append(cli_id) + return failures installed = detect_installed() if not installed: print(" No supported CLIs detected. Install hooks manually later with:") print(" truememory-ingest setup --cli ") - return + return failures print(" \033[1mCLI Integration\033[0m") print(" ──────────────") @@ -553,10 +661,61 @@ def _setup_cli_integrations(args, config): print(f" \033[32m✓ {adapter.name} — MCP server configured, hooks installed\033[0m") else: print(f" \033[31m✗ {adapter.name} — setup failed\033[0m") + failures.append(adapter.cli_id) if len(selected) > 1: names = ", ".join(a.name for a in selected) - print(f"\n TrueMemory is ready. Your memories sync across {names}.") + print(f"\n CLI setup attempted for {names}. Check each result above.") + return failures + + +def _setup_model_readiness(tier: str, existing_tier: str) -> list[str]: + """Switch through certification, then exercise the admitted serving identity.""" + from truememory.maintenance import maintenance_owner + from truememory.tier_switch.manager import RebuildManager, _get_db_path, _open_db + from truememory.tier_switch.runtime import serving_operation + + failures: list[str] = [] + manager = RebuildManager() + try: + if not manager.run_rebuild_sync(tier): + return ["tier activation"] + except Exception as error: + print(f"Tier activation remains incomplete: {error}", file=sys.stderr) + return ["tier activation"] + if manager._last_action == "config_only" or {tier, existing_tier} == {"base", "pro"}: + return failures + try: + with maintenance_owner(_get_db_path()): + conn = _open_db(initialize=True) + try: + with serving_operation(conn) as operation: + if operation.tier != tier: + raise RuntimeError("Requested setup tier is not the admitted serving tier") + try: + from truememory.vector_search import _encode_with_mps_fallback, get_model + _encode_with_mps_fallback(get_model(), ["TrueMemory setup readiness check."], show_progress_bar=False) + if tier in ("base", "pro"): + print(" \033[32mQwen3-Embedding-0.6B @ 256d Matryoshka ready\033[0m") + else: + print(" \033[32mpotion-base-8M (256-dim) ready\033[0m") + except Exception as error: + failures.append("embedding model") + print(f"Embedding model readiness check failed: {error}", file=sys.stderr) + try: + from truememory.reranker import get_reranker + get_reranker().predict([("TrueMemory setup readiness check.", "TrueMemory setup readiness check.")], + show_progress_bar=False) + print(" \033[32mCross-encoder reranker ready\033[0m") + except Exception as error: + failures.append("reranker") + print(f"Reranker readiness check failed: {error}", file=sys.stderr) + finally: + conn.close() + except Exception as error: + failures.append("serving admission") + print(f"Serving readiness admission failed: {error}", file=sys.stderr) + return failures def _run_setup(args): @@ -586,30 +745,7 @@ def _run_setup(args): print(f" \033[33m⚠ Invalid choice '{choice}', defaulting to Edge tier.\033[0m") tier = "edge" - # Pre-load the embedding model so first search isn't slow - print() - print(" \033[1mDownloading embedding model...\033[0m") - try: - os.environ["TRUEMEMORY_EMBED_MODEL"] = tier - from truememory.vector_search import set_embedding_model, get_model - set_embedding_model(tier) - get_model() # triggers download if not cached - if tier in ("base", "pro"): - print(" \033[32m✓ Qwen3-Embedding-0.6B @ 256d Matryoshka ready\033[0m") - else: - print(" \033[32m✓ potion-base-8M (256-dim) ready\033[0m") - except Exception as e: - print(f" \033[33m⚠ Model download failed: {e}\033[0m") - print(" The model will download on first use instead.") - - # Also pre-download the cross-encoder reranker if available - try: - from truememory.reranker import get_reranker - print(" Downloading reranker model...") - get_reranker() - print(" \033[32m✓ Cross-encoder reranker ready\033[0m") - except Exception: - pass # Optional component, don't fail setup + model_failures = _setup_model_readiness(tier, existing_tier) # ── Step 2: API key for HyDE / deep search ─────────────────────── print() @@ -680,25 +816,46 @@ def _run_setup(args): from getpass import getpass as _getpass api_key = _getpass(f" {prompt_text}: ").strip() - # ── Save config ─────────────────────────────────────────────────── - config["tier"] = tier + # Independent settings cannot publish a requested tier before certification. + from truememory.tier_switch.projection import ( + CONFIG_WRITE_LOCK, ConfigFileLock, _read_config, _write_config, patch_config_fields, + ) + fields = {} if llm_provider: - config["llm_provider"] = llm_provider + fields["llm_provider"] = llm_provider if api_key_field and api_key: - config[api_key_field] = api_key - - _save_truememory_config(config) + fields[api_key_field] = api_key + config = patch_config_fields(fields, config_path=_TRUEMEMORY_CONFIG_PATH) + if tier == "edge" and not existing_tier: + with ConfigFileLock(_TRUEMEMORY_CONFIG_PATH.with_name("config.json.lock"), strict=True), CONFIG_WRITE_LOCK: + config = _read_config(_TRUEMEMORY_CONFIG_PATH) + if "tier" not in config: + config["tier"] = "edge" + _write_config(_TRUEMEMORY_CONFIG_PATH, config) print() print(f" \033[32m✓ Config saved to {_TRUEMEMORY_CONFIG_PATH}\033[0m") # ── Step 3: CLI integration ───────────────────────────────────────── print() - _setup_cli_integrations(args, config) + integration_failures = _setup_cli_integrations(args, config) # ���─ Done ────────────────────────────────────────────────────────── print() print(" \033[1;33m══════════════════════════════════════════════\033[0m") - print(" \033[1mSetup complete!\033[0m") + if model_failures or integration_failures: + print(" Setup saved; readiness is incomplete.") + if model_failures: + print(" Selected-tier model readiness is incomplete.") + print(f" Failed model checks: {', '.join(model_failures)}") + print(" Restore model-download access or fix the model error above, then retry:") + print(" truememory-ingest setup --non-interactive") + if integration_failures: + print(f" Failed CLI integrations: {', '.join(integration_failures)}") + print(" Resolve the CLI setup errors above, then retry using a supported CLI name:") + print(" truememory-ingest setup --non-interactive --cli ") + print(" Existing installation and saved configuration were retained.") + else: + print(" \033[1mSetup complete!\033[0m") print(f" Tier: {tier}") print(f" Provider: {llm_provider or 'none (basic search only)'}") print() @@ -706,81 +863,35 @@ def _run_setup(args): print() print(" \033[2mThanks for using TrueMemory, a Sauron company.\033[0m") print() + if model_failures or integration_failures: + raise SystemExit(1) def _run_upgrade_tier(args): """Switch embedding tier and re-embed all memories with the new model.""" tier = args.tier.lower().strip() config = _load_truememory_config() - old_tier = config.get("tier", "edge") - - if tier == old_tier and not args.force: - print(f"Already on {tier} tier. Use --force to re-embed anyway.") - return - - from truememory.tier_switch.cache import get_transition_action - - action = get_transition_action(old_tier, tier, force=args.force) - - if action == "noop": - print(f"Already on {tier} tier.") - return - - if action == "config_only": - config["tier"] = tier - _save_truememory_config(config) - os.environ["TRUEMEMORY_EMBED_MODEL"] = tier - from truememory.vector_search import set_embedding_model - set_embedding_model(tier) - from truememory.reranker import set_active_tier - set_active_tier(tier) - print(f"\033[32m✓ Switched to {tier} instantly (same embedding model)\033[0m") - return + from truememory.tier_switch.manager import RebuildManager - print(f"Switching from {old_tier} to {tier}...") + def progress_callback(processed, total, metrics): + pct = 100.0 * processed / max(1, total) + print(f"\r Re-embedding: {pct:.0f}% ({processed}/{total})", end="", flush=True) - os.environ.pop("HF_HUB_OFFLINE", None) - os.environ.pop("TRANSFORMERS_OFFLINE", None) + manager = RebuildManager() try: - os.environ["TRUEMEMORY_EMBED_MODEL"] = tier - from truememory.vector_search import set_embedding_model - set_embedding_model(tier) - - from truememory.reranker import set_active_tier - set_active_tier(tier) - - from truememory.tier_switch.manager import RebuildManager - - def progress_callback(processed, total, metrics): - pct = metrics.get("progress_pct", 0) - eta = metrics.get("eta_seconds", 0) - bs = metrics.get("batch_size", 0) - ram = metrics.get("ram_pct", 0) - print( - f"\r Re-embedding: {pct:.0f}% ({processed}/{total}) " - f"| ETA {eta:.0f}s | BS={bs} | RAM={ram:.0f}%", - end="", flush=True, - ) - - manager = RebuildManager() - success = manager.run_rebuild_sync( - target_tier=tier, - force=args.force, - progress_callback=progress_callback, - ) - - if success: - print(f"\n \033[32m✓ Tier switched to {tier}\033[0m") - else: - print("\n \033[31m✗ Rebuild failed. Run again to retry.\033[0m") - sys.exit(1) - except Exception as e: - print(f"\n\033[31mError during tier switch: {e}\033[0m", file=sys.stderr) - print("Config was saved. Re-run to retry, or use truememory-ingest setup.") - sys.exit(1) - finally: - os.environ["HF_HUB_OFFLINE"] = "1" - os.environ["TRANSFORMERS_OFFLINE"] = "1" + success = manager.run_rebuild_sync(target_tier=tier, force=args.force, progress_callback=progress_callback) + except Exception as error: + state = "activation pending" if manager._last_outcome == "activation_pending" else "switch incomplete" + print(f"Tier {state}: {error}", file=sys.stderr) + print("Read truememory_status() for rebuild.id and its matching rebuild.job_id. " + "For interrupted work, use truememory-ingest cancel-rebuild --status-id ID --job-id JOB_ID, " + "then retry the original request explicitly.", file=sys.stderr) + raise SystemExit(1) from error + if not success: + print("Tier activation is incomplete. Read truememory_status() before retrying. " + "Use its matching rebuild.id and rebuild.job_id with cancel-rebuild only for interrupted work.", file=sys.stderr) + raise SystemExit(1) + config = _load_truememory_config() _tier_descriptions = { "edge": "Edge — 89.6% LoCoMo, Model2Vec + MiniLM", @@ -848,47 +959,8 @@ def _run_install(args): py = sys.executable if args.dry_run: - hooks_dir = Path(__file__).parent / "hooks" - hook_files = { - "SessionStart": hooks_dir / "session_start.py", - "UserPromptSubmit": hooks_dir / "user_prompt_submit.py", - "SessionEnd": hooks_dir / "stop.py", - "PreCompact": hooks_dir / "compact.py", - } - import shlex - _HOOK_MODULES = { - "session_start.py": "truememory.ingest.hooks.session_start", - "user_prompt_submit.py": "truememory.ingest.hooks.user_prompt_submit", - "stop.py": "truememory.ingest.hooks.stop", - "compact.py": "truememory.ingest.hooks.compact", - } - def _build_command(hook_path: Path) -> str: - module = _HOOK_MODULES.get(hook_path.name) - if module: - parts: list[str] = [py, "-m", module] - else: - parts = [py, str(hook_path)] - if args.user: - parts.extend(["--user", args.user]) - if args.db: - parts.extend(["--db", args.db]) - if sys.platform == "win32": - import subprocess as _sp - return _sp.list2cmdline(parts) - return " ".join(shlex.quote(p) for p in parts) - - settings = { - "hooks": { - event: [{ - "matcher": "", - "hooks": [{ - "type": "command", - "command": _build_command(path), - }], - }] - for event, path in hook_files.items() - } - } + from truememory.hooks.adapters.claude import claude_hook_settings + settings = claude_hook_settings(py, args.user, args.db or "") print("Add the following to your Claude Code settings.json:\n") print(json.dumps(settings, indent=2)) return diff --git a/truememory/ingest/completion.py b/truememory/ingest/completion.py new file mode 100644 index 00000000..afc4d7f8 --- /dev/null +++ b/truememory/ingest/completion.py @@ -0,0 +1,770 @@ +"""Bounded, retained completion evidence for observed transcript bytes. + +Receipts certify one immutable capture, never a whole native turn/session. +Legacy size/PID markers are advisory and do not participate in this protocol. +""" +from __future__ import annotations + +import contextlib +import hashlib +import json +import logging +import os +import re +import stat +import subprocess +import sys +import time +import uuid +from dataclasses import dataclass +from itertools import islice +from pathlib import Path +from types import SimpleNamespace +from typing import Iterator + +log = logging.getLogger(__name__) +MAX_CAPTURE_BYTES = 16 * 1024 * 1024 +MAX_ACCOUNT_BYTES = 64 * 1024 * 1024 +MAX_CAPTURES = 1024 +MAX_STATE_BYTES = 8 * 1024 * 1024 +MAX_ATTEMPTS = 5 +MAX_RECEIPT_BYTES = 32768 +_KEY = re.compile(r"[0-9a-f]{64}") +_COUNTS = ("facts_extracted", "facts_stored", "facts_updated", "facts_skipped_gate", "facts_skipped_dedup") + + +class CompletionError(OSError): + """Retained nonterminal failure; never permission to discard evidence.""" + + +def _json(value: object) -> bytes: + return json.dumps(value, ensure_ascii=False, sort_keys=True, + separators=(",", ":"), allow_nan=False).encode("utf-8") + + +def _digest(value: object) -> str: + return hashlib.sha256(_json(value)).hexdigest() + + +def _text(value: object, limit: int = 4096) -> str: + if type(value) is not str or not value or "\0" in value: + raise CompletionError("invalid_identity") + try: + if len(value.encode("utf-8")) > limit: + raise CompletionError("identity_limit") + except UnicodeEncodeError as error: + raise CompletionError("invalid_identity") from error + return value + + +def normalized_database(value: str | None) -> str: + return str(Path(value or Path.home() / ".truememory" / "memories.db").expanduser().resolve()) + + +def account_root(user_id: str, db_path: str | None, root: Path | None = None) -> Path: + if type(user_id) is not str or "\0" in user_id: + raise CompletionError("invalid_account") + if user_id: + _text(user_id) + base = root or Path(os.environ.get("TRUEMEMORY_BACKLOG_DIR", str(Path.home() / ".truememory" / "backlog"))).parent / "ingest-completion-v1" + return base / _digest([user_id, normalized_database(db_path)]) + + +def _private_directory(directory: Path) -> None: + # Do not follow an existing link or change permissions of foreign paths. + if directory.is_symlink(): + raise CompletionError("unsafe_directory") + directory.mkdir(parents=True, mode=0o700, exist_ok=True) + for value in (directory.parent, directory): + info = value.lstat() + if not stat.S_ISDIR(info.st_mode) or value.is_symlink(): + raise CompletionError("unsafe_directory") + if os.name != "nt" and hasattr(os, "getuid") and info.st_uid != os.getuid(): + raise CompletionError("foreign_directory") + if value == directory and os.name != "nt" and info.st_mode & 0o077: + raise CompletionError("nonprivate_directory") + if os.name == "nt": + _windows_private_directory(directory) + + +def _account_directory(directory: Path) -> None: + _private_directory(directory.parent) + _private_directory(directory) + + +def _open_private(path: Path, flags: int) -> int: + if path.is_symlink(): + raise CompletionError("unsafe_file") + fd = os.open(path, flags | getattr(os, "O_NOFOLLOW", 0), 0o600) + try: + info = os.fstat(fd) + if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1: + raise CompletionError("unsafe_file") + if os.name != "nt" and hasattr(os, "getuid") and info.st_uid != os.getuid(): + raise CompletionError("foreign_file") + if os.name != "nt" and info.st_mode & 0o077: + raise CompletionError("nonprivate_file") + if os.name == "nt": + try: + import msvcrt + except ImportError as error: + raise CompletionError("private_ownership_unavailable") from error + if not _windows_security(msvcrt.get_osfhandle(fd), private=True): + raise CompletionError("nonprivate_file") + except OSError: + os.close(fd) + raise + return fd + + +@contextlib.contextmanager +def _lock(path: Path, *, blocking: bool = True) -> Iterator[bool]: + fd = _open_private(path, os.O_CREAT | os.O_RDWR) + acquired = False + try: + if os.name == "nt": + try: + import msvcrt + except ImportError as error: + raise CompletionError("os_ownership_unavailable") from error + if os.fstat(fd).st_size == 0: + os.write(fd, b"0") + os.lseek(fd, 0, os.SEEK_SET) + try: + msvcrt.locking(fd, msvcrt.LK_LOCK if blocking else msvcrt.LK_NBLCK, 1) + acquired = True + except OSError: + if blocking: + raise + else: + try: + import fcntl + except ImportError as error: + raise CompletionError("os_ownership_unavailable") from error + try: + fcntl.flock(fd, fcntl.LOCK_EX | (0 if blocking else fcntl.LOCK_NB)) + acquired = True + except BlockingIOError: + pass + yield acquired + finally: + if acquired: + if os.name == "nt": + os.lseek(fd, 0, os.SEEK_SET) + msvcrt.locking(fd, msvcrt.LK_UNLCK, 1) + else: + fcntl.flock(fd, fcntl.LOCK_UN) + os.close(fd) + + +def _read_private(path: Path, limit: int) -> bytes: + fd = _open_private(path, os.O_RDONLY) + with os.fdopen(fd, "rb") as handle: + data = handle.read(limit + 1) + if len(data) > limit: + raise CompletionError("retained_state_limit") + return data + + +def _state(directory: Path) -> dict: + target = directory / "state.json" + if target.is_symlink(): + raise CompletionError("unsafe_state") + if not target.exists(): + return {"version": 1, "captures": {}} + try: + value = json.loads(_read_private(target, MAX_STATE_BYTES)) + except (ValueError, UnicodeDecodeError) as error: + raise CompletionError("invalid_retained_state") from error + if (type(value) is not dict or type(value.get("version")) is not int + or value["version"] != 1 or type(value.get("captures")) is not dict + or len(value["captures"]) > MAX_CAPTURES): + raise CompletionError("invalid_retained_state") + return value + + +def _save_state(directory: Path, value: dict) -> None: + payload = _json(value) + retained = sum(path.lstat().st_size for path in directory.glob("state-*.pending")) + if len(payload) + retained > MAX_STATE_BYTES: + raise CompletionError("retained_state_limit") + if (directory / "state.json").is_symlink(): + raise CompletionError("unsafe_state") + # Unique failed temporaries are retained rather than replacing evidence. + temporary = directory / ("state-" + uuid.uuid4().hex + ".pending") + fd = _open_private(temporary, os.O_CREAT | os.O_EXCL | os.O_WRONLY) + with os.fdopen(fd, "wb") as handle: + handle.write(payload) + handle.flush() + os.fsync(handle.fileno()) + os.replace(temporary, directory / "state.json") + _sync_directory(directory) + + +def _sync_directory(directory: Path) -> None: + if os.name == "nt": + return + fd = os.open(directory, os.O_RDONLY | getattr(os, "O_DIRECTORY", 0)) + try: + os.fsync(fd) + finally: + os.close(fd) + + +def _immutable(path: Path, data: bytes) -> None: + try: + fd = _open_private(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY) + except FileExistsError: + if _read_private(path, max(len(data), MAX_STATE_BYTES)) != data: + raise CompletionError("immutable_evidence_conflict") + return + with os.fdopen(fd, "wb") as handle: + handle.write(data) + handle.flush() + os.fsync(handle.fileno()) + _sync_directory(path.parent) + + +def _file_identity(info: os.stat_result) -> list[int]: + return [info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns, info.st_ctime_ns] + + +def _windows_security(handle: int, *, private: bool = False) -> bool: + """Compare documented file-owner/token SIDs; unsupported ACLs fail closed.""" + import ctypes + from ctypes import wintypes + try: + security = ctypes.WinDLL("advapi32", use_last_error=True) + kernel = ctypes.WinDLL("kernel32", use_last_error=True) + pointer = ctypes.c_void_p + security.GetSecurityInfo.argtypes = [wintypes.HANDLE, wintypes.DWORD, wintypes.DWORD, ctypes.POINTER(pointer), pointer, ctypes.POINTER(pointer), pointer, ctypes.POINTER(pointer)] + security.GetSecurityInfo.restype = wintypes.DWORD + security.OpenProcessToken.argtypes = [wintypes.HANDLE, wintypes.DWORD, ctypes.POINTER(wintypes.HANDLE)] + security.OpenProcessToken.restype = wintypes.BOOL + security.GetTokenInformation.argtypes = [wintypes.HANDLE, wintypes.DWORD, pointer, wintypes.DWORD, ctypes.POINTER(wintypes.DWORD)] + security.GetTokenInformation.restype = wintypes.BOOL + security.EqualSid.argtypes = [pointer, pointer] + security.EqualSid.restype = wintypes.BOOL + security.IsWellKnownSid.argtypes = [pointer, wintypes.DWORD] + security.IsWellKnownSid.restype = wintypes.BOOL + security.GetAclInformation.argtypes = [pointer, pointer, wintypes.DWORD, wintypes.DWORD] + security.GetAclInformation.restype = wintypes.BOOL + security.GetAce.argtypes = [pointer, wintypes.DWORD, ctypes.POINTER(pointer)] + security.GetAce.restype = wintypes.BOOL + kernel.GetCurrentProcess.restype = wintypes.HANDLE + kernel.CloseHandle.argtypes = [wintypes.HANDLE] + kernel.CloseHandle.restype = wintypes.BOOL + kernel.LocalFree.argtypes = [pointer] + kernel.LocalFree.restype = pointer + owner, descriptor, dacl = pointer(), pointer(), pointer() + token = wintypes.HANDLE() + try: + if security.GetSecurityInfo(handle, 1, 1 | (4 if private else 0), ctypes.byref(owner), None, + ctypes.byref(dacl) if private else None, None, ctypes.byref(descriptor)): + return False + if not owner or not security.OpenProcessToken(kernel.GetCurrentProcess(), 8, ctypes.byref(token)): + return False + needed = wintypes.DWORD() + ctypes.set_last_error(0) + queried = security.GetTokenInformation(token, 1, None, 0, ctypes.byref(needed)) + if queried or ctypes.get_last_error() != 122 or not 1 <= needed.value <= 65536: + return False + buffer = ctypes.create_string_buffer(needed.value) + if not security.GetTokenInformation(token, 1, buffer, needed.value, ctypes.byref(needed)): + return False + user_sid = pointer.from_buffer(buffer) + if not security.EqualSid(owner, user_sid): + return False + if not private: + return True + if not dacl: + return False + class AclSize(ctypes.Structure): + _fields_ = [("count", wintypes.DWORD), ("used", wintypes.DWORD), ("free", wintypes.DWORD)] + size = AclSize() + if not security.GetAclInformation(dacl, ctypes.byref(size), ctypes.sizeof(size), 2) or size.count > 1024: + return False + for index in range(size.count): + ace = pointer() + if not security.GetAce(dacl, index, ctypes.byref(ace)): + return False + header = ctypes.string_at(ace, 4) + if header[0] == 1 or header[1] & 8: + continue + if header[0] != 0 or int.from_bytes(header[2:4], "little") < 16: + return False + sid = pointer(ace.value + 8) + if not (security.EqualSid(sid, user_sid) or security.IsWellKnownSid(sid, 22) or security.IsWellKnownSid(sid, 26)): + return False + return True + finally: + if token: + kernel.CloseHandle(token) + if descriptor: + kernel.LocalFree(descriptor) + except (OSError, AttributeError, ImportError, ValueError): + return False + + +def _windows_private_directory(path: Path) -> None: + import ctypes + from ctypes import wintypes + try: + kernel = ctypes.WinDLL("kernel32", use_last_error=True) + kernel.CreateFileW.argtypes = [wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD, ctypes.c_void_p, + wintypes.DWORD, wintypes.DWORD, wintypes.HANDLE] + kernel.CreateFileW.restype = wintypes.HANDLE + kernel.CloseHandle.argtypes = [wintypes.HANDLE] + kernel.CloseHandle.restype = wintypes.BOOL + handle = kernel.CreateFileW(str(path), 0x20080, 7, None, 3, 0x02200000, None) + if handle in (None, ctypes.c_void_p(-1).value): + raise CompletionError("private_ownership_unavailable") + try: + if not _windows_security(handle, private=True): + raise CompletionError("private_ownership_unavailable") + finally: + kernel.CloseHandle(handle) + except (OSError, AttributeError, ImportError, ValueError) as error: + raise CompletionError("private_ownership_unavailable") from error + + +def _source_owned(info: os.stat_result, fd: int) -> None: + if not stat.S_ISREG(info.st_mode): + raise CompletionError("source_not_regular") + if os.name == "nt": + try: + import msvcrt + except ImportError as error: + raise CompletionError("source_ownership_unavailable") from error + if not _windows_security(msvcrt.get_osfhandle(fd)): + raise CompletionError("source_ownership_unavailable") + elif hasattr(os, "getuid"): + if info.st_uid != os.getuid(): + raise CompletionError("source_not_owned") + else: + raise CompletionError("source_ownership_unavailable") + + +def _capture_inventory(directory: Path) -> tuple[int, int]: + count = total = 0 + with os.scandir(directory) as entries: + for entry in entries: + count += 1 + if count > MAX_CAPTURES: + raise CompletionError("capture_count_limit") + fd = _open_private(Path(entry.path), os.O_RDONLY) + try: + total += os.fstat(fd).st_size + finally: + os.close(fd) + return count, total + + +@dataclass(frozen=True) +class Capture: + directory: Path + key: str + path: Path + metadata: dict + + +def _capture_from(directory: Path, key: str, entry: dict) -> Capture: + if type(key) is not str or _KEY.fullmatch(key) is None or type(entry) is not dict: + raise CompletionError("invalid_capture") + metadata = entry.get("metadata") + if type(metadata) is not dict or _digest(metadata) != key: + raise CompletionError("invalid_capture") + if (type(metadata.get("version")) is not int or metadata["version"] != 1 + or metadata.get("host") not in ("claude", "codex", "gemini", "cursor") + or type(metadata.get("byte_count")) is not int or not 0 <= metadata["byte_count"] <= MAX_CAPTURE_BYTES + or type(metadata.get("sha256")) is not str or _KEY.fullmatch(metadata["sha256"]) is None + or type(metadata.get("file_identity")) is not list or len(metadata["file_identity"]) != 5 + or any(type(value) is not int for value in metadata["file_identity"]) + or type(entry.get("attempts")) is not int or not 0 <= entry["attempts"] <= MAX_ATTEMPTS + or entry.get("status") not in ("queued", "pending", "running", "retry", "exhausted", "complete") + or type(entry.get("outcomes")) is not list or len(entry["outcomes"]) > MAX_ATTEMPTS): + raise CompletionError("invalid_capture") + _text(metadata.get("session_id"), 256) + _text(metadata.get("source")) + _text(metadata.get("database")) + if type(metadata.get("user_id")) is not str or "\0" in metadata["user_id"]: + raise CompletionError("invalid_capture") + suffix = ".jsonl" if metadata.get("source_suffix") == ".jsonl" else ".json" + return Capture(directory, key, directory / "captures" / (key + suffix), metadata) + + +def admit_capture(source: str, session_id: str, user_id: str = "", db_path: str | None = None, + host: str = "claude", *, root: Path | None = None) -> Capture: + from truememory.ingest.hooks._shared import is_allowed_transcript, normalize_transcript_host + host = normalize_transcript_host(host) + _text(session_id, 256) + if session_id == "unknown" or not host or not is_allowed_transcript(source, host=host): + raise CompletionError("source_not_admitted") + locator = str(Path(_text(source)).expanduser().resolve()) + original = Path(source).expanduser() + if original.is_symlink(): + raise CompletionError("source_link") + try: + fd = os.open(original, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)) + with os.fdopen(fd, "rb") as handle: + before_stat = os.fstat(handle.fileno()) + _source_owned(before_stat, handle.fileno()) + before = _file_identity(before_stat) + if before[2] > MAX_CAPTURE_BYTES: + raise CompletionError("capture_limit") + data = handle.read(MAX_CAPTURE_BYTES + 1) + after_stat = os.fstat(handle.fileno()) + _source_owned(after_stat, handle.fileno()) + after = _file_identity(after_stat) + fd = os.open(original, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)) + with os.fdopen(fd, "rb") as handle: + path_stat = os.fstat(handle.fileno()) + _source_owned(path_stat, handle.fileno()) + path_after = _file_identity(path_stat) + except CompletionError: + raise + except OSError as error: + raise CompletionError("source_capture_failed") from error + if len(data) > MAX_CAPTURE_BYTES: + raise CompletionError("capture_limit") + if before != after or after != path_after or len(data) != after[2]: + raise CompletionError("source_changed") + metadata = {"version": 1, "user_id": user_id, "database": normalized_database(db_path), + "host": host, "session_id": session_id, "source": locator, + "source_suffix": original.suffix.lower(), "file_identity": before, + "byte_count": len(data), "sha256": hashlib.sha256(data).hexdigest()} + key = _digest(metadata) + directory = account_root(user_id, db_path, root) + _account_directory(directory) + for child in ("captures", "receipts", "logs"): + _private_directory(directory / child) + with _lock(directory / "state.lock"): + state = _state(directory) + if key in state["captures"]: + capture = _capture_from(directory, key, state["captures"][key]) + verify_capture(capture) + return capture + retained_count, retained_bytes = _capture_inventory(directory / "captures") + suffix = ".jsonl" if metadata["source_suffix"] == ".jsonl" else ".json" + already_retained = (directory / "captures" / (key + suffix)).exists() + if retained_count + int(not already_retained) > MAX_CAPTURES or len(state["captures"]) >= MAX_CAPTURES: + raise CompletionError("capture_count_limit") + if retained_bytes + (0 if already_retained else len(data)) > MAX_ACCOUNT_BYTES: + raise CompletionError("capture_account_quota") + entry = {"metadata": metadata, "attempts": 0, "status": "queued", + "next_attempt_at": 0, "worker_pid": 0, "outcomes": []} + state["captures"][key] = entry + if len(_json(state)) > MAX_STATE_BYTES: + raise CompletionError("retained_state_limit") + capture = _capture_from(directory, key, entry) + _immutable(capture.path, data) + _save_state(directory, state) + return capture + + +def load_capture(key: str, user_id: str, db_path: str | None, host: str, + *, root: Path | None = None) -> Capture: + directory = account_root(user_id, db_path, root) + _account_directory(directory) + with _lock(directory / "state.lock"): + state = _state(directory) + capture = _capture_from(directory, key, state["captures"].get(key)) + if (capture.metadata["user_id"] != user_id or capture.metadata["database"] != normalized_database(db_path) + or capture.metadata["host"] != host): + raise CompletionError("capture_scope_mismatch") + verify_capture(capture) + return capture + + +def verify_capture(capture: Capture) -> None: + data = _read_private(capture.path, MAX_CAPTURE_BYTES) + if (len(data) != capture.metadata["byte_count"] + or hashlib.sha256(data).hexdigest() != capture.metadata["sha256"]): + raise CompletionError("capture_version_mismatch") + + +def strict_complete(result: object, capture: Capture | None = None) -> bool: + coverage = getattr(result, "coverage", None) + if (type(coverage) is not dict or coverage.get("parser_status") != "complete" + or coverage.get("extractor_status") != "complete" + or type(coverage.get("extractor_calls")) is not int or coverage["extractor_calls"] != 1 + or coverage.get("stable") is not True or coverage.get("provenance_observed") is not True): + return False + if any(type(coverage.get(name)) is not int or coverage[name] != 0 + for name in ("malformed_records", "unrecognized_records")): + return False + if any(type(coverage.get(name)) is not list or coverage[name] + for name in ("parser_errors", "extractor_errors")): + return False + version = coverage.get("source_version") + if (type(version) is not dict or type(version.get("sha256")) is not str + or _KEY.fullmatch(version["sha256"]) is None): + return False + if type(version.get("byte_count")) is not int or version["byte_count"] < 0: + return False + if capture is not None and any(version.get(name) != capture.metadata[name] for name in ("sha256", "byte_count")): + return False + values = [getattr(result, name, None) for name in _COUNTS] + if any(type(value) is not int or value < 0 for value in values) or values[0] != sum(values[1:]): + return False + trace = getattr(result, "trace", None) + if type(trace) is not list or len(trace) != values[0]: + return False + actions = ("stored", "updated", "skipped_gate", "skipped_dedup") + if any(type(item) is not dict or item.get("action") not in actions or "storage_error" in item for item in trace): + return False + return all(sum(item["action"] == action for item in trace) == count + for action, count in zip(actions, values[1:])) + + +def has_receipt(capture: Capture) -> bool: + path = capture.directory / "receipts" / (capture.key + ".json") + if not path.exists(): + return False + try: + value = json.loads(_read_private(path, MAX_RECEIPT_BYTES)) + except (OSError, ValueError, UnicodeDecodeError): + return False + if not (type(value) is dict and type(value.get("version")) is int and value["version"] == 1 + and value.get("capture_key") == capture.key + and value.get("scope") == capture.metadata and value.get("coverage") == "observed_bytes" + and value.get("parser_status") == "complete" and value.get("extractor_status") == "complete"): + return False + proof = value.get("proof") + if (type(proof) is not dict or type(proof.get("counts")) is not dict + or set(proof["counts"]) != set(_COUNTS) or type(proof.get("actions")) is not list): + return False + reconstructed = SimpleNamespace(**proof["counts"], coverage=proof.get("coverage"), + trace=[{"action": action} for action in proof["actions"]]) + return strict_complete(reconstructed, capture) + + +def _outcome(entry: dict, category: str, details: dict | None = None) -> None: + attempt = entry.get("attempts", 0) + if type(attempt) is not int or not 0 <= attempt <= MAX_ATTEMPTS: + raise CompletionError("invalid_attempt_state") + entry["status"] = "exhausted" if attempt >= MAX_ATTEMPTS else "retry" + entry["worker_pid"] = 0 + entry["next_attempt_at"] = time.time() + min(300, 2 ** min(attempt, 9)) + outcome = {"attempt": attempt, "category": category, "observed_at": time.time()} + if details is not None: + if len(_json(details)) > 8192: + raise CompletionError("outcome_limit") + outcome["details"] = details + entry["outcomes"].append(outcome) + + +def record_failure(capture: Capture, category: str, details: dict | None = None) -> None: + with _lock(capture.directory / "state.lock"): + state = _state(capture.directory) + entry = state["captures"][capture.key] + _outcome(entry, category, details) + _save_state(capture.directory, state) + + +def record_receipt(capture: Capture, result: object) -> bool: + if not strict_complete(result, capture): + coverage = getattr(result, "coverage", None) + details = {"coverage": coverage if type(coverage) is dict else None, + "counts": {name: getattr(result, name, None) for name in ( + "facts_extracted", "facts_stored", "facts_updated", "facts_skipped_gate", "facts_skipped_dedup")}, + "actions": [entry.get("action") for entry in getattr(result, "trace", []) if type(entry) is dict]} + record_failure(capture, "coverage_unproven", details) + return False + verify_capture(capture) + receipt = {"version": 1, "capture_key": capture.key, "scope": capture.metadata, + "coverage": "observed_bytes", "parser_status": "complete", "extractor_status": "complete", + "proof": {"coverage": result.coverage, + "counts": {name: getattr(result, name) for name in _COUNTS}, + "actions": [entry["action"] for entry in result.trace]}} + with _lock(capture.directory / "state.lock"): + state = _state(capture.directory) + entry = state["captures"][capture.key] + if has_receipt(capture): + return True + if entry["status"] != "running" or entry["worker_pid"] != os.getpid() or not 1 <= entry["attempts"] <= MAX_ATTEMPTS: + raise CompletionError("receipt_without_owned_attempt") + payload = _json(receipt) + if len(payload) > MAX_RECEIPT_BYTES: + raise CompletionError("receipt_limit") + _immutable(capture.directory / "receipts" / (capture.key + ".json"), payload) + state["captures"][capture.key]["status"] = "complete" + state["captures"][capture.key]["worker_pid"] = 0 + _save_state(capture.directory, state) + return True + + +@contextlib.contextmanager +def worker_claim(capture: Capture, attempt: int) -> Iterator[bool]: + # The OS releases ownership on exit/crash. TTL and PID are not lock owners. + with _lock(capture.directory / (capture.key + ".lock"), blocking=False) as allowed: + if not allowed or has_receipt(capture): + yield False + return + with _lock(capture.directory / "state.lock"): + state = _state(capture.directory) + entry = state["captures"][capture.key] + if (type(attempt) is not int or attempt != entry["attempts"] + or not 1 <= attempt <= MAX_ATTEMPTS or entry["status"] != "pending"): + yield False + return + entry["status"] = "running" + entry["worker_pid"] = os.getpid() + _save_state(capture.directory, state) + try: + verify_capture(capture) + yield True + finally: + with _lock(capture.directory / "state.lock"): + state = _state(capture.directory) + entry = state["captures"][capture.key] + if entry["attempts"] == attempt and entry["status"] == "running": + _outcome(entry, "worker_unproven") + _save_state(capture.directory, state) + + +def reserve_inline_attempt(capture: Capture) -> int: + from truememory._platform import pid_is_alive + with _lock(capture.directory / (capture.key + ".lock"), blocking=False) as allowed: + if not allowed or has_receipt(capture): + return 0 + with _lock(capture.directory / "state.lock"): + state = _state(capture.directory) + entry = state["captures"][capture.key] + if entry["attempts"] >= MAX_ATTEMPTS or entry.get("next_attempt_at", 0) > time.time(): + return 0 + pid = entry.get("worker_pid", 0) + if entry["status"] == "pending" and type(pid) is int and pid > 0 and pid_is_alive(pid): + return 0 + entry["attempts"] += 1 + entry["status"] = "pending" + entry["worker_pid"] = os.getpid() + _save_state(capture.directory, state) + return entry["attempts"] + + +def dispatch(capture: Capture, threshold: float = 0.30) -> int: + """One durable attempt before launch; cap/budget denials retain queued work.""" + from truememory.ingest.hooks._shared import check_extraction_budget, refund_extraction_budget + from truememory.hooks.core import spawn_gate, register_spawned_pid + from truememory._platform import pid_is_alive, spawn_kwargs + from truememory.ingest.hooks.stop import MIN_MESSAGES, _has_enough_messages + verify_capture(capture) + if not _has_enough_messages(str(capture.path), MIN_MESSAGES): + return 0 + directory = capture.directory + with _lock(directory / "state.lock"): + state = _state(directory) + entry = state["captures"][capture.key] + if has_receipt(capture) or entry["attempts"] >= MAX_ATTEMPTS: + return 0 + if entry.get("next_attempt_at", 0) > time.time(): + return 0 + with _lock(directory / (capture.key + ".lock"), blocking=False) as free: + if not free: + return 0 + pid = entry.get("worker_pid", 0) + if entry["status"] == "pending" and type(pid) is int and pid > 0 and pid_is_alive(pid): + return 0 + if not check_extraction_budget(): + return 0 + with spawn_gate() as allowed: + if not allowed: + refund_extraction_budget() + return 0 + entry["attempts"] += 1 + entry["status"] = "pending" + entry["worker_pid"] = 0 + _save_state(directory, state) + attempt = entry["attempts"] + meta = capture.metadata + command = [sys.executable, "-m", "truememory.ingest.cli", "ingest", str(capture.path), + "--automatic", "--completion-key", capture.key, "--completion-attempt", str(attempt), + "--user", meta["user_id"], "--db", meta["database"], "--session", meta["session_id"], + "--transcript-host", meta["host"], "--threshold", str(threshold)] + try: + log_path = directory / "logs" / (capture.key + "-" + str(attempt) + ".log") + fd = _open_private(log_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY) + with os.fdopen(fd, "wb") as output: + process = subprocess.Popen(command, stdout=output, stderr=subprocess.STDOUT, + stdin=subprocess.DEVNULL, **spawn_kwargs()) + entry["worker_pid"] = process.pid + _save_state(directory, state) + register_spawned_pid(process.pid) + return process.pid + except OSError as error: + refund_extraction_budget() + _outcome(entry, "spawn_failed_" + type(error).__name__) + _save_state(directory, state) + log.warning("Completion worker launch failed (%s)", type(error).__name__) + return 0 + + +def drain(user_id: str = "", db_path: str | None = None, host: str = "claude") -> None: + directory = account_root(user_id, db_path) + if not directory.exists(): + return + _account_directory(directory) + with _lock(directory / "state.lock"): + state = _state(directory) + captures = [_capture_from(directory, key, entry) for key, entry in state["captures"].items() + if entry.get("metadata", {}).get("host") == host] + for capture in captures: + try: + if dispatch(capture): + return + except OSError as error: + log.warning("Completion drain retained nonterminal work (%s)", type(error).__name__) + + +def convert_legacy_pointers(user_id: str = "", db_path: str | None = None, host: str = "claude") -> None: + """Admit current observed bytes; retain every unversioned legacy pointer.""" + from truememory.ingest.hooks._shared import backlog_transcript_source + from truememory._platform import pid_is_alive + directory = Path(os.environ.get("TRUEMEMORY_BACKLOG_DIR", str(Path.home() / ".truememory" / "backlog"))) + if not directory.exists() or directory.is_symlink(): + return + with os.scandir(directory) as entries: + for entry in islice(entries, MAX_CAPTURES): + pointer = Path(entry.path) + if pointer.suffix not in (".json", ".processing"): + continue + try: + fd = _open_private(pointer, os.O_RDONLY) + with os.fdopen(fd, "rb") as handle: + before_stat = os.fstat(handle.fileno()) + _source_owned(before_stat, handle.fileno()) + before = _file_identity(before_stat) + raw = handle.read(16385) + after_stat = os.fstat(handle.fileno()) + _source_owned(after_stat, handle.fileno()) + after = _file_identity(after_stat) + fd = _open_private(pointer, os.O_RDONLY) + with os.fdopen(fd, "rb") as handle: + path_stat = os.fstat(handle.fileno()) + _source_owned(path_stat, handle.fileno()) + path_after = _file_identity(path_stat) + if before != after or after != path_after or len(raw) > 16384 or len(raw) != after[2]: + continue + value = json.loads(raw) + if (type(value) is not dict or type(value.get("user_id")) is not str + or value["user_id"] != user_id or type(value.get("db_path")) is not str + or normalized_database(value["db_path"]) != normalized_database(db_path) + or value.get("transcript_host") != host): + continue + source = backlog_transcript_source(value, host=host) + if source is None: + continue + transcript, session = source + if value.get("session_id") != session: + continue + pid = value.get("claimed_pid", 0) + if type(pid) is not int or pid < 0: + continue + if pointer.suffix == ".processing" and (not pid or pid_is_alive(pid)): + continue + capture = admit_capture(transcript, session, user_id, db_path, host) + if dispatch(capture): + return + except (OSError, ValueError, TypeError) as error: + log.warning("Legacy pointer retained without completion (%s)", type(error).__name__) diff --git a/truememory/ingest/dedup.py b/truememory/ingest/dedup.py index 5404cfcc..c4c50ee7 100644 --- a/truememory/ingest/dedup.py +++ b/truememory/ingest/dedup.py @@ -152,6 +152,8 @@ def check_duplicate( try: results = memory.search_vectors(fact, limit=3) or [] except Exception as e: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(e) log.warning("Dedup search failed: %s", e) return DedupDecision(action=DedupAction.ADD, fact=fact, reason="search failed, defaulting to add") diff --git a/truememory/ingest/encoding_gate.py b/truememory/ingest/encoding_gate.py index 213ccf41..14e17a30 100644 --- a/truememory/ingest/encoding_gate.py +++ b/truememory/ingest/encoding_gate.py @@ -232,6 +232,7 @@ def __init__( self._norm = total if total > 0 else 1.0 self._last_search_results: list[dict] = [] self._embed_model = None + self._serving_key = None self._pe_available: bool = True # assume available until first failure self._pe_degradation_count: int = 0 # track PE failures for status (#585) self._batch_scores: list[float] = [] @@ -239,6 +240,16 @@ def __init__( self._batch_saliences: list[float] = [] self._batch_pes: list[float] = [] + def _refresh_serving_generation(self) -> None: + import sys + runtime = sys.modules.get("truememory.tier_switch.runtime") + operation = runtime.current_runtime_operation() if runtime is not None else None + if operation is not None and getattr(self, "_serving_key", None) != operation.key: + self._embed_model = None + self._last_search_results = [] + self._pe_available = True + self._serving_key = operation.key + def evaluate(self, fact: str, category: str = "") -> EncodingDecision: """ Pass a candidate fact through the encoding gate. @@ -252,6 +263,7 @@ def evaluate(self, fact: str, category: str = "") -> EncodingDecision: """ # Contradiction bypass: corrections are critical for memory accuracy # and must never be dropped by the gate (#585). + EncodingGate._refresh_serving_generation(self) is_contradiction = _is_contradiction(fact, category) novelty = self._compute_novelty(fact) @@ -365,7 +377,9 @@ def _compute_novelty(self, fact: str) -> float: if hasattr(self.memory, "search_vectors"): try: results = self.memory.search_vectors(fact, limit=10) - except Exception: + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) pass if results is None: # search_vectors() fell back to the full pipeline (no vectors). @@ -419,6 +433,8 @@ def _compute_novelty(self, fact: str) -> float: return max(0.05, novelty) except Exception as e: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(e) log.warning("Compression novelty failed: %s — returning neutral 0.5", e) return 0.5 @@ -485,6 +501,7 @@ def _compute_prediction_error(self, fact: str) -> float: Independent of novelty (r=0.30) and salience (r=0.23). """ + EncodingGate._refresh_serving_generation(self) if not fact or fact.lower().strip().rstrip("!?.… ") in _PE_NOISE: return 0.0 if len(fact.strip()) < 3: @@ -493,11 +510,18 @@ def _compute_prediction_error(self, fact: str) -> float: if not self._last_search_results: return 0.0 + nearest = self._last_search_results[0] + mem_content = nearest.get("content", "") + if not mem_content: + return 0.0 + if self._embed_model is None: try: from truememory.vector_search import get_model self._embed_model = get_model() except Exception as e: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(e) # PE model failed to load — degrade open (#585) if self._pe_available: log.warning( @@ -509,15 +533,12 @@ def _compute_prediction_error(self, fact: str) -> float: return 0.0 model = self._embed_model - nearest = self._last_search_results[0] - mem_content = nearest.get("content", "") - if not mem_content: - return 0.0 - try: - embeddings = model.encode([fact, mem_content, - fact + " [SEP] " + mem_content, - mem_content + " [SEP] " + mem_content]) + from truememory.mps_utils import encode_with_model_ownership + embeddings = encode_with_model_ownership( + model, [fact, mem_content, fact + " [SEP] " + mem_content, + mem_content + " [SEP] " + mem_content], + ) emb_fact = embeddings[0] emb_mem = embeddings[1] @@ -542,6 +563,8 @@ def _compute_prediction_error(self, fact: str) -> float: return pe except Exception as e: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(e) log.debug("PE embedding scorer failed: %s", e) # Runtime encode failure — mark PE as degraded (#585) if self._pe_available: @@ -551,6 +574,10 @@ def _compute_prediction_error(self, fact: str) -> float: self._pe_available = False self._pe_degradation_count += 1 return 0.0 + finally: + # The process singleton remains the cache. A gate must not pin an + # old native model between facts while activation replaces it. + self._embed_model = None # ------------------------------------------------------------------ # Helpers @@ -575,6 +602,8 @@ def _search(self, query: str, limit: int = 5, skip_reranker: bool = False) -> li pass # backing search() lacks the kwarg — fall through return self.memory.search(query, **kwargs) except Exception as e: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(e) log.warning("Memory search failed during encoding gate: %s", e) return [] diff --git a/truememory/ingest/extractor.py b/truememory/ingest/extractor.py index 93e0800b..44030230 100644 --- a/truememory/ingest/extractor.py +++ b/truememory/ingest/extractor.py @@ -24,7 +24,7 @@ import json import logging import re -from dataclasses import dataclass +from dataclasses import dataclass, field from truememory.ingest.models import LLMConfig, LLMError, complete @@ -118,6 +118,55 @@ class ExtractedFact: category: str = "general" confidence: str = "medium" source_role: str = "user" + @property + def source_spans(self) -> tuple[tuple[int, int], ...]: + return getattr(self, "_source_spans", ()) + + @source_spans.setter + def source_spans(self, value: tuple[tuple[int, int], ...]) -> None: + self._source_spans = value + + +@dataclass(frozen=True) +class ExtractionResponseOutcome: + """Response coverage; facts retain the compatibility parser's selection.""" + facts: list[ExtractedFact] = field(repr=False) + status: str + invalid_items: int = 0 + omitted_items: int = 0 + omitted_facts: int = 0 + salvaged: bool = False + error_categories: tuple[str, ...] = () + + +@dataclass(frozen=True) +class ExtractionOutcome: + """Coverage of one extraction attempt, without retry or storage policy. + + Chunk counters partition attempted work into successful, partial and + failed responses. Deferred chunks were not sent to a provider. Fact-cap + omissions count valid response occurrences, not globally unique facts. + Facts and provider errors are excluded from diagnostic representations. + """ + facts: list[ExtractedFact] = field(repr=False) + status: str + total_chunks: int = 0 + attempted_chunks: int = 0 + successful_chunks: int = 0 + partial_chunks: int = 0 + failed_chunks: int = 0 + deferred_chunks: int = 0 + invalid_responses: int = 0 + salvaged_responses: int = 0 + invalid_items: int = 0 + omitted_items: int = 0 + chunk_fact_omissions: int = 0 + merged_fact_omissions: int = 0 + error_categories: tuple[str, ...] = () + + @property + def complete(self) -> bool: + return self.status == "complete" # Chunking configuration for long transcripts. @@ -181,13 +230,17 @@ def _dedupe_facts_by_content(facts: list[ExtractedFact]) -> list[ExtractedFact]: cheap at the extractor layer and saves a Claude CLI subprocess per duplicate downstream. """ - seen: set[str] = set() + seen: dict[str, ExtractedFact] = {} out: list[ExtractedFact] = [] for fact in facts: key = fact.content.strip().lower() - if not key or key in seen: + if not key: + continue + if key in seen: + first = seen[key] + first.source_spans = tuple(dict.fromkeys((*first.source_spans, *fact.source_spans))) continue - seen.add(key) + seen[key] = fact out.append(fact) return out @@ -227,10 +280,26 @@ def extract_facts( Returns: List of ExtractedFact objects, deduplicated by content. """ + return extract_facts_outcome(transcript, config, max_facts, max_chunks).facts + + +def extract_facts_outcome( + transcript: str, + config: LLMConfig, + max_facts: int = 50, + max_chunks: int = _DEFAULT_MAX_CHUNKS, +) -> ExtractionOutcome: + """Extract once with explicit empty, failed, partial and limited coverage. + + Uses the existing chunk selection, provider calls, parsing and merge + order. No automatic retries, fallback provider or budget changes occur. + Callers must not interpret returned partial facts as completed coverage. + """ if not transcript.strip(): - return [] + return ExtractionOutcome([], "complete") chunks = _chunk_transcript(transcript, budget=_CHUNK_CHAR_BUDGET) + total_chunks = len(chunks) if len(chunks) > max_chunks: total_chars = len(transcript) @@ -251,26 +320,54 @@ def extract_facts( ) all_facts: list[ExtractedFact] = [] + successful = partial = failed = invalid_responses = salvaged = 0 + invalid_items = omitted_items = chunk_omissions = 0 + errors: list[str] = [] + deferred = total_chunks - len(chunks) + if deferred: + errors.append("chunk_limit") + source_offset = 0 for i, chunk in enumerate(chunks): + source_span = (source_offset, source_offset + len(chunk)) + source_offset += len(chunk) + 2 prompt = EXTRACTION_PROMPT.format(transcript=_neutralize_delimiters(chunk)) try: response = complete(config, prompt, system=EXTRACTION_SYSTEM) - except LLMError as e: + except LLMError: log.error( - "LLM extraction failed for chunk %d/%d (%s): %s", - i + 1, len(chunks), config.provider, e, + "LLM extraction failed for chunk %d/%d (provider_error)", + i + 1, len(chunks), ) # Keep whatever we've gathered from earlier chunks; don't let a # single mid-transcript failure wipe out the whole extraction. + failed += 1 + errors.append("provider_error") continue - except Exception as e: - log.exception( - "Unexpected error during LLM extraction of chunk %d/%d: %s", - i + 1, len(chunks), e, + except Exception: + log.error( + "Unexpected error during LLM extraction of chunk %d/%d (provider_exception)", + i + 1, len(chunks), ) + failed += 1 + errors.append("provider_exception") continue - chunk_facts = _parse_extraction_response(response, max_facts) + outcome = _parse_extraction_response_outcome(response, max_facts) + chunk_facts = outcome.facts + for fact in chunk_facts: + fact.source_spans = (source_span,) + if outcome.status == "failed": + failed += 1 + elif outcome.status == "partial": + partial += 1 + else: + successful += 1 + invalid_responses += int(outcome.status in ("failed", "partial")) + salvaged += int(outcome.salvaged) + invalid_items += outcome.invalid_items + omitted_items += outcome.omitted_items + chunk_omissions += outcome.omitted_facts + errors.extend(outcome.error_categories) if len(chunks) > 1: log.debug("Chunk %d/%d yielded %d facts", i + 1, len(chunks), len(chunk_facts)) all_facts.extend(chunk_facts) @@ -280,10 +377,24 @@ def extract_facts( merged = _dedupe_facts_by_content(all_facts) # Respect the caller's max_facts cap on the merged result. + merged_omissions = 0 if len(merged) > max_facts: + previous_count = len(merged) merged = merged[:max_facts] - - return merged + merged_omissions = previous_count - len(merged) + if merged_omissions: + errors.append("fact_limit") + + if failed or partial: + status = "partial" if successful or partial else "failed" + elif deferred or omitted_items or merged_omissions: + status = "limited" + else: + status = "complete" + return ExtractionOutcome(merged, status, total_chunks, len(chunks), successful, partial, + failed, deferred, invalid_responses, salvaged, invalid_items, + omitted_items, chunk_omissions, merged_omissions, + tuple(dict.fromkeys(errors))) def _parse_extraction_response(response: str, max_facts: int) -> list[ExtractedFact]: @@ -297,6 +408,30 @@ def _parse_extraction_response(response: str, max_facts: int) -> list[ExtractedF - Object wrapper: `{"facts": [...]}` (unwrapped automatically) - Malformed responses (falls back to brace-matching salvage) """ + return _parse_extraction_response_outcome(response, max_facts).facts + + +def _response_list_shape(text: str, selected: list) -> bool: + """Recognize the envelope without changing the legacy array-first choice.""" + array_at = text.find("[") + object_at = text.find("{") + if object_at < 0 or 0 <= array_at < object_at: + return True + outer = _find_first_balanced(text, "{", "}") + if outer is None: + return False + try: + data = json.loads(outer) + except json.JSONDecodeError: + return False + return any(isinstance(data.get(key), list) and data[key] == selected + for key in ("facts", "items", "results", "data", "extracted")) + + +def _parse_extraction_response_outcome(response: str, max_facts: int) -> ExtractionResponseOutcome: + """Keep legacy fact selection, but never label salvage as complete.""" + if not isinstance(response, str): + return ExtractionResponseOutcome([], "failed", error_categories=("response_type",)) json_str = response.strip() # Strip leading markdown code fence if present @@ -313,14 +448,17 @@ def _parse_extraction_response(response: str, max_facts: int) -> list[ExtractedF extracted = _find_first_balanced(json_str, "{", "}") if extracted is None: log.warning("No JSON array or object found in extraction response") - return [] + return ExtractionResponseOutcome([], "failed", error_categories=("response_missing_json",)) + envelope = json_str json_str = extracted try: facts_data = json.loads(json_str) - except json.JSONDecodeError as e: - log.warning("Failed to parse extraction JSON: %s", e) - return _salvage_partial_json(json_str) + except json.JSONDecodeError: + log.warning("Failed to parse extraction JSON (response_invalid_json)") + facts = _salvage_partial_json(json_str) + return ExtractionResponseOutcome(facts, "partial" if facts else "failed", + salvaged=True, error_categories=("response_invalid_json",)) # Unwrap common object wrappers: {"facts": [...]}, {"items": [...]} if isinstance(facts_data, dict): @@ -331,21 +469,29 @@ def _parse_extraction_response(response: str, max_facts: int) -> list[ExtractedF else: # No list found inside the object log.warning("Extraction response is an object without a known list key") - return [] + return ExtractionResponseOutcome([], "failed", error_categories=("response_shape",)) if not isinstance(facts_data, list): log.warning("Extraction response is not a list after unwrap") - return [] + return ExtractionResponseOutcome([], "failed", error_categories=("response_shape",)) facts = [] - for item in facts_data[:max_facts]: + invalid_items = omitted_facts = 0 + kept_count = len(facts_data[:max_facts]) + omitted_items = len(facts_data) - kept_count + for index, item in enumerate(facts_data): if not isinstance(item, dict): + invalid_items += 1 continue content = item.get("content", "") if not isinstance(content, str): content = str(content) content = content.strip() if not content: + invalid_items += 1 + continue + if index >= kept_count: + omitted_facts += 1 continue facts.append(ExtractedFact( content=content, @@ -355,7 +501,23 @@ def _parse_extraction_response(response: str, max_facts: int) -> list[ExtractedF )) log.info("Extracted %d facts from transcript", len(facts)) - return facts + errors: list[str] = [] + if not _response_list_shape(envelope, facts_data): + errors.append("response_shape") + if invalid_items: + errors.append("response_invalid_items") + if omitted_items: + errors.append("fact_limit") + if "response_shape" in errors: + status = "failed" + elif invalid_items: + status = "partial" if facts or omitted_facts else "failed" + elif omitted_items: + status = "limited" + else: + status = "complete" + return ExtractionResponseOutcome(facts, status, invalid_items, omitted_items, omitted_facts, + error_categories=tuple(errors)) def _find_first_balanced(text: str, open_ch: str, close_ch: str) -> str | None: diff --git a/truememory/ingest/hooks/_shared.py b/truememory/ingest/hooks/_shared.py index dc434931..abf4420b 100644 --- a/truememory/ingest/hooks/_shared.py +++ b/truememory/ingest/hooks/_shared.py @@ -31,41 +31,119 @@ # compact hook only; it is hoisted here so stop.py and the SessionStart # backlog drain share the same check. -def _transcript_roots() -> list[Path]: - """Directories a ``transcript_path`` is allowed to live under. - - Defaults to Claude Code's ``~/.claude/projects``. An explicit - ``TRUEMEMORY_TRANSCRIPT_DIR`` override (tests / non-default installs) is - honored when set. +def normalize_transcript_host(host: object) -> str: + """Validate the installed command identity; never infer it from stdin.""" + if isinstance(host, str) and host in ("claude", "codex", "gemini", "cursor"): + return host + return "" + + +def hook_session_id(payload: object, host: str = "claude", *, default: str = "unknown") -> str: + """Read native identity using the trusted host, retaining caller fallback.""" + host = normalize_transcript_host(host) + if not host or not isinstance(payload, dict): + return default + keys = ("conversation_id", "session_id") if host == "cursor" else ("session_id",) + for key in keys: + value = payload.get(key) + if isinstance(value, str) and value.strip() and "\0" not in value: + return value.strip() + return default + + +def transcript_input_path(value: object) -> str: + """Reject malformed source paths before any filesystem probe.""" + if not isinstance(value, str) or not value.strip() or "\0" in value: + return "" + return value + + +def _transcript_roots(host: str = "claude", transcript_path: str = "") -> list[Path]: + """Native transcript roots scoped to the trusted installed host. + + Gemini's tmp directory also contains non-transcript state. Derive only + the candidate's single-workspace ``chats`` subtree after canonical + containment, without enumerating workspace directories. """ - roots = [Path.home() / ".claude" / "projects"] + host = normalize_transcript_host(host) + if not host: + return [] + roots: list[Path] = [] + if host == "claude": + roots.append(Path.home() / ".claude" / "projects") + elif host == "codex": + codex_home = os.environ.get("CODEX_HOME", "") + roots.append((Path(codex_home) if codex_home else Path.home() / ".codex") / "sessions") + elif host == "gemini" and transcript_input_path(transcript_path): + gemini_home = os.environ.get("GEMINI_CLI_HOME", "") + home = Path(gemini_home) if gemini_home else Path.home() + runtime = home / ".gemini" + if os.environ.get("SANDBOX") == "sandbox-exec": + runtime = home / ".cache" / ".gemini" + try: + temp_root = (runtime / "tmp").resolve() + relative = Path(transcript_path).resolve().relative_to(temp_root) + if len(relative.parts) >= 3 and relative.parts[1] == "chats": + roots.append(temp_root / relative.parts[0] / "chats") + except (OSError, ValueError, RuntimeError): + pass override = os.environ.get("TRUEMEMORY_TRANSCRIPT_DIR", "") if override: - roots.insert(0, Path(override)) + try: + override_root = Path(override).resolve() + # A directory override cannot turn the allowlist into all of home. + if not Path.home().resolve().is_relative_to(override_root): + roots.insert(0, override_root) + except (OSError, ValueError, RuntimeError): + pass return roots -def is_allowed_transcript(transcript_path: str) -> bool: +def is_allowed_transcript(transcript_path: str, host: str = "claude") -> bool: """Return True only if *transcript_path* resolves inside a transcripts root. Resolves symlinks (``.resolve()``) and requires containment, so neither a ``..`` escape nor a symlink whose target is outside the root passes. """ - if not transcript_path: + transcript_path = transcript_input_path(transcript_path) + if not transcript_path or not normalize_transcript_host(host): return False try: - resolved = Path(transcript_path).resolve() - except (OSError, ValueError): + source = Path(transcript_path) + if ".." in source.parts: + return False + resolved = source.resolve() + # Non-strict resolution can leave cyclic/dangling links unresolved + # on newer Python versions. Such paths have no canonical admission. + if resolved.is_symlink() or any(parent.is_symlink() for parent in resolved.parents): + return False + except (OSError, ValueError, RuntimeError): return False - for root in _transcript_roots(): + for root in _transcript_roots(host, str(resolved)): try: - if resolved.is_relative_to(root.resolve()): + if resolved != root.resolve() and resolved.is_relative_to(root.resolve()): return True - except (OSError, ValueError): + except (OSError, ValueError, RuntimeError): continue return False +def backlog_transcript_source(data: object, host: str = "claude") -> tuple[str, str] | None: + """Validate marker shape/source without letting markers select another host.""" + active_host = normalize_transcript_host(host) + if not active_host or not isinstance(data, dict): + return None + marker_host = normalize_transcript_host(data.get("transcript_host", "claude")) + transcript = transcript_input_path(data.get("transcript_path", "")) + if marker_host != active_host or not is_allowed_transcript(transcript, host=active_host): + return None + for key in ("session_id", "user_id", "db_path"): + value = data.get(key, "") + if not isinstance(value, str) or "\0" in value: + return None + return transcript, _safe_session_id(data.get("session_id", "")) + + EXTRACTED_DIR = Path.home() / ".truememory" / "extracted" BACKLOG_DIR = Path.home() / ".truememory" / "backlog" RECALL_MARKER_DIR = Path.home() / ".truememory" / "recall_markers" @@ -118,6 +196,8 @@ def get_recall_deadline() -> float | None: def _safe_session_id(session_id: str) -> str: """Sanitize session_id to prevent path traversal.""" + if not isinstance(session_id, str): + return "" return "".join(c for c in session_id if c.isalnum() or c in "-_")[:64] @@ -344,6 +424,8 @@ def record_stale_processing_pid(processing_path: Path, pid: int) -> None: """Write the spawned PID into a .processing file for liveness checks.""" try: data = json.loads(processing_path.read_text(encoding="utf-8")) + if not isinstance(data, dict): + return data["claimed_pid"] = pid _atomic_write_text(processing_path, json.dumps(data)) except (OSError, json.JSONDecodeError): @@ -418,7 +500,13 @@ def cleanup_stale_processing(backlog_dir: Path) -> None: continue try: data = json.loads(stale.read_text(encoding="utf-8")) + if not isinstance(data, dict): + _quarantine_marker(stale) + continue pid = data.get("claimed_pid", 0) + if not isinstance(pid, int) or isinstance(pid, bool) or pid < 0: + _quarantine_marker(stale) + continue if pid and _pid_is_alive(pid): continue except (json.JSONDecodeError, OSError): diff --git a/truememory/ingest/hooks/compact.py b/truememory/ingest/hooks/compact.py index eb26c915..df274c65 100755 --- a/truememory/ingest/hooks/compact.py +++ b/truememory/ingest/hooks/compact.py @@ -33,16 +33,16 @@ # (hoisted so stop.py and the SessionStart backlog drain share the same guard). # These thin shims keep the original names working for callers/tests. -def _transcript_roots() -> list[Path]: +def _transcript_roots(host: str = "claude", transcript_path: str = "") -> list[Path]: """Shim — see ``_shared._transcript_roots``.""" from truememory.ingest.hooks._shared import _transcript_roots as _shared_roots - return _shared_roots() + return _shared_roots(host, transcript_path) -def _is_allowed_transcript(transcript_path: str) -> bool: +def _is_allowed_transcript(transcript_path: str, host: str = "claude") -> bool: """Shim — delegates to the shared M-90 guard (``_shared.is_allowed_transcript``).""" from truememory.ingest.hooks._shared import is_allowed_transcript - return is_allowed_transcript(transcript_path) + return is_allowed_transcript(transcript_path, host=host) def _parse_args() -> argparse.Namespace: @@ -54,11 +54,13 @@ def _parse_args() -> argparse.Namespace: p = argparse.ArgumentParser(add_help=False) p.add_argument("--user", default=os.environ.get("TRUEMEMORY_USER_ID", "")) p.add_argument("--db", default=os.environ.get("TRUEMEMORY_DB_PATH", "")) + p.add_argument("--host", choices=("claude", "codex", "gemini", "cursor"), default="claude") + p.add_argument("--event", default="PreCompact") args, _ = p.parse_known_args() return args -def main(): +def main() -> None: if os.environ.get("TRUEMEMORY_EXTRACTION"): return @@ -69,11 +71,11 @@ def main(): except (json.JSONDecodeError, EOFError): input_data = {} - # M-95: stdin fields may be non-strings (e.g. an int session_id) from a - # misbehaving / hostile caller. Coerce to str before any str/Path op so the - # hook never crashes with TypeError. - transcript_path = str(input_data.get("transcript_path", "") or "") - session_id = str(input_data.get("session_id", "unknown") or "unknown") + if not isinstance(input_data, dict): + return + from truememory.ingest.hooks._shared import transcript_input_path, hook_session_id + transcript_path = transcript_input_path(input_data.get("transcript_path", "")) + session_id = hook_session_id(input_data, host=args.host) # Sanitize session_id to prevent injection (consistent with user_prompt_submit.py) safe_id = "".join(c for c in session_id if c.isalnum() or c in "-_")[:64] @@ -81,13 +83,15 @@ def main(): safe_id = "unknown" session_id = safe_id - if not transcript_path or not Path(transcript_path).exists(): + if not transcript_path: return # M-90: reject transcript paths outside the expected transcripts root so an # attacker-chosen arbitrary file is never read into the memory store. - if not _is_allowed_transcript(transcript_path): - log.warning("Rejecting transcript_path outside transcripts root: %r", transcript_path) + if not _is_allowed_transcript(transcript_path, host=args.host): + log.warning("Compact transcript source rejected for host=%s", args.host) + return + if not Path(transcript_path).is_file(): return try: @@ -108,6 +112,7 @@ def main(): spawned_pid = _run_background_ingestion( transcript_path, session_id, user_id=args.user, db_path=args.db, + transcript_host=args.host, ) # Only mark extracted on a real spawn; pid==0 means the session # was queued to the backlog (spawn cap / Popen failure) and must diff --git a/truememory/ingest/hooks/hermes.py b/truememory/ingest/hooks/hermes.py new file mode 100644 index 00000000..80ac53d2 --- /dev/null +++ b/truememory/ingest/hooks/hermes.py @@ -0,0 +1,719 @@ +#!/usr/bin/env python3 +"""Native Hermes shell hooks with private, retained content snapshots. + +The installed argv selects the event and account. Native input supplies +content and correlation identifiers, never a transcript filename. Worker +return, context delivery and complete extraction are separate observations. +""" +from __future__ import annotations + +import argparse +import errno +import hashlib +import json +import logging +import os +import re +import stat +import subprocess +import sys +import time +import uuid +from contextlib import contextmanager +from dataclasses import dataclass +from pathlib import Path +from typing import Iterator, TextIO + +log = logging.getLogger(__name__) +EVENTS = ("on_session_start", "pre_llm_call", "post_llm_call", + "on_session_end", "on_session_finalize") +MAX_INPUT_BYTES = 4 * 1024 * 1024 +MAX_SCOPE_BYTES = 64 * 1024 * 1024 +MAX_SNAPSHOTS = 1024 +MAX_CONTEXT_BYTES = 32 * 1024 +MAX_STATE_BYTES = 8 * 1024 * 1024 +CONTEXT_WAIT_SECONDS = 2.0 +MAX_ATTEMPTS = 5 +_HEX = re.compile(r"[a-f0-9]{64}\Z") + + +class BridgeError(ValueError): + """A rejected native event or unavailable owned state.""" + + +def _encoded(value: object) -> bytes: + return json.dumps(value, ensure_ascii=False, sort_keys=True, + separators=(",", ":"), allow_nan=False).encode("utf-8") + + +def _digest(value: object) -> str: + return hashlib.sha256(_encoded(value)).hexdigest() + + +def _identity(value: object, name: str) -> str: + if (not isinstance(value, str) or not value.strip() + or value in {"unknown", "undefined", "null"} + or len(value.encode("utf-8")) > 1024 + or any(ord(char) < 32 for char in value)): + raise BridgeError(f"invalid {name}") + return value + + +def _text(value: object) -> str: + if isinstance(value, str): + return value + if not isinstance(value, list): + raise BridgeError("unsupported native text") + parts: list[str] = [] + for part in value: + if not isinstance(part, dict): + raise BridgeError("invalid native content part") + kind = part.get("type") + if kind in {"text", "input_text", "output_text"}: + text = part.get("text") + if not isinstance(text, str): + raise BridgeError("invalid native text part") + parts.append(text) + elif kind not in {"image", "image_url", "input_image", "audio", "input_audio"}: + raise BridgeError("unsupported native content part") + return "\n".join(parts) + + +def _messages(extra: dict[str, object], event: str) -> list[dict[str, str]]: + history = extra.get("conversation_history", []) + if not isinstance(history, list): + raise BridgeError("invalid native conversation history") + messages: list[dict[str, str]] = [] + for row in history: + if not isinstance(row, dict) or not isinstance(row.get("role"), str): + raise BridgeError("invalid native history row") + role = row["role"] + if role in {"system", "developer", "tool", "tool_use", "tool_result"}: + continue + if role not in {"user", "human", "assistant"}: + raise BridgeError("unsupported native history role") + content = _text(row.get("content")) + messages.append({"role": "human" if role in {"user", "human"} else role, + "content": content}) + user = _text(extra.get("user_message")) + # The current user is the final user row in the native producer's history. + # Never discover the current prompt by searching historical text. + last_user = next((i for i in range(len(messages) - 1, -1, -1) + if messages[i]["role"] == "human"), None) + if last_user is None: + messages.append({"role": "human", "content": user}) + elif messages[last_user]["content"] != user: + messages.append({"role": "human", "content": user}) + if event == "post_llm_call": + assistant = _text(extra.get("assistant_response")) + if not messages or messages[-1] != {"role": "assistant", "content": assistant}: + messages.append({"role": "assistant", "content": assistant}) + return messages + + +def _check_stat(value: os.stat_result, *, directory: bool = False) -> None: + valid = stat.S_ISDIR(value.st_mode) if directory else stat.S_ISREG(value.st_mode) + if not valid or (not directory and value.st_nlink != 1): + raise BridgeError("owned path is not a private regular file") + if hasattr(os, "getuid") and value.st_uid != os.getuid(): + raise BridgeError("owned path has a foreign owner") + if directory and os.name != "nt" and value.st_mode & 0o077: + raise BridgeError("owned directory is not private") + + +def _directory(path: Path) -> None: + try: + path.mkdir(mode=0o700) + except FileExistsError: + pass + _check_stat(path.lstat(), directory=True) + + +def _open_owned(path: Path, flags: int) -> int: + try: + before = path.lstat() + except FileNotFoundError: + before = None + if before is not None: + _check_stat(before) + fd = os.open(str(path), flags | getattr(os, "O_NOFOLLOW", 0), 0o600) + try: + actual = os.fstat(fd) + _check_stat(actual) + after = path.lstat() + _check_stat(after) + if (actual.st_dev, actual.st_ino) != (after.st_dev, after.st_ino): + raise BridgeError("owned file changed during open") + if before is not None and (before.st_dev, before.st_ino) != (actual.st_dev, actual.st_ino): + raise BridgeError("owned file substituted during open") + return fd + except (OSError, BridgeError): + os.close(fd) + raise + + +def _read_owned(path: Path, limit: int = MAX_STATE_BYTES) -> dict[str, object]: + fd = _open_owned(path, os.O_RDONLY) + with os.fdopen(fd, "rb") as stream: + before = os.fstat(stream.fileno()) + raw = stream.read(limit + 1) + after = os.fstat(stream.fileno()) + final = path.lstat() + _check_stat(final) + def version(value: os.stat_result) -> tuple[int, int, int, int, int]: + return (value.st_dev, value.st_ino, value.st_size, value.st_mtime_ns, value.st_ctime_ns) + if (version(before) != version(after) or (after.st_dev, after.st_ino, after.st_size, + after.st_mtime_ns, after.st_ctime_ns) != + (final.st_dev, final.st_ino, final.st_size, final.st_mtime_ns, final.st_ctime_ns)): + raise BridgeError("owned file changed during read") + if len(raw) > limit: + raise BridgeError("owned state exceeds bound") + data = json.loads(raw) + if not isinstance(data, dict): + raise BridgeError("invalid owned state") + return data + + +def _write_owned(path: Path, value: object, *, immutable: bool = False) -> None: + raw = _encoded(value) + if len(raw) > MAX_STATE_BYTES: + raise BridgeError("owned state exceeds bound") + if immutable: + try: + existing = _read_owned(path) + except FileNotFoundError: + existing = None + if existing is not None: + if _encoded(existing) != raw: + raise BridgeError("immutable capture mismatch") + return + elif path.exists() or path.is_symlink(): + _check_stat(path.lstat()) + temporary = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp") + fd = _open_owned(temporary, os.O_WRONLY | os.O_CREAT | os.O_EXCL) + with os.fdopen(fd, "wb") as stream: + stream.write(raw) + stream.flush() + os.fsync(stream.fileno()) + # The scope lock serializes replacement. Failed temporary files remain. + os.replace(temporary, path) + if os.name != "nt": + directory_fd = os.open(str(path.parent), os.O_RDONLY) + try: + os.fsync(directory_fd) + finally: + os.close(directory_fd) + + +@contextmanager +def _lock(path: Path, wait: float = 0.0) -> Iterator[bool]: + fd = _open_owned(path, os.O_RDWR | os.O_CREAT) + acquired = False + deadline = time.monotonic() + wait + try: + while True: + try: + if sys.platform == "win32": + import msvcrt + os.lseek(fd, 0, os.SEEK_SET) + msvcrt.locking(fd, msvcrt.LK_NBLCK, 1) + else: + import fcntl + fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + acquired = True + break + except OSError as error: + if error.errno not in {errno.EACCES, errno.EAGAIN, errno.EDEADLK}: + raise + if time.monotonic() >= deadline: + break + time.sleep(0.01) + yield acquired + finally: + if acquired: + if sys.platform == "win32": + import msvcrt + os.lseek(fd, 0, os.SEEK_SET) + msvcrt.locking(fd, msvcrt.LK_UNLCK, 1) + else: + import fcntl + fcntl.flock(fd, fcntl.LOCK_UN) + os.close(fd) + + +def _birth(pid: int) -> float | None: + from truememory._platform import process_birth + return process_birth(pid) + + +def _owner() -> dict[str, object]: + return {"pid": os.getpid(), "born": _birth(os.getpid())} + + +def _alive(identity: object) -> bool: + if not isinstance(identity, dict): + return False + pid, born = identity.get("pid"), identity.get("born") + if type(pid) is not int or pid <= 0: + return False + try: + actual = _birth(pid) + except OSError: + return True + # Unknown birth identity is never permission to replace a live owner. + return actual is not None and (born is None or born == actual) + + +@dataclass +class Store: + root: Path + user: str + db: str + session: str + + def __post_init__(self) -> None: + self.db = (self.db if self.db == ":memory:" else + str(Path(self.db or Path.home() / ".truememory/memories.db").expanduser().resolve())) + self.account = _digest([self.user, self.db]) + self.key = _digest([self.account, self.session]) + if self.root.is_symlink(): + raise BridgeError("spool root is a symlink") + self.root.parent.mkdir(parents=True, exist_ok=True) + _directory(self.root) + self.account_dir = self.root / self.account + _directory(self.account_dir) + self.path = self.account_dir / self.key + _directory(self.path) + self.capture_lock = self.path / "capture.lock" + self.worker_lock = self.path / "worker.lock" + self.state_path = self.path / "state.json" + + def state(self) -> dict[str, object]: + self.validate_paths() + try: + data = _read_owned(self.state_path) + except FileNotFoundError: + return {"version": 1, "account": self.account, "session_id": self.session, + "turns": {}, "captures": {}, "worker": {}} + if (data.get("version") != 1 or data.get("account") != self.account + or data.get("session_id") != self.session + or not all(isinstance(data.get(name), dict) for name in ("turns", "captures", "worker"))): + raise BridgeError("owned state identity mismatch") + return data + + def save(self, state: dict[str, object], *, quota_held: bool = False) -> None: + self.validate_paths() + self.write(self.state_path, state, quota_held=quota_held) + + def usage(self) -> tuple[int, int]: + size = snapshots = 0 + for directory, subdirs, files in os.walk(self.account_dir, followlinks=False): + for subdir in subdirs: + _check_stat((Path(directory) / subdir).lstat(), directory=True) + for filename in files: + value = (Path(directory) / filename).lstat() + _check_stat(value) + size += value.st_size + snapshots += bool(re.fullmatch(r"[a-f0-9]{64}\.json", filename)) + return size, snapshots + + def write(self, path: Path, value: object, *, quota_held: bool = False, + immutable: bool = False) -> None: + if not quota_held: + with _lock(self.root / "quota.lock", 0.25) as allowed: + if not allowed: + raise BridgeError("state backpressure: quota lock busy") + self.write(path, value, quota_held=True, immutable=immutable) + return + self.validate_paths() + size, _snapshots = self.usage() + # Reserve the temporary write too. Denial preserves all old bytes. + if size + len(_encoded(value)) > MAX_SCOPE_BYTES: + raise BridgeError("state backpressure: retained account quota") + _write_owned(path, value, immutable=immutable) + + def validate_paths(self) -> None: + for path in (self.root, self.account_dir, self.path): + _check_stat(path.lstat(), directory=True) + + def snapshot(self, name: str) -> dict[str, object]: + self.validate_paths() + if not re.fullmatch(r"[a-f0-9]{64}\.json", name): + raise BridgeError("invalid owned snapshot key") + data = _read_owned(self.path / name) + expected = _digest({key: value for key, value in data.items() if key != "sha256"}) + if (data.get("sha256") != expected or name != f"{expected}.json" + or data.get("account") != self.account or data.get("session_id") != self.session + or data.get("user") != self.user or data.get("db") != self.db): + raise BridgeError("owned snapshot identity mismatch") + return data + + def capture(self, payload: dict[str, object], event: str) -> str: + if event not in {"pre_llm_call", "post_llm_call"}: + raise BridgeError("invalid capture event") + if _identity(payload.get("session_id"), "session_id") != self.session: + raise BridgeError("native session identity mismatch") + extra = payload.get("extra") + if not isinstance(extra, dict): + raise BridgeError("invalid native extra") + turn = _identity(extra.get("turn_id"), "turn_id") + if "task_id" in extra: + _identity(extra["task_id"], "task_id") + messages = _messages(extra, event) + data: dict[str, object] = { + "version": 1, "account": self.account, "session_id": self.session, + "turn_id": turn, "task_id": extra.get("task_id", ""), + "user": self.user, "db": self.db, "event": event, "messages": messages, + "native_user_message": extra.get("user_message"), + "native_assistant_response": extra.get("assistant_response"), + "native_history": extra.get("conversation_history", []), + } + digest = _digest(data) + data["sha256"] = digest + name, turn_key = f"{digest}.json", _digest([self.account, self.session, turn]) + with _lock(self.root / "quota.lock", 0.25) as quota_lock: + if not quota_lock: + raise BridgeError("capture backpressure: quota lock busy") + with _lock(self.capture_lock, 0.25) as capture_lock: + if not capture_lock: + raise BridgeError("capture backpressure: scope busy") + state = self.state() + captures, turns = state["captures"], state["turns"] + if name not in captures: + _size, count = self.usage() + if count >= MAX_SNAPSHOTS: + raise BridgeError("capture backpressure: retained snapshot quota") + self.write(self.path / name, data, immutable=True, quota_held=True) + captures[name] = {"turn": turn_key, "state": "captured", "attempts": 0, + "retry_at": 0.0, "flush": event == "post_llm_call"} + else: + self.snapshot(name) + if turn_key not in turns: + turns[turn_key] = {"ordinal": len(turns) + 1, "snapshot": name, + "context_state": "pending", "context_attempts": 0, + "context_retry_at": 0.0, "context_requested": False} + turn_state = turns[turn_key] + if event == "pre_llm_call": + turn_state["context_requested"] = True + # Recall always uses original current input, never a post history row. + turn_state["snapshot"] = name + self.save(state, quota_held=True) + return turn_key + + def flush(self) -> None: + with _lock(self.capture_lock, 0.25) as allowed: + if not allowed: + raise BridgeError("flush backpressure: scope busy") + state = self.state() + for item in state["captures"].values(): + item["flush"] = True + self.save(state) + + +def _recall(snapshot: dict[str, object], ordinal: int) -> str: + from truememory.ingest.hooks import session_start, user_prompt_submit + user, db = snapshot["user"], snapshot["db"] + prompt = _text(snapshot["native_user_message"]) + if not prompt.strip(): + return "" + intensity, _store_intensity = user_prompt_submit._get_intensity_config() + if ordinal == 1: + return session_start.recall_memories( + {}, user_id=user, db_path=db, budget=session_start._intensity_budget(intensity), + intensity=intensity, + ) + context, searched = user_prompt_submit._try_proactive_recall( + prompt, user, db, snapshot["session_id"], intensity, ordinal, debounced=False, + ) + if not context and not searched: + context = user_prompt_submit._try_auto_recall( + prompt, user, db, snapshot["session_id"], debounced=False, + ) + return context or "" + + +def _run_pipeline(snapshot: dict[str, object]) -> dict[str, object]: + from truememory.ingest import extractor, pipeline + from truememory.ingest.hooks.stop import GATE_THRESHOLD, MIN_MESSAGES + from truememory.ingest.transcript import Message, format_for_extraction + messages = [Message(role=row["role"], content=row["content"]) + for row in snapshot["messages"]] + text = format_for_extraction(messages) + if len(messages) < MIN_MESSAGES or len(text.strip()) < 50: + return {"state": "deferred", "reason": "original_minimum"} + outcomes: list[object] = [] + original, simple = pipeline.extract_facts, pipeline.extract_facts_simple + outcome_api = getattr(extractor, "extract_facts_outcome", None) + coverage_supported = callable(outcome_api) + + def observe(*args: object, **kwargs: object) -> list[object]: + outcome = outcome_api(*args, **kwargs) + outcomes.append(outcome) + return outcome.facts + + heuristic_completed = False + + def observe_simple(*args: object, **kwargs: object) -> list[object]: + nonlocal heuristic_completed + facts = simple(*args, **kwargs) + heuristic_completed = True + return facts + + worker = pipeline.IngestionPipeline(user_id=snapshot["user"], db_path=snapshot["db"], + gate_threshold=GATE_THRESHOLD) + # This process runs one pipeline on one thread. Restore bindings even if + # extraction or storage throws; the underlying selection is unchanged. + if coverage_supported: + pipeline.extract_facts, pipeline.extract_facts_simple = observe, observe_simple + try: + result = worker.ingest_text(text, session_id=f"hermes:{snapshot['session_id']}") + finally: + pipeline.extract_facts, pipeline.extract_facts_simple = original, simple + fields = ("facts_extracted", "facts_stored", "facts_updated", "facts_skipped_gate", + "facts_skipped_dedup", "facts_encoded") + counts = {name: getattr(result, name, None) for name in fields} + valid = all(type(value) is int and value >= 0 for value in counts.values()) + accounted = (valid and counts["facts_extracted"] == sum(counts[name] for name in fields[1:5]) + and counts["facts_encoded"] == sum(counts[name] for name in + ("facts_stored", "facts_updated", "facts_skipped_dedup"))) + extraction_complete = coverage_supported and len(outcomes) == 1 and outcomes[0].complete + coverage = ("coverage_unproven" if not coverage_supported else + outcomes[0].status if len(outcomes) == 1 else + "heuristic_returned" if heuristic_completed else "unobserved") + return {"state": "extraction_complete" if extraction_complete and accounted else "worker_returned", + "extraction_status": coverage, "terminal_accounting_complete": bool(accounted), + "counts": counts} + + +def _retry(item: dict[str, object], prefix: str = "") -> None: + attempts = int(item.get(f"{prefix}attempts", 0)) + item[f"{prefix}retry_at"] = time.time() + min(300, 2 ** min(attempts, 9)) + + +def _due(item: dict[str, object], prefix: str = "") -> bool: + return (int(item.get(f"{prefix}attempts", 0)) < MAX_ATTEMPTS + and float(item.get(f"{prefix}retry_at", 0)) <= time.time()) + + +def _work_available(state: dict[str, object]) -> bool: + return (any(item.get("context_requested") and item.get("context_state") not in {"ready", "written"} + and _due(item, "context_") for item in state["turns"].values()) + or any(item.get("flush") and item.get("state") not in {"extraction_complete", "deferred"} + and _due(item) for item in state["captures"].values())) + + +def _spawn(store: Store) -> None: + from truememory.hooks.core import register_spawned_pid, spawn_gate + from truememory.ingest.hooks._shared import check_extraction_budget, refund_extraction_budget + with _lock(store.capture_lock, 0.25) as scope_lock: + if not scope_lock: + return + state = store.state() + if not _work_available(state): + return + with _lock(store.worker_lock) as worker_free: + if not worker_free: + return + reservation = state["worker"] + if (reservation.get("state") in {"reserved", "running"} + and (_alive(reservation.get("child")) or _alive(reservation.get("launcher")))): + return + with _lock(store.root / "budget.lock", 0.25) as budget_lock: + if not budget_lock or not check_extraction_budget(): + return + spawned = False + try: + with spawn_gate() as allowed: + if not allowed: + refund_extraction_budget() + return + token = uuid.uuid4().hex + state["worker"] = {"state": "reserved", "token": token, + "launcher": _owner(), "child": None} + store.save(state) + args = [sys.executable, str(Path(__file__).resolve()), "--worker", store.key, + "--token", token, "--session", store.session, + "--user", store.user, "--db", store.db] + env = dict(os.environ, TRUEMEMORY_EXTRACTION="1", + TRUEMEMORY_HERMES_SPOOL_DIR=str(store.root)) + kwargs: dict[str, object] = {"stdin": subprocess.DEVNULL, + "stdout": subprocess.DEVNULL, + "stderr": subprocess.DEVNULL, "env": env} + if sys.platform == "win32": + kwargs["creationflags"] = subprocess.CREATE_NEW_PROCESS_GROUP + else: + kwargs["start_new_session"] = True + child = subprocess.Popen(args, **kwargs) + spawned = True + register_spawned_pid(child.pid) + state["worker"]["child"] = {"pid": child.pid, "born": _birth(child.pid)} + store.save(state) + except (OSError, BridgeError): + if not spawned: + refund_extraction_budget() + state["worker"]["state"] = "spawn_failed" + store.save(state) + raise + + +def _worker(store: Store, token: str) -> None: + from truememory.ingest.hooks._shared import check_extraction_budget, refund_extraction_budget + with _lock(store.worker_lock) as allowed: + if not allowed: + return + with _lock(store.capture_lock, 2.0) as scope_lock: + if not scope_lock: + raise BridgeError("worker scope lock busy") + state = store.state() + if state["worker"].get("token") != token: + raise BridgeError("worker reservation mismatch") + state["worker"].update({"state": "running", "child": _owner(), "launcher": None}) + store.save(state) + # Refresh after every attempt so content arriving during extraction + # stays eligible. One launch handles at most 32 bounded attempts. + extraction_attempts = 0 + for _attempt in range(32): + with _lock(store.capture_lock, 2.0) as scope_lock: + if not scope_lock: + raise BridgeError("worker queue claim busy") + current = store.state() + contexts = [(key, item) for key, item in current["turns"].items() + if item.get("context_requested") and item.get("context_state") not in {"ready", "written"} + and _due(item, "context_")] + captures = [(key, item) for key, item in current["captures"].items() + if item.get("flush") and item.get("state") not in {"extraction_complete", "deferred"} + and _due(item)] + if not contexts and not captures: + break + is_context = bool(contexts) + key, target = (contexts or captures)[0] + if not is_context and extraction_attempts: + with _lock(store.root / "budget.lock", 0.25) as budget_lock: + if not budget_lock or not check_extraction_budget(): + break + target = dict(target) + selected = current["turns" if is_context else "captures"][key] + prefix = "context_" if is_context else "" + selected[f"{prefix}attempts"] += 1 + selected[f"{prefix}state"] = "processing" + _retry(selected, prefix) + store.save(current) + try: + if is_context: + context = _recall(store.snapshot(target["snapshot"]), int(target["ordinal"])) + if not isinstance(context, str) or len(context.encode("utf-8")) > MAX_CONTEXT_BYTES: + raise BridgeError("context output exceeds bound") + result = {"account": store.account, "turn": key, "context": context, + "sha256": _digest(context)} + store.write(store.path / f"{key}.context.json", result) + observation = {"context_state": "ready", "context_error": ""} + else: + extraction_attempts += 1 + observation = _run_pipeline(store.snapshot(key)) + except Exception as error: + observation = {f"{prefix}state": "failed", f"{prefix}error_category": type(error).__name__} + with _lock(store.capture_lock, 2.0) as scope_lock: + if not scope_lock: + raise BridgeError("worker observation commit busy") + current = store.state() + selected = current["turns" if is_context else "captures"][key] + selected.update(observation) + if selected[f"{prefix}state"] not in {"ready", "extraction_complete", "deferred"}: + _retry(selected, prefix) + store.save(current) + with _lock(store.capture_lock, 2.0) as scope_lock: + if not scope_lock: + raise BridgeError("worker final observation busy") + current = store.state() + current["worker"]["state"] = "returned" + store.save(current) + if not extraction_attempts: + with _lock(store.root / "budget.lock", 0.25) as budget_lock: + if budget_lock: + refund_extraction_budget() + + +def _emit_context(store: Store, turn_key: str, output: TextIO) -> bool: + deadline = time.monotonic() + CONTEXT_WAIT_SECONDS + while True: + with _lock(store.capture_lock, 0.1) as scope_lock: + if scope_lock: + state = store.state() + turn = state["turns"][turn_key] + if turn["context_state"] in {"ready", "written"}: + result = _read_owned(store.path / f"{turn_key}.context.json", MAX_CONTEXT_BYTES * 6) + context = result.get("context") + if (not isinstance(context, str) or len(context.encode("utf-8")) > MAX_CONTEXT_BYTES + or result.get("account") != store.account or result.get("turn") != turn_key + or result.get("sha256") != _digest(context)): + raise BridgeError("context cache identity mismatch") + rendered = json.dumps({"context": context}, ensure_ascii=False) + written = output.write(rendered + "\n") + if written is not None and written != len(rendered) + 1: + raise BridgeError("incomplete context output write") + output.flush() + turn["context_state"] = "written" + store.save(state) + return bool(context) + if time.monotonic() >= deadline: + return False + time.sleep(0.01) + + +def _input(stream: TextIO) -> dict[str, object]: + source = getattr(stream, "buffer", stream) + raw = source.read(MAX_INPUT_BYTES + 1) + encoded = raw if isinstance(raw, bytes) else raw.encode("utf-8") + if len(encoded) > MAX_INPUT_BYTES: + raise BridgeError("native input exceeds bound") + value = json.loads(encoded, parse_constant=lambda _value: (_ for _ in ()).throw(BridgeError("invalid JSON number"))) + if not isinstance(value, dict): + raise BridgeError("native input is not an object") + return value + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("--event", choices=EVENTS) + parser.add_argument("--user", default=os.environ.get("TRUEMEMORY_USER_ID", "")) + parser.add_argument("--db", default=os.environ.get("TRUEMEMORY_DB_PATH", "")) + parser.add_argument("--worker") + parser.add_argument("--token") + parser.add_argument("--session") + args = parser.parse_args() + root = Path(os.environ.get("TRUEMEMORY_HERMES_SPOOL_DIR", + str(Path.home() / ".truememory/hermes-native-v1"))) + try: + if args.worker is not None: + if (not _HEX.fullmatch(args.worker) or not args.token + or not re.fullmatch(r"[a-f0-9]{32}", args.token)): + raise BridgeError("invalid worker reservation") + store = Store(root, args.user, args.db, _identity(args.session, "session_id")) + if store.key != args.worker: + raise BridgeError("worker account mismatch") + _worker(store, args.token) + return 0 + if os.environ.get("TRUEMEMORY_EXTRACTION"): + return 0 + if args.event is None: + raise BridgeError("installed event required") + payload = _input(sys.stdin) + session = _identity(payload.get("session_id"), "session_id") + store = Store(root, args.user, args.db, session) + if args.event in {"pre_llm_call", "post_llm_call"}: + turn_key = store.capture(payload, args.event) + else: + turn_key = "" + if args.event != "on_session_start": + store.flush() + _spawn(store) + if args.event == "pre_llm_call": + _emit_context(store, turn_key, sys.stdout) + return 0 + except (OSError, ValueError, RuntimeError, ImportError) as error: + log.warning("Hermes native hook rejected or deferred (%s)", type(error).__name__) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/truememory/ingest/hooks/openclaw.py b/truememory/ingest/hooks/openclaw.py new file mode 100644 index 00000000..769f33f8 --- /dev/null +++ b/truememory/ingest/hooks/openclaw.py @@ -0,0 +1,69 @@ +"""Native OpenClaw recall transport without capture or transcript admission.""" +from __future__ import annotations + +import argparse +import json +import os +import re +import sys +from typing import TextIO + +MAX_PROMPT_BYTES = 128 * 1024 +MAX_INPUT_BYTES = MAX_PROMPT_BYTES + 4096 +_SESSION_RE = re.compile(r"openclaw_[0-9a-f]{48}\Z") +_RUN_RE = re.compile(r"[0-9a-f]{64}\Z") + + +def _parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(add_help=False) + parser.add_argument("--event", choices=("before_prompt_build",), required=True) + parser.add_argument("--user", default="") + parser.add_argument("--db", default="") + return parser.parse_args() + + +def _request(stream: TextIO) -> tuple[str, str] | None: + try: + text = stream.read(MAX_INPUT_BYTES + 1) + if len(text.encode("utf-8")) > MAX_INPUT_BYTES: + return None + payload = json.loads(text) + except (OSError, ValueError, TypeError, UnicodeError): + return None + if not isinstance(payload, dict) or set(payload) != {"session_id", "run_id", "prompt"}: + return None + session_id, run_id, prompt = (payload[key] for key in ("session_id", "run_id", "prompt")) + if not isinstance(session_id, str) or not _SESSION_RE.fullmatch(session_id): + return None + if not isinstance(run_id, str) or not _RUN_RE.fullmatch(run_id): + return None + if not isinstance(prompt, str) or len(prompt.strip()) < 3 or "\0" in prompt: + return None + try: + if len(prompt.encode("utf-8")) > MAX_PROMPT_BYTES: + return None + except UnicodeError: + return None + return session_id, prompt + + +def main() -> None: + if os.environ.get("TRUEMEMORY_EXTRACTION"): + return + args = _parse_args() + request = _request(sys.stdin) + if request is None: + return + session_id, prompt = request + from truememory.ingest.hooks.user_prompt_submit import _try_auto_recall + + # Native run correlation supplies no admitted-turn cadence or capture proof. + context = _try_auto_recall(prompt, args.user, args.db, session_id, debounced=False) + if context is not None and not isinstance(context, str): + return + sys.stdout.write(json.dumps({"context": context or ""}) + "\n") + sys.stdout.flush() + + +if __name__ == "__main__": + main() diff --git a/truememory/ingest/hooks/output.py b/truememory/ingest/hooks/output.py new file mode 100644 index 00000000..f924c235 --- /dev/null +++ b/truememory/ingest/hooks/output.py @@ -0,0 +1,58 @@ +"""Event-specific context output for installed recall hooks.""" +from __future__ import annotations + +import json +import logging +import sys +from typing import TextIO + +log = logging.getLogger(__name__) + +HOOK_HOSTS = ("claude", "cursor", "gemini", "codex") +_CONTEXT_EVENTS = { + "claude": frozenset({"SessionStart", "UserPromptSubmit"}), + "cursor": frozenset({"sessionStart"}), + "gemini": frozenset({"SessionStart", "BeforeAgent"}), + "codex": frozenset({"SessionStart", "UserPromptSubmit"}), +} + + +def supports_context(host: str, event: str) -> bool: + """Use trusted installer flags, never host identity supplied on stdin.""" + return event in _CONTEXT_EVENTS.get(host, ()) + + +def emit_context( + context: str, *, host: str, event: str, stream: TextIO | None = None, +) -> bool: + """Return success only after the supported JSON response is flushed. + + This confirms transport emission, not native host/model acceptance. + Cursor beforeSubmitPrompt has no documented context output contract. + """ + if not isinstance(context, str) or not context or not supports_context(host, event): + return False + + output: dict[str, object] + if host == "claude": + output = {"additionalContext": context} + elif host == "cursor": + output = {"additional_context": context} + else: + output = { + "hookSpecificOutput": { + "hookEventName": event, + "additionalContext": context, + }, + } + + target = stream if stream is not None else sys.stdout + try: + response = json.dumps(output) + "\n" + if target.write(response) != len(response): + return False + target.flush() + except (OSError, ValueError, TypeError): + log.warning("Recall context emission failed for host=%s event=%s", host, event) + return False + return True diff --git a/truememory/ingest/hooks/session_start.py b/truememory/ingest/hooks/session_start.py index 96948de7..aa4304d9 100755 --- a/truememory/ingest/hooks/session_start.py +++ b/truememory/ingest/hooks/session_start.py @@ -30,6 +30,7 @@ from truememory import _platform from truememory._platform import _env_int from truememory._sanitize import sanitize_injection_content +from truememory.ingest.hooks.output import HOOK_HOSTS, emit_context try: import fcntl @@ -38,6 +39,7 @@ _HAS_FCNTL = False log = logging.getLogger(__name__) +_COMPLETION_ACCOUNT: tuple[str, str] | None = None _BASE_MEMORY_LIMIT = _env_int("TRUEMEMORY_RECALL_LIMIT", 25, lo=1) @@ -170,6 +172,8 @@ def _parse_args() -> argparse.Namespace: p = argparse.ArgumentParser(add_help=False) p.add_argument("--user", default=os.environ.get("TRUEMEMORY_USER_ID", "")) p.add_argument("--db", default=os.environ.get("TRUEMEMORY_DB_PATH", "")) + p.add_argument("--host", choices=HOOK_HOSTS, default="claude") + p.add_argument("--event", default="SessionStart") p.add_argument("--scan-stale", action="store_true", help=argparse.SUPPRESS) args, _ = p.parse_known_args() return args @@ -231,7 +235,7 @@ def _check_email_needed() -> str: return "" -def _drain_backlog() -> None: +def _drain_backlog(host: str = "claude") -> None: """Process queued sessions from the backlog directory. Uses the flock-based spawn gate from core to prevent the avalanche @@ -241,6 +245,16 @@ def _drain_backlog() -> None: Uses atomic rename (.json → .processing) to prevent TOCTOU races where multiple drainers read the same marker before either acquires the flock. """ + if os.environ.get("TRUEMEMORY_MAINTENANCE_CHILD") == "1": + from truememory.ingest.completion import drain, convert_legacy_pointers + user_id = os.environ.get("TRUEMEMORY_COMPLETION_USER", os.environ.get("TRUEMEMORY_USER_ID", "")) + db_path = os.environ.get("TRUEMEMORY_COMPLETION_DB", os.environ.get("TRUEMEMORY_DB_PATH", "")) + try: + drain(user_id, db_path, host) + convert_legacy_pointers(user_id, db_path, host) + except OSError as error: + log.warning("Drain retained incomplete capture (%s)", type(error).__name__) + return if not BACKLOG_DIR.exists(): return @@ -252,17 +266,25 @@ def _drain_backlog() -> None: mark_session_extracted, _quarantine_marker, is_allowed_transcript, + backlog_transcript_source, + normalize_transcript_host, ) + host = normalize_transcript_host(host) + if not host: + return cleanup_stale_processing(BACKLOG_DIR) try: - markers = sorted(BACKLOG_DIR.glob("*.json"))[:_DRAIN_CAP] + markers = sorted(BACKLOG_DIR.glob("*.json")) except Exception: return from truememory.hooks.core import spawn_gate, register_spawned_pid + drained = 0 for marker_path in markers: + if drained >= _DRAIN_CAP: + return claimed_path = marker_path.with_suffix(".processing") try: marker_path.rename(claimed_path) @@ -287,16 +309,24 @@ def _drain_backlog() -> None: continue try: - transcript = data.get("transcript_path", "") - if not transcript or not Path(transcript).exists(): - claimed_path.unlink(missing_ok=True) - continue # M-90: a backlog marker's transcript_path is attacker-influenceable; # confirm containment in an allowed transcripts root before parsing # (parse_transcript's plaintext fallback would read any readable file). - if not is_allowed_transcript(transcript): - log.warning("Drain: transcript_path %r outside allowed roots; dropping marker", transcript) - claimed_path.unlink(missing_ok=True) + if ( + isinstance(data, dict) + and normalize_transcript_host(data.get("transcript_host", "claude")) + and data.get("transcript_host", "claude") != host + ): + claimed_path.rename(marker_path) + continue + source = backlog_transcript_source(data, host=host) + if source is None: + log.warning("Drain: rejected backlog transcript source for host=%s", host) + _quarantine_marker(claimed_path) + continue + transcript, session_id = source + if not is_allowed_transcript(transcript, host=host) or not Path(transcript).is_file(): + _quarantine_marker(claimed_path) continue if not check_extraction_budget(): @@ -323,8 +353,9 @@ def _drain_backlog() -> None: cmd = [ sys.executable, "-m", "truememory.ingest.cli", "ingest", transcript, + "--transcript-host", host, + "--automatic", ] - session_id = data.get("session_id", "") if session_id: cmd.extend(["--session", session_id]) if data.get("user_id"): @@ -334,7 +365,7 @@ def _drain_backlog() -> None: from truememory.ingest.hooks._shared import _safe_session_id _log_dir = Path.home() / ".truememory" / "logs" _log_dir.mkdir(parents=True, exist_ok=True) - _safe_sid = _safe_session_id(data.get('session_id', 'unknown')) or 'unknown' + _safe_sid = _safe_session_id(session_id) or 'unknown' _log_file = open( _log_dir / f"{_safe_sid}.log", "a", encoding="utf-8", @@ -347,6 +378,7 @@ def _drain_backlog() -> None: stdin=subprocess.DEVNULL, **_platform.spawn_kwargs(), ) + drained += 1 finally: _log_file.close() register_spawned_pid(proc.pid) @@ -624,7 +656,7 @@ def _commit_watermark(value: float) -> None: pass -def _run_maintenance_background() -> None: +def _run_maintenance_background(host: str = "claude") -> None: """Spawn backlog drain + stale-session scan in a detached subprocess. Uses ``Popen(start_new_session=True)`` so the child survives hook exit @@ -637,8 +669,13 @@ def _run_maintenance_background() -> None: # storm). The flag was previously set but never read. if os.environ.get("TRUEMEMORY_MAINTENANCE_CHILD") == "1": return + from truememory.ingest.hooks._shared import normalize_transcript_host + host = normalize_transcript_host(host) + if not host: + return import subprocess + completion_account = globals().get("_COMPLETION_ACCOUNT") _log_dir = Path.home() / ".truememory" / "logs" _log_dir.mkdir(parents=True, exist_ok=True) @@ -646,8 +683,8 @@ def _run_maintenance_background() -> None: script = ( "from truememory.ingest.hooks.session_start import _drain_backlog, _scan_stale_sessions; " - "_drain_backlog(); " - "_scan_stale_sessions()" + f"_drain_backlog({host!r}); " + + ("_scan_stale_sessions()" if host == "claude" else "") ) try: @@ -658,7 +695,9 @@ def _run_maintenance_background() -> None: stdout=log_file, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, - env={**os.environ, "TRUEMEMORY_MAINTENANCE_CHILD": "1"}, + env={**os.environ, "TRUEMEMORY_MAINTENANCE_CHILD": "1", + **({"TRUEMEMORY_COMPLETION_USER": completion_account[0], + "TRUEMEMORY_COMPLETION_DB": completion_account[1]} if completion_account else {})}, **_platform.spawn_kwargs(), ) finally: @@ -706,13 +745,15 @@ def main(): return args = _parse_args() + global _COMPLETION_ACCOUNT + _COMPLETION_ACCOUNT = (args.user, args.db) # --scan-stale: background-only entry point (issue #558). if args.scan_stale: _scan_stale_sessions() return - _run_maintenance_background() + _run_maintenance_background(args.host) try: input_data = json.load(sys.stdin) @@ -748,16 +789,15 @@ def main(): if email_notice: context = (context or "") + "\n\n" + email_notice - if context: - output = {"additionalContext": context} - print(json.dumps(output)) + if context and emit_context(context, host=args.host, event=args.event): # Mark recall as injected so the first UserPromptSubmit can skip # its redundant per-message auto-recall (issue #561). Written only - # after the context has actually been emitted, and only when the + # after the supported response has been flushed, and only when the # recall portion was non-empty — an empty/failed recall must not # suppress the first prompt's targeted recall. if recall_injected: - session_id = input_data.get("session_id", "") + from truememory.ingest.hooks._shared import hook_session_id + session_id = hook_session_id(input_data, args.host, default="") if session_id: from truememory.ingest.hooks._shared import mark_recall_injected mark_recall_injected(session_id) diff --git a/truememory/ingest/hooks/stop.py b/truememory/ingest/hooks/stop.py index 26766d1a..f434c7d7 100755 --- a/truememory/ingest/hooks/stop.py +++ b/truememory/ingest/hooks/stop.py @@ -79,6 +79,8 @@ def _safe_int_env(name: str, default: int) -> int: def _sanitize_session_id(session_id: str) -> str: """Sanitize session_id to prevent path traversal.""" + if not isinstance(session_id, str): + return "unknown" safe = "".join(c for c in session_id if c.isalnum() or c in "-_")[:64] return safe or "unknown" @@ -108,47 +110,55 @@ def _parse_args() -> argparse.Namespace: p = argparse.ArgumentParser(add_help=False) p.add_argument("--user", default=os.environ.get("TRUEMEMORY_USER_ID", "")) p.add_argument("--db", default=os.environ.get("TRUEMEMORY_DB_PATH", "")) + p.add_argument("--host", choices=("claude", "codex", "gemini", "cursor"), default="claude") + p.add_argument("--event", default="Stop") args, _ = p.parse_known_args() return args -def main(): +def main() -> None: + args = _parse_args() if os.environ.get("TRUEMEMORY_EXTRACTION"): - try: - input_data = json.load(sys.stdin) - except (json.JSONDecodeError, EOFError): - input_data = {} - transcript_path = input_data.get("transcript_path", "") - session_id = input_data.get("session_id", "") - if transcript_path and session_id: - from truememory.ingest.hooks._shared import mark_session_extracted - try: - mark_session_extracted(session_id, transcript_path) - except Exception as exc: - log.debug("stop hook: failed to mark extraction session %s: %s", session_id, exc) return - args = _parse_args() - try: input_data = json.load(sys.stdin) except (json.JSONDecodeError, EOFError): input_data = {} - transcript_path = input_data.get("transcript_path", "") - session_id = input_data.get("session_id", "unknown") + if not isinstance(input_data, dict): + return + if input_data.get("stop_hook_active") is True: + return + from truememory.ingest.hooks._shared import is_allowed_transcript, transcript_input_path, hook_session_id + transcript_path = transcript_input_path(input_data.get("transcript_path", "")) + native_session_id = hook_session_id(input_data, host=args.host) + session_id = native_session_id if args.host == "claude" else _sanitize_session_id(native_session_id) - if not transcript_path or not Path(transcript_path).exists(): + if not transcript_path: return # M-90: transcript_path comes from attacker-influenceable hook stdin. # parse_transcript's plaintext fallback would read ANY readable file into # the memory store, so confirm containment in an allowed transcripts root # (the compact hook already did this; stop/drain previously did not). - from truememory.ingest.hooks._shared import is_allowed_transcript - if not is_allowed_transcript(transcript_path): - log.warning("stop hook: transcript_path %r outside allowed roots; skipping", transcript_path) + if not is_allowed_transcript(transcript_path, host=args.host): + log.warning("stop hook: transcript source rejected for host=%s", args.host) + return + if not Path(transcript_path).is_file(): return + capture = None + if args.host == "claude": + from truememory.ingest.completion import admit_capture, has_receipt + try: + capture = admit_capture(transcript_path, session_id, args.user, args.db, args.host, + root=BACKLOG_DIR.parent / "ingest-completion-v1") + if has_receipt(capture): + return + except OSError as error: + log.warning("stop hook: capture retained nonterminal source (%s)", type(error).__name__) + return + # Pre-flight check: ensure our write directories exist and are writable # so we fail early with a clear message instead of silently losing work if not _writable_dirs_ok(): @@ -157,15 +167,23 @@ def main(): # Proper transcript validation: parse and count actual message objects # instead of substring-matching (which false-positives on content that # contains the literal strings "human" or "user"). - if not _has_enough_messages(transcript_path, MIN_MESSAGES): + if not _has_enough_messages(str(capture.path) if capture else transcript_path, MIN_MESSAGES): return from truememory.ingest.hooks._shared import should_extract_session, mark_session_extracted - if not should_extract_session(session_id, transcript_path): + if capture is None and not should_extract_session(session_id, transcript_path): log.info("stop hook: session %s already extracted at this size, skipping", session_id) return - spawned_pid = _run_background_ingestion(transcript_path, session_id, args.user, args.db) + if capture is not None: + spawned_pid = _run_background_ingestion( + transcript_path, session_id, args.user, args.db, transcript_host=args.host, + completion_capture=capture, + ) + else: + spawned_pid = _run_background_ingestion( + transcript_path, session_id, args.user, args.db, transcript_host=args.host, + ) # Only mark the session extracted when a real ingest process was spawned. # _run_background_ingestion returns 0 when the spawn cap denied it or Popen @@ -224,56 +242,15 @@ def _writable_dirs_ok() -> bool: def _has_enough_messages(transcript_path: str, min_messages: int) -> bool: - """Check whether the transcript has at least `min_messages` user turns. + """Count meaningful canonical human turns, independently of completion coverage.""" + from truememory.ingest.transcript import parse_transcript_outcome, strip_truememory_blocks - Parses the transcript properly instead of substring-counting so that - conversations containing the literal strings "human"/"user" in content - don't inflate the count. - """ - try: - content = Path(transcript_path).read_text(encoding="utf-8", errors="replace") - except Exception: - return False - - if not content.strip(): + outcome = parse_transcript_outcome(Path(transcript_path)) + if outcome.status == "unreadable": return False - - # Try JSON array first (Claude Code format) - count = 0 - try: - if content.lstrip().startswith("["): - data = json.loads(content) - if isinstance(data, list): - for entry in data: - if isinstance(entry, dict): - role = entry.get("type") or entry.get("role") or "" - if role in ("human", "user"): - count += 1 - return count >= min_messages - except json.JSONDecodeError: - pass - - # Try JSONL - try: - for line in content.splitlines(): - line = line.strip() - if not line: - continue - try: - entry = json.loads(line) - if isinstance(entry, dict): - role = entry.get("type") or entry.get("role") or "" - if role in ("human", "user"): - count += 1 - except json.JSONDecodeError: - continue - if count > 0: - return count >= min_messages - except Exception: - pass - - # Fall back to length heuristic for plain text - return len(content) > min_messages * 50 + count = sum(message.role == "human" and bool(strip_truememory_blocks(message.content).strip()) + for message in outcome.messages) + return count >= min_messages def _count_active_ingest_processes() -> int: @@ -303,6 +280,7 @@ def _queue_to_backlog( user_id: str, db_path: str, reason: str, + transcript_host: str = "claude", ) -> None: """Drop a queue marker so a later session can re-attempt this ingestion. @@ -312,6 +290,13 @@ def _queue_to_backlog( BACKLOG_DIR and let the next session's startup hook drain it (drain path is a follow-up item). """ + from truememory.ingest.hooks._shared import is_allowed_transcript, normalize_transcript_host + transcript_host = normalize_transcript_host(transcript_host) + if not is_allowed_transcript(transcript_path, host=transcript_host): + return + if any(not isinstance(value, str) or "\0" in value for value in (user_id, db_path)): + return + session_id = _sanitize_session_id(session_id) try: from datetime import datetime, timezone BACKLOG_DIR.mkdir(parents=True, exist_ok=True) @@ -328,6 +313,7 @@ def _queue_to_backlog( marker = BACKLOG_DIR / f"{safe_sid}.json" _atomic_write_text(marker, json.dumps({ "transcript_path": transcript_path, + "transcript_host": transcript_host, "session_id": session_id, "user_id": user_id, "db_path": db_path, @@ -345,6 +331,9 @@ def _run_background_ingestion( session_id: str, user_id: str, db_path: str, + transcript_host: str = "claude", + *, + completion_capture=None, ) -> int: """Launch the ingestion pipeline as a background process. @@ -358,6 +347,21 @@ def _run_background_ingestion( for a later session to re-attempt — NEVER fall back to synchronous inline ingestion (that blocks Claude Code's shutdown). """ + from truememory.ingest.hooks._shared import is_allowed_transcript, normalize_transcript_host + transcript_host = normalize_transcript_host(transcript_host) + if not is_allowed_transcript(transcript_path, host=transcript_host): + log.warning("stop hook: background transcript source rejected") + return 0 + if any(not isinstance(value, str) or "\0" in value for value in (user_id, db_path)): + return 0 + if completion_capture is not None: + from truememory.ingest.completion import dispatch + try: + return dispatch(completion_capture, GATE_THRESHOLD) + except OSError as error: + log.warning("stop hook: completion dispatch retained work (%s)", type(error).__name__) + return 0 + session_id = _sanitize_session_id(session_id) # Log the effective config for debugging. Operators commonly wire up # multiple profiles and need to confirm which user/db the hook actually # saw after the arg-parse + env-var resolution. @@ -370,6 +374,8 @@ def _run_background_ingestion( cmd = [ sys.executable, "-m", "truememory.ingest.cli", "ingest", transcript_path, + "--transcript-host", transcript_host, + "--automatic", ] if user_id: @@ -405,6 +411,7 @@ def _run_background_ingestion( _queue_to_backlog( transcript_path, session_id, user_id, db_path, reason="extraction_budget_exhausted", + transcript_host=transcript_host, ) return 0 @@ -424,6 +431,7 @@ def _run_background_ingestion( _queue_to_backlog( transcript_path, session_id, user_id, db_path, reason=f"spawn_cap_reached:SPAWN_CAP={effective_cap}", + transcript_host=transcript_host, ) return 0 @@ -449,6 +457,7 @@ def _run_background_ingestion( _queue_to_backlog( transcript_path, session_id, user_id, db_path, reason=f"popen_failed:{type(e).__name__}:{e}", + transcript_host=transcript_host, ) return 0 finally: diff --git a/truememory/ingest/hooks/user_prompt_submit.py b/truememory/ingest/hooks/user_prompt_submit.py index 93003639..840ef08d 100755 --- a/truememory/ingest/hooks/user_prompt_submit.py +++ b/truememory/ingest/hooks/user_prompt_submit.py @@ -32,6 +32,7 @@ from truememory._platform import _env_int from truememory._sanitize import sanitize_injection_content +from truememory.ingest.hooks.output import HOOK_HOSTS, emit_context, supports_context # Optional: fcntl isn't available on Windows, so we gracefully degrade try: @@ -83,15 +84,14 @@ def _word_overlap(a: str, b: str) -> float: def _parse_args() -> argparse.Namespace: """Parse command-line overrides the installer threads through. - UserPromptSubmit doesn't actually use ``--user`` or ``--db`` — it only - writes a per-session diagnostic buffer — but the installer passes the - same flags to every hook for consistency, so we must accept them here - without erroring out. ``parse_known_args`` ensures forward compat with - future flags. + The trusted host/event select recall output and transcript admission. + ``parse_known_args`` keeps the existing forward-compatible flags. """ p = argparse.ArgumentParser(add_help=False) p.add_argument("--user", default=os.environ.get("TRUEMEMORY_USER_ID", "")) p.add_argument("--db", default=os.environ.get("TRUEMEMORY_DB_PATH", "")) + p.add_argument("--host", choices=HOOK_HOSTS, default="claude") + p.add_argument("--event", default="UserPromptSubmit") args, _ = p.parse_known_args() return args @@ -737,7 +737,8 @@ def main(): return prompt = input_data.get("prompt", "").strip() - session_id = input_data.get("session_id", "unknown") + from truememory.ingest.hooks._shared import hook_session_id + session_id = hook_session_id(input_data, args.host) if not prompt or len(prompt) < 3: # A too-short first prompt is still the session's first prompt: @@ -765,10 +766,16 @@ def main(): prompt_count = _increment_prompt_count(session_id) transcript_path = input_data.get("transcript_path", "") - if transcript_path and Path(transcript_path).exists(): + if transcript_path: try: - from truememory.ingest.hooks._shared import should_extract_session, mark_session_extracted - if should_extract_session(session_id, transcript_path): + from truememory.ingest.hooks._shared import ( + is_allowed_transcript, should_extract_session, mark_session_extracted, + ) + if ( + is_allowed_transcript(transcript_path, host=args.host) + and Path(transcript_path).is_file() + and should_extract_session(session_id, transcript_path) + ): from truememory.ingest.hooks.stop import ( _has_enough_messages, _run_background_ingestion, TRACE_DIR, LOG_DIR, @@ -778,6 +785,7 @@ def main(): if _has_enough_messages(transcript_path, 10): spawned_pid = _run_background_ingestion( transcript_path, session_id, args.user, args.db, + transcript_host=args.host, ) # Only mark extracted on a real spawn; pid==0 means the # session was queued to the backlog and must stay eligible @@ -793,6 +801,11 @@ def main(): except Exception: pass # Never crash the hook + # Capture still runs on Cursor, whose beforeSubmitPrompt response cannot + # inject context. Avoid searching for recall that the host cannot receive. + if not supports_context(args.host, args.event): + return + # Issue #561 / #636 (M-41): consume the one-shot SessionStart recall marker # exactly ONCE per prompt. Both the proactive and auto-recall paths used to # call consume_recall_injected() independently — the proactive path consumed @@ -828,7 +841,7 @@ def main(): ) if recall_context: - print(json.dumps({"additionalContext": recall_context})) + emit_context(recall_context, host=args.host, event=args.event) def buffer_message(session_id: str, prompt: str): diff --git a/truememory/ingest/pipeline.py b/truememory/ingest/pipeline.py index a10cbe3b..f4822007 100644 --- a/truememory/ingest/pipeline.py +++ b/truememory/ingest/pipeline.py @@ -31,6 +31,7 @@ import re import sqlite3 import time +from collections.abc import Iterator from dataclasses import dataclass, field from pathlib import Path @@ -39,6 +40,10 @@ from truememory.ingest.encoding_gate import EncodingGate from truememory.ingest.dedup import check_duplicate, DedupAction from truememory.ingest.models import LLMConfig, auto_detect +from truememory.ingest.provenance import ( + ProvenanceError, SourceSegment, fact_metadata, gate_observation, + merge_metadata, source_context, source_timestamp, +) log = logging.getLogger(__name__) @@ -343,6 +348,24 @@ class IngestionResult: facts_skipped_dedup: int = 0 # Blocked by dedup elapsed_seconds: float = 0.0 trace: list[dict] = field(default_factory=list) # Per-fact decision log + facts_provenance_updated: int = 0 # Metadata-only writes after dedup SKIP + coverage: dict | None = None # Observed rich outcomes, never inferred from counters + + +@contextlib.contextmanager +def _invalidate_recall_after_batch(result: IngestionResult) -> Iterator[None]: + """Invalidate confirmed writes even when a later fact aborts the batch.""" + try: + yield + finally: + if result.facts_stored or result.facts_updated or result.facts_provenance_updated: + try: + from truememory.ingest.hooks._shared import invalidate_recall_cache + invalidate_recall_cache() + except Exception as exc: + # A cache failure must not replay committed facts or replace + # the exception that interrupted the remaining ingestion. + log.warning("Recall cache invalidation failed after committed ingestion (%s)", type(exc).__name__) class IngestionPipeline: @@ -387,6 +410,7 @@ def __init__( _set_busy_timeout(memory) self.memory = memory + self._record_source_provenance = True self.user_id = user_id self.use_llm_dedup = use_llm_dedup @@ -416,6 +440,8 @@ def ingest_transcript( self, transcript_path: str | Path, session_id: str = "", + *, + require_complete: bool = False, ) -> IngestionResult: """ Ingest a full conversation transcript. @@ -438,7 +464,24 @@ def ingest_transcript( # 1. Parse transcript log.info("Parsing transcript: %s", transcript_path) - messages = parse_transcript(transcript_path) + if require_complete: + from truememory.ingest.transcript import parse_transcript_outcome + parsed = parse_transcript_outcome(transcript_path) + messages = parsed.messages + version = parsed.file_version + result.coverage = { + "parser_status": parsed.status, + "parser_errors": list(getattr(parsed, "error_categories", ())), + "malformed_records": getattr(parsed, "malformed_records", 0), + "unrecognized_records": getattr(parsed, "unrecognized_records", 0), + "stable": version is not None and version.stable, + "source_version": {"sha256": version.sha256, "byte_count": version.byte_count} if version else None, + "extractor_status": "unobserved", + "extractor_calls": 0, + "provenance_observed": getattr(self, "_record_source_provenance", False) is True, + } + else: + messages = parse_transcript(transcript_path) if not messages: log.info("Empty transcript, nothing to ingest") result.elapsed_seconds = time.time() - start @@ -446,6 +489,17 @@ def ingest_transcript( # Filter to conversation messages only (skip tool calls) conversation = format_for_extraction(messages) + context = None + if getattr(self, "_record_source_provenance", False): + segments = [] + offset = 0 + for index, message in enumerate(messages): + formatted = format_for_extraction([message]) + if not formatted: + continue + segments.append(SourceSegment(offset, offset + len(formatted), index, message.role, message.timestamp)) + offset += len(formatted) + 2 + context = source_context(conversation, session_id, tuple(segments)) if len(conversation.strip()) < 50: log.info("Transcript too short for extraction (%d chars)", len(conversation)) result.elapsed_seconds = time.time() - start @@ -454,9 +508,19 @@ def ingest_transcript( # 2. Extract facts log.info("Extracting facts from %d messages (%d chars)", len(messages), len(conversation)) if self.llm_config: - facts = extract_facts(conversation, self.llm_config) + if require_complete: + from truememory.ingest.extractor import extract_facts_outcome + extracted = extract_facts_outcome(conversation, self.llm_config) + facts = extracted.facts + result.coverage["extractor_status"] = extracted.status + result.coverage["extractor_calls"] = 1 + result.coverage["extractor_errors"] = list(getattr(extracted, "error_categories", ())) + else: + facts = extract_facts(conversation, self.llm_config) else: facts = extract_facts_simple(conversation) + if require_complete: + result.coverage["extractor_status"] = "heuristic" result.facts_extracted = len(facts) log.info("Extracted %d candidate facts", len(facts)) @@ -465,117 +529,138 @@ def ingest_transcript( return result # 3-5. Process each fact through gate and dedup - for fact in facts: - trace_entry = { - "fact": fact.content, - "category": fact.category, - "confidence": fact.confidence, - } - - # 3. Encoding gate - if self.gate_enabled: - decision = self.gate.evaluate(fact.content, fact.category) - else: - from truememory.ingest.encoding_gate import EncodingDecision - decision = EncodingDecision( - should_encode=True, encoding_score=1.0, - novelty=1.0, salience=1.0, prediction_error=1.0, - reason="gate disabled", - ) - trace_entry["gate"] = { - "passed": decision.should_encode, - "score": decision.encoding_score, - "novelty": decision.novelty, - "salience": decision.salience, - "prediction_error": decision.prediction_error, - "reason": decision.reason, - } - - if not decision.should_encode: - result.facts_skipped_gate += 1 - trace_entry["action"] = "skipped_gate" - result.trace.append(trace_entry) - log.debug("Gate blocked: %s — %s", _safe_log(fact.content[:50]), decision.reason) - continue - - result.facts_encoded += 1 - - # 4-5. Deduplication + storage (atomic critical section). - # - # Holding the process-level lock around BOTH the dedup search - # and the subsequent add/update prevents a TOCTOU race where - # process A checks "no duplicate" for fact X, process B writes - # fact Y (semantically equal), then A writes X as new. Without - # this lock, concurrent Stop hooks from overlapping Claude Code - # sessions accumulate near-duplicate memories. - with _dedup_store_lock(): - dedup = check_duplicate( - fact.content, - self.memory, - user_id=self.user_id, - config=self.llm_config if self.use_llm_dedup else None, - category=fact.category, - ) - trace_entry["dedup"] = { - "action": dedup.action.value, - "reason": dedup.reason, - "existing_id": dedup.existing_id, - } - - # We catch sqlite3.OperationalError around the storage calls - # so a transient DB lock or "unable to open database file" - # condition doesn't abort the whole transcript with a stack - # trace. The pipeline continues with the remaining facts; - # each failed fact is flagged in the trace as - # ``storage_failed`` so operators can diagnose what got - # dropped. See Bug #2 in EDGE_CASE_REPORT.md. - if dedup.action == DedupAction.ADD: - try: - self._store_fact(dedup.fact, fact, session_id) - except sqlite3.OperationalError as e: - log.error( - "Storage failed for fact (db=%s): %s — fact=%r", - getattr(self.memory, "db_path", ""), - e, - _safe_log(dedup.fact[:120]), - ) - trace_entry["action"] = "storage_failed" - trace_entry["storage_error"] = { - "reason": "db locked" if "locked" in str(e).lower() else str(e), - "exception": type(e).__name__, - } + with _invalidate_recall_after_batch(result): + for fact in facts: + from truememory.tier_switch.runtime import fact_operation + with fact_operation(self.memory): + trace_entry = { + "fact": fact.content, + "category": fact.category, + "confidence": fact.confidence, + } + + # 3. Encoding gate + if self.gate_enabled: + decision = self.gate.evaluate(fact.content, fact.category) else: - result.facts_stored += 1 - trace_entry["action"] = "stored" - log.info("Stored: %s", _safe_log(dedup.fact[:80])) - - elif dedup.action == DedupAction.UPDATE: - try: - self._update_fact(dedup.existing_id, dedup.fact, fact, session_id) - except sqlite3.OperationalError as e: - log.error( - "Update failed for memory id=%s (db=%s): %s — fact=%r", - dedup.existing_id, - getattr(self.memory, "db_path", ""), - e, - _safe_log(dedup.fact[:120]), + from truememory.ingest.encoding_gate import EncodingDecision + decision = EncodingDecision( + should_encode=True, encoding_score=1.0, + novelty=1.0, salience=1.0, prediction_error=1.0, + reason="gate disabled", ) - trace_entry["action"] = "storage_failed" - trace_entry["storage_error"] = { - "reason": "db locked" if "locked" in str(e).lower() else str(e), - "exception": type(e).__name__, - } - else: - result.facts_updated += 1 - trace_entry["action"] = "updated" - log.info("Updated [%s]: %s", dedup.existing_id, _safe_log(dedup.fact[:80])) + trace_entry["gate"] = { + "passed": decision.should_encode, + "score": decision.encoding_score, + "novelty": decision.novelty, + "salience": decision.salience, + "prediction_error": decision.prediction_error, + "reason": decision.reason, + } + + if not decision.should_encode: + result.facts_skipped_gate += 1 + trace_entry["action"] = "skipped_gate" + result.trace.append(trace_entry) + log.debug("Gate blocked: %s — %s", _safe_log(fact.content[:50]), decision.reason) + continue - elif dedup.action == DedupAction.SKIP: - result.facts_skipped_dedup += 1 - trace_entry["action"] = "skipped_dedup" - log.debug("Dedup skipped: %s — %s", _safe_log(fact.content[:50]), dedup.reason) + result.facts_encoded += 1 + + # 4-5. Deduplication + storage (atomic critical section). + # + # Holding the process-level lock around BOTH the dedup search + # and the subsequent add/update prevents a TOCTOU race where + # process A checks "no duplicate" for fact X, process B writes + # fact Y (semantically equal), then A writes X as new. Without + # this lock, concurrent Stop hooks from overlapping Claude Code + # sessions accumulate near-duplicate memories. + with _dedup_store_lock(): + dedup = check_duplicate( + fact.content, + self.memory, + user_id=self.user_id, + config=self.llm_config if self.use_llm_dedup else None, + category=fact.category, + ) + trace_entry["dedup"] = { + "action": dedup.action.value, + "reason": dedup.reason, + "existing_id": dedup.existing_id, + } - result.trace.append(trace_entry) + # We catch sqlite3.OperationalError around the storage calls + # so a transient DB lock or "unable to open database file" + # condition doesn't abort the whole transcript with a stack + # trace. The pipeline continues with the remaining facts; + # each failed fact is flagged in the trace as + # ``storage_failed`` so operators can diagnose what got + # dropped. See Bug #2 in EDGE_CASE_REPORT.md. + if dedup.action == DedupAction.ADD: + try: + metadata = fact_metadata(context, fact, gate_observation(self.gate, decision, self.gate_enabled), datetime.datetime.now(datetime.timezone.utc).isoformat()) if context is not None else None + if context is None: + self._store_fact(dedup.fact, fact, session_id) + else: + self._store_fact(dedup.fact, fact, session_id, metadata=metadata) + except sqlite3.OperationalError as e: + log.error( + "Storage failed for fact (db=%s): %s — fact=%r", + getattr(self.memory, "db_path", ""), + e, + _safe_log(dedup.fact[:120]), + ) + trace_entry["action"] = "storage_failed" + trace_entry["storage_error"] = { + "reason": "db locked" if "locked" in str(e).lower() else str(e), + "exception": type(e).__name__, + } + else: + result.facts_stored += 1 + trace_entry["action"] = "stored" + log.info("Stored: %s", _safe_log(dedup.fact[:80])) + + elif dedup.action == DedupAction.UPDATE: + try: + metadata = fact_metadata(context, fact, gate_observation(self.gate, decision, self.gate_enabled), datetime.datetime.now(datetime.timezone.utc).isoformat()) if context is not None else None + if context is None: + self._update_fact(dedup.existing_id, dedup.fact, fact, session_id) + else: + self._update_fact(dedup.existing_id, dedup.fact, fact, session_id, metadata=metadata) + except sqlite3.OperationalError as e: + log.error( + "Update failed for memory id=%s (db=%s): %s — fact=%r", + dedup.existing_id, + getattr(self.memory, "db_path", ""), + e, + _safe_log(dedup.fact[:120]), + ) + trace_entry["action"] = "storage_failed" + trace_entry["storage_error"] = { + "reason": "db locked" if "locked" in str(e).lower() else str(e), + "exception": type(e).__name__, + } + else: + result.facts_updated += 1 + trace_entry["action"] = "updated" + log.info("Updated [%s]: %s", dedup.existing_id, _safe_log(dedup.fact[:80])) + + elif dedup.action == DedupAction.SKIP: + try: + metadata = fact_metadata(context, fact, gate_observation(self.gate, decision, self.gate_enabled), datetime.datetime.now(datetime.timezone.utc).isoformat()) if context is not None else None + changed = self._merge_fact_provenance(dedup.existing_id, metadata) if context is not None else False + except sqlite3.OperationalError as exc: + trace_entry["action"] = "storage_failed" + trace_entry["storage_error"] = {"reason": "provenance write failed", "exception": type(exc).__name__} + log.error("Dedup provenance write failed (%s)", type(exc).__name__) + else: + result.facts_skipped_dedup += 1 + result.facts_provenance_updated += int(changed) + trace_entry["action"] = "skipped_dedup" + trace_entry["provenance_updated"] = changed + log.debug("Dedup skipped: %s — %s", _safe_log(fact.content[:50]), dedup.reason) + + result.trace.append(trace_entry) result.elapsed_seconds = round(time.time() - start, 2) log.info( @@ -585,17 +670,6 @@ def ingest_transcript( result.facts_skipped_gate, result.facts_skipped_dedup, result.elapsed_seconds, ) - # Issue #645 (M-64): the engine.add fast path bypasses - # client.Memory.add (which invalidates), so a full transcript could - # store fresh facts without ever dropping the recall cache — the next - # session would inject stale recall for the rest of the TTL. Invalidate - # once after the batch commits if anything was written. - if result.facts_stored or result.facts_updated: - try: - from truememory.ingest.hooks._shared import invalidate_recall_cache - invalidate_recall_cache() - except Exception: - pass return result def ingest_text(self, text: str, session_id: str = "") -> IngestionResult: @@ -614,6 +688,7 @@ def ingest_text(self, text: str, session_id: str = "") -> IngestionResult: if len(text.strip()) < 50: result.elapsed_seconds = time.time() - start return result + context = source_context(text, session_id) if getattr(self, "_record_source_provenance", False) else None # Extract facts if self.llm_config: @@ -626,51 +701,71 @@ def ingest_text(self, text: str, session_id: str = "") -> IngestionResult: result.elapsed_seconds = time.time() - start return result - for fact in facts: - if self.gate_enabled: - decision = self.gate.evaluate(fact.content, fact.category) - else: - from truememory.ingest.encoding_gate import EncodingDecision - decision = EncodingDecision( - should_encode=True, encoding_score=1.0, - novelty=1.0, salience=1.0, prediction_error=1.0, - reason="gate disabled", - ) - if not decision.should_encode: - result.facts_skipped_gate += 1 - continue - result.facts_encoded += 1 - - # Hold the process-level lock across the dedup-then-store pair - # so concurrent ingest callers don't race and produce duplicates. - with _dedup_store_lock(): - dedup = check_duplicate( - fact.content, - self.memory, - user_id=self.user_id, - config=self.llm_config if self.use_llm_dedup else None, - category=fact.category, - ) - - if dedup.action == DedupAction.ADD: - try: - self._store_fact(dedup.fact, fact, session_id) - except sqlite3.OperationalError as e: - log.error("Storage failed in ingest_text: %s — fact=%r", e, _safe_log(dedup.fact[:120])) - continue - result.facts_stored += 1 - elif dedup.action == DedupAction.UPDATE: - try: - self._update_fact(dedup.existing_id, dedup.fact, fact, session_id) - except sqlite3.OperationalError as e: - log.error( - "Update failed in ingest_text for memory id=%s: %s — fact=%r", - dedup.existing_id, e, _safe_log(dedup.fact[:120]), + with _invalidate_recall_after_batch(result): + for fact in facts: + from truememory.tier_switch.runtime import fact_operation + with fact_operation(self.memory): + if self.gate_enabled: + decision = self.gate.evaluate(fact.content, fact.category) + else: + from truememory.ingest.encoding_gate import EncodingDecision + decision = EncodingDecision( + should_encode=True, encoding_score=1.0, + novelty=1.0, salience=1.0, prediction_error=1.0, + reason="gate disabled", ) + if not decision.should_encode: + result.facts_skipped_gate += 1 continue - result.facts_updated += 1 - else: - result.facts_skipped_dedup += 1 + result.facts_encoded += 1 + + # Hold the process-level lock across the dedup-then-store pair + # so concurrent ingest callers don't race and produce duplicates. + with _dedup_store_lock(): + dedup = check_duplicate( + fact.content, + self.memory, + user_id=self.user_id, + config=self.llm_config if self.use_llm_dedup else None, + category=fact.category, + ) + + if dedup.action == DedupAction.ADD: + try: + metadata = fact_metadata(context, fact, gate_observation(self.gate, decision, self.gate_enabled), datetime.datetime.now(datetime.timezone.utc).isoformat()) if context is not None else None + if context is None: + self._store_fact(dedup.fact, fact, session_id) + else: + self._store_fact(dedup.fact, fact, session_id, metadata=metadata) + except sqlite3.OperationalError as e: + log.error("Storage failed in ingest_text: %s — fact=%r", e, _safe_log(dedup.fact[:120])) + result.trace.append({"action": "storage_failed", "storage_error": {"reason": "source write failed", "exception": type(e).__name__}}) + continue + result.facts_stored += 1 + elif dedup.action == DedupAction.UPDATE: + try: + metadata = fact_metadata(context, fact, gate_observation(self.gate, decision, self.gate_enabled), datetime.datetime.now(datetime.timezone.utc).isoformat()) if context is not None else None + if context is None: + self._update_fact(dedup.existing_id, dedup.fact, fact, session_id) + else: + self._update_fact(dedup.existing_id, dedup.fact, fact, session_id, metadata=metadata) + except sqlite3.OperationalError as e: + log.error( + "Update failed in ingest_text for memory id=%s: %s — fact=%r", + dedup.existing_id, e, _safe_log(dedup.fact[:120]), + ) + result.trace.append({"action": "storage_failed", "storage_error": {"reason": "source write failed", "exception": type(e).__name__}}) + continue + result.facts_updated += 1 + else: + try: + metadata = fact_metadata(context, fact, gate_observation(self.gate, decision, self.gate_enabled), datetime.datetime.now(datetime.timezone.utc).isoformat()) if context is not None else None + changed = self._merge_fact_provenance(dedup.existing_id, metadata) if context is not None else False + except sqlite3.OperationalError as exc: + result.trace.append({"action": "storage_failed", "storage_error": {"reason": "provenance write failed", "exception": type(exc).__name__}}) + continue + result.facts_skipped_dedup += 1 + result.facts_provenance_updated += int(changed) result.elapsed_seconds = round(time.time() - start, 2) return result @@ -680,6 +775,132 @@ def _store_fact( content: str, fact: ExtractedFact, session_id: str, + *, + metadata: dict[str, object] | None = None, + ) -> None: + """Store source time and provenance together, without a lossy fallback.""" + if not getattr(self, "_record_source_provenance", False): + self._store_legacy_fact(content, fact, session_id) + return + if metadata is None: + metadata = fact_metadata(source_context(content, session_id), fact, {}, datetime.datetime.now(datetime.timezone.utc).isoformat()) + + # Encode category tag inline so it survives to storage + # Format: "[category] content" — recognizable by humans and LLMs + tagged_content = f"[{fact.category}] {content}" if fact.category and fact.category != "general" else content + + engine = getattr(self.memory, "_engine", None) + if engine is not None and hasattr(engine, "add"): + try: + stored = engine.add( + content=tagged_content, + sender=self.user_id or "", + timestamp=source_timestamp(metadata), + category=fact.category or "", + metadata=metadata, + ) + if getattr(self, "_record_source_provenance", False) is True and ( + type(stored) is not dict or type(stored.get("id")) is not int or stored["id"] <= 0 + or stored.get("content") != tagged_content or stored.get("sender") != (self.user_id or "") + or stored.get("timestamp") != source_timestamp(metadata) + or stored.get("category") != (fact.category or "") or stored.get("metadata") != metadata + ): + raise ProvenanceError("source-aware engine add acknowledgement unavailable") + return + except sqlite3.OperationalError: + # Real DB failure (locked, unwritable, etc.) — propagate so + # the pipeline loop records it in the trace. + raise + except Exception as e: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(e) + raise ProvenanceError("engine could not preserve source provenance") from e + + # An older client stamps ingestion time and cannot preserve unknown or + # backfilled source time. Refuse that write before it can lose evidence. + raise ProvenanceError("source-aware engine add capability unavailable") + + def _update_fact( + self, + existing_id: int | None, + new_content: str, + fact: ExtractedFact, + session_id: str, + *, + metadata: dict[str, object] | None = None, + ) -> None: + """Merge immutable citations in the same source write as the content.""" + if not getattr(self, "_record_source_provenance", False): + self._update_legacy_fact(existing_id, new_content, fact, session_id) + return + if metadata is None: + metadata = fact_metadata(source_context(new_content, session_id), fact, {}, datetime.datetime.now(datetime.timezone.utc).isoformat()) + if existing_id is not None: + engine = getattr(self.memory, "_engine", None) + getter = getattr(engine, "get", None) + updater = getattr(engine, "update", None) + if not callable(getter) or not callable(updater): + raise ProvenanceError("source-aware engine update capability unavailable") + try: + import inspect + if ("_source_snapshot" not in inspect.signature(getter).parameters + or "_expected_source" not in inspect.signature(updater).parameters): + raise ProvenanceError("source-fenced engine update capability unavailable") + existing = getter(existing_id, _source_snapshot=True) + if existing is not None: + merged, _ = merge_metadata(existing.get("metadata", {}), metadata) + result = updater(existing_id, content=new_content, metadata=merged, + timestamp=source_timestamp(merged), _expected_source=existing) + if result is not None: + log.debug("Updated memory %d with: %s", existing_id, _safe_log(new_content[:60])) + return + except sqlite3.OperationalError: + # DB lock / "unable to open" — propagate so the pipeline + # loop records a storage_failed entry (Bug #2). + raise + except Exception as e: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(e) + raise ProvenanceError("engine could not preserve merged source provenance") from e + + self._store_fact(new_content, fact, session_id, metadata=metadata) + + def _merge_fact_provenance(self, existing_id: int | None, metadata: dict[str, object]) -> bool: + """Append a citation on SKIP without changing content or its primary vector.""" + if existing_id is None: + raise ProvenanceError("dedup source target identifier unavailable") + engine = getattr(self.memory, "_engine", None) + getter, updater = getattr(engine, "get", None), getattr(engine, "update", None) + if not callable(getter) or not callable(updater): + raise ProvenanceError("source-aware engine update capability unavailable") + try: + import inspect + if ("_source_snapshot" not in inspect.signature(getter).parameters + or "_expected_source" not in inspect.signature(updater).parameters): + raise ProvenanceError("source-fenced engine update capability unavailable") + existing = getter(existing_id, _source_snapshot=True) + if existing is None: + raise ProvenanceError("dedup source target no longer exists") + merged, changed = merge_metadata(existing.get("metadata", {}), metadata) + if not changed: + return False + if updater(existing_id, metadata=merged, timestamp=source_timestamp(merged), + _expected_source=existing) is None: + raise ProvenanceError("dedup source target no longer exists") + return True + except sqlite3.OperationalError: + raise + except Exception as exc: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(exc) + raise ProvenanceError("engine could not preserve dedup source provenance") from exc + + + def _store_legacy_fact( + self, + content: str, + fact: ExtractedFact, + session_id: str, ) -> None: """Store a new fact in truememory. @@ -719,6 +940,8 @@ def _store_fact( # the pipeline loop records it in the trace. raise except Exception as e: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(e) # Signature mismatch or other recoverable error — fall back # to the client-level API. log.warning("Engine-level add failed: %s, falling back to client", e) @@ -729,7 +952,7 @@ def _store_fact( user_id=self.user_id or None, ) - def _update_fact( + def _update_legacy_fact( self, existing_id: int | None, new_content: str, @@ -753,10 +976,12 @@ def _update_fact( # loop records a storage_failed entry (Bug #2). raise except Exception as e: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(e) log.warning("Failed to update memory %d: %s, storing as new", existing_id, e) # Fallback: store as new if update fails or isn't available - self._store_fact(new_content, fact, session_id) + self._store_legacy_fact(new_content, fact, session_id) def save_trace(result: IngestionResult, output_path: str | Path) -> bool: @@ -774,6 +999,7 @@ def save_trace(result: IngestionResult, output_path: str | Path) -> bool: "facts_encoded": result.facts_encoded, "facts_stored": result.facts_stored, "facts_updated": result.facts_updated, + "facts_provenance_updated": result.facts_provenance_updated, "facts_skipped_gate": result.facts_skipped_gate, "facts_skipped_dedup": result.facts_skipped_dedup, "elapsed_seconds": result.elapsed_seconds, diff --git a/truememory/ingest/provenance.py b/truememory/ingest/provenance.py new file mode 100644 index 00000000..2103c150 --- /dev/null +++ b/truememory/ingest/provenance.py @@ -0,0 +1,263 @@ +"""Bounded input-scope citations, distinct from precise fact attribution.""" + +from __future__ import annotations + +import datetime +import hashlib +import json +import math +import sqlite3 +from dataclasses import dataclass +from functools import lru_cache +from pathlib import Path + +NAMESPACE = "truememory_ingest" +MAX_CITATIONS = 64 +MAX_NAMESPACE_BYTES = 32768 +MAX_SESSION_BYTES = 256 +_MAX_SCORER_BYTES = 1024 * 1024 + + +class ProvenanceError(sqlite3.OperationalError): + """A citation cannot be persisted without losing existing evidence.""" + + +@dataclass(frozen=True) +class SourceSegment: + start: int + end: int + message_index: int + role: str + timestamp: object + + +@dataclass(frozen=True) +class SourceContext: + digest: str + scope_digest: str + length: int + session_id: str + segments: tuple[SourceSegment, ...] = () + + +def source_context(text: str, session_id: str, segments: tuple[SourceSegment, ...] = ()) -> SourceContext: + if not isinstance(session_id, str): + raise ProvenanceError("source session identifier exceeds provenance bounds") + try: + encoded_session, encoded_text = session_id.encode("utf-8"), text.encode("utf-8") + except UnicodeError as exc: + raise ProvenanceError("source provenance encoding is invalid") from exc + if len(encoded_session) > MAX_SESSION_BYTES: + raise ProvenanceError("source session identifier exceeds provenance bounds") + scope_identity = json.dumps([ + [segment.start, segment.end, segment.message_index, segment.role, _known_time(segment.timestamp)] + for segment in segments + ], separators=(",", ":")) + return SourceContext(hashlib.sha256(encoded_text).hexdigest(), hashlib.sha256(scope_identity.encode("utf-8")).hexdigest(), len(text), session_id, segments) + + +def _known_time(value: object) -> str | None: + if not isinstance(value, str) or not value or len(value) > 64: + return None + try: + parsed = datetime.datetime.fromisoformat(value.replace("Z", "+00:00")) + if parsed.tzinfo is None: + return None + return parsed.astimezone(datetime.timezone.utc).isoformat() + except (ValueError, OverflowError): + return None + + +@lru_cache(maxsize=1) +def _scorer_sources() -> dict[str, str | None]: + package = Path(__file__).parent + sources: dict[str, str | None] = {} + for relative in ("encoding_gate.py", "encoding_salience.py", "markers.py", "../salience.py"): + try: + with (package / relative).open("rb") as stream: + data = stream.read(_MAX_SCORER_BYTES + 1) + sources[relative] = hashlib.sha256(data).hexdigest() if len(data) <= _MAX_SCORER_BYTES else None + except OSError: + sources[relative] = None + return sources + + +def _number(value: object) -> float | None: + if isinstance(value, (int, float)) and not isinstance(value, bool): + try: + return float(value) if math.isfinite(value) else None + except (OverflowError, ValueError): + return None + return None + + +def gate_observation(gate: object, decision: object, enabled: bool) -> dict[str, object]: + status = "disabled" if not enabled else ( + "degraded_open" if getattr(gate, "_pe_available", True) is False else "evaluated" + ) + sources = _scorer_sources() + return { + "enabled": enabled, + "status": status, + "passed": getattr(decision, "should_encode", False) is True, + "score": _number(getattr(decision, "encoding_score", None)) if enabled else None, + "novelty": _number(getattr(decision, "novelty", None)) if enabled else None, + "salience": _number(getattr(decision, "salience", None)) if enabled else None, + "prediction_error": _number(getattr(decision, "prediction_error", None)) if enabled else None, + "settings": { + key: _number(getattr(gate, key, None)) + for key in ("threshold", "w_novelty", "w_salience", "w_prediction_error", "salience_floor") + }, + "scorer": "truememory.ingest.encoding_gate.EncodingGate", + "source_fingerprint_status": "observed_files" if all(sources.values()) else "unavailable", + "source_sha256": dict(sources), + } + + +def _scope(context: SourceContext, start: int, end: int) -> dict[str, object]: + segments = [segment for segment in context.segments if segment.start < end and segment.end > start] + known = [timestamp for segment in segments if (timestamp := _known_time(segment.timestamp)) is not None] + unknown = len(segments) - len(known) if segments else 1 + status = "mixed" if known and unknown else "known" if known else "unknown" + return { + "kind": "extraction_input_scope", + "formatted_start": start, + "formatted_end": end, + "message_start": segments[0].message_index if segments else None, + "message_end": segments[-1].message_index if segments else None, + "message_count": len(segments), + "observed_roles": sorted({segment.role for segment in segments if segment.role in ("human", "assistant")}), + "source_time": { + "status": status, + "start": min(known) if known else None, + "end": max(known) if known else None, + "unknown_count": unknown, + }, + } + + +def _check_bounds(namespace: dict[str, object]) -> None: + citations = namespace.get("citations") + if not isinstance(citations, list) or len(citations) > MAX_CITATIONS: + raise ProvenanceError("source citation count exceeds provenance bounds") + try: + encoded = json.dumps(namespace, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8") + except (TypeError, ValueError) as exc: + raise ProvenanceError("source provenance is not valid bounded JSON") from exc + if len(encoded) > MAX_NAMESPACE_BYTES: + raise ProvenanceError("source provenance byte count exceeds provenance bounds") + + +def fact_metadata(context: SourceContext, fact: object, observation: dict[str, object], ingested_at: str) -> dict[str, object]: + spans = getattr(fact, "source_spans", ()) or ((0, context.length),) + content = getattr(fact, "content", "") + if not isinstance(content, str): + raise ProvenanceError("source fact content is invalid") + fact_digest = hashlib.sha256(content.strip().lower().encode("utf-8")).hexdigest() + citations: list[dict[str, object]] = [] + seen: set[str] = set() + for span in spans: + if (not isinstance(span, tuple) or len(span) != 2 + or any(type(value) is not int for value in span) + or not 0 <= span[0] <= span[1] <= context.length): + raise ProvenanceError("source extraction span is invalid") + start, end = span + identity = json.dumps([context.session_id, context.digest, context.scope_digest, fact_digest, start, end], separators=(",", ":")) + citation_id = hashlib.sha256(identity.encode("utf-8")).hexdigest() + if citation_id in seen: + continue + seen.add(citation_id) + citations.append({ + "id": citation_id, + "session_id": context.session_id or None, + "formatted_input_sha256": context.digest, + "source_scope_sha256": context.scope_digest, + "fact_sha256": fact_digest, + "scope": _scope(context, start, end), + "extractor_role_assertion": getattr(fact, "source_role", "") if getattr(fact, "source_role", "") in ("user", "inferred") else "unknown", + "gate": observation, + }) + if len(citations) > MAX_CITATIONS: + raise ProvenanceError("source citation count exceeds provenance bounds") + namespace: dict[str, object] = { + "version": 1, + "ingested_at": ingested_at, + "last_ingested_at": ingested_at, + "timestamp_semantics": "source_scope_end", + "citations": citations, + } + _check_bounds(namespace) + return {NAMESPACE: namespace} + + +def merge_metadata(existing: object, incoming: dict[str, object]) -> tuple[dict[str, object], bool]: + if not isinstance(existing, dict): + raise ProvenanceError("existing source metadata is not an object") + new = incoming.get(NAMESPACE) + if not isinstance(new, dict) or type(new.get("version")) is not int or new.get("version") != 1: + raise ProvenanceError("incoming source provenance version is unsupported") + _check_bounds(new) + previous = existing.get(NAMESPACE) + if previous is None: + namespace = {**new, "legacy_unknown": True} + _check_bounds(namespace) + return {**existing, NAMESPACE: namespace}, True + if not isinstance(previous, dict) or type(previous.get("version")) is not int or previous.get("version") != 1: + raise ProvenanceError("existing source provenance version is unsupported") + _check_bounds(previous) + old_citations, new_citations = previous["citations"], new["citations"] + if any(not isinstance(citation, dict) or not isinstance(citation.get("id"), str) for citation in old_citations + new_citations): + raise ProvenanceError("source citation identity is invalid") + seen = {citation["id"] for citation in old_citations} + added = [citation for citation in new_citations if citation["id"] not in seen] + if not added: + return dict(existing), False + namespace = {**previous, "citations": [*old_citations, *added], "last_ingested_at": new["last_ingested_at"]} + _check_bounds(namespace) + return {**existing, NAMESPACE: namespace}, True + + +def source_intervals(metadata: object) -> tuple[list[tuple[str, str]], bool] | None: + """Individual observed scope windows and unknown coverage; None is legacy.""" + if not isinstance(metadata, dict) or NAMESPACE not in metadata: + return None + namespace = metadata[NAMESPACE] + if (not isinstance(namespace, dict) or type(namespace.get("version")) is not int + or namespace.get("version") != 1 or type(namespace.get("legacy_unknown", False)) is not bool + or namespace.get("timestamp_semantics") != "source_scope_end"): + return [], True + try: + _check_bounds(namespace) + except ProvenanceError: + return [], True + citations = namespace.get("citations") + if not isinstance(citations, list) or not citations or len(citations) > MAX_CITATIONS: + return [], True + intervals: list[tuple[str, str]] = [] + unknown = namespace.get("legacy_unknown", False) + for citation in citations: + scope = citation.get("scope") if isinstance(citation, dict) else None + value = scope.get("source_time") if isinstance(scope, dict) else None + if (not isinstance(scope, dict) or scope.get("kind") != "extraction_input_scope" + or not isinstance(value, dict) or type(value.get("unknown_count")) is not int + or value["unknown_count"] < 0): + return [], True + start, end = _known_time(value.get("start")), _known_time(value.get("end")) + status = value.get("status") + if ((status == "known" and value["unknown_count"] == 0) + or (status == "mixed" and value["unknown_count"] > 0)) and start is not None and end is not None and start <= end: + if (start, end) not in intervals: + intervals.append((start, end)) + unknown |= status != "known" + elif status == "unknown" and value["unknown_count"] > 0 and value.get("start") is None and value.get("end") is None: + unknown = True + else: + return [], True + return intervals, unknown + + +def source_timestamp(metadata: object) -> str: + coverage = source_intervals(metadata) + if coverage is None or coverage[1] or not coverage[0]: + return "" + return max(end for _, end in coverage[0]) diff --git a/truememory/ingest/transcript.py b/truememory/ingest/transcript.py index 5aee41ba..624e419b 100644 --- a/truememory/ingest/transcript.py +++ b/truememory/ingest/transcript.py @@ -14,11 +14,17 @@ from __future__ import annotations +import errno +import hashlib import json import logging +import os import re -from dataclasses import dataclass +import stat +from bisect import bisect_left +from dataclasses import dataclass, field from pathlib import Path +from collections.abc import Iterator log = logging.getLogger(__name__) @@ -32,6 +38,953 @@ class Message: tool_name: str = "" # Tool name for tool_use messages +@dataclass(frozen=True) +class FileState: + """File identity without a path or transcript content.""" + device: int + inode: int + size: int + mtime_ns: int + ctime_ns: int + + @classmethod + def from_stat(cls, value: os.stat_result) -> FileState: + return cls(value.st_dev, value.st_ino, value.st_size, value.st_mtime_ns, value.st_ctime_ns) + + +@dataclass(frozen=True) +class TranscriptFileVersion: + """Identity of the bytes read, not a later completion-time stat.""" + byte_count: int + sha256: str + before: FileState + after: FileState + path_after: FileState | None + + @property + def stable(self) -> bool: + return self.before == self.after == self.path_after and self.byte_count == self.after.size + + +@dataclass(frozen=True) +class TranscriptOutcome: + """Coverage evidence; only complete outcomes include all recognized input. + + Status is complete, partial, salvaged, malformed or unreadable. Messages + remain available for inspection even when legacy parsing salvaged input. + This API does not change the ingestion pipeline's completion policy. + """ + messages: list[Message] = field(repr=False) + status: str + total_records: int = 0 + malformed_records: int = 0 + error_categories: tuple[str, ...] = () + file_version: TranscriptFileVersion | None = None + unrecognized_records: int = 0 + + @property + def complete(self) -> bool: + return self.status == "complete" + + +# Pinned Codex rollout grammar: openai/codex 0b755b1945bf4a31560df2ce1469aeb044dccbc9. +# These are exact exclusions, never prefix rules or recursive payload discovery. +_CODEX_CONTEXT = { + "session_meta", "turn_context", "compacted", "token_usage_record", "world_state", + "retained_context", "security_risk_score", "inter_agent_communication", + "inter_agent_communication_metadata", +} +_CODEX_RESPONSE_CONTEXT = { + "additional_tools", "agent_message", "reasoning", "local_shell_call", "function_call", + "function_call_output", "custom_tool_call", "custom_tool_call_output", "tool_search_call", + "tool_search_output", "web_search_call", "image_generation_call", "compaction", + "compaction_summary", "configuration_update", "compaction_trigger", "context_compaction", +} +_CODEX_EVENT_CONTEXT = { + "token_count", "agent_reasoning", "agent_reasoning_raw_content", "context_compacted", + "exec_command_begin", "exec_command_end", "exec_command_output_delta", + "mcp_tool_call_begin", "mcp_tool_call_end", "dynamic_tool_call_request", + "dynamic_tool_call_response", "patch_apply_begin", "patch_apply_end", "patch_apply_updated", + "web_search_begin", "web_search_end", "view_image_tool_call", "image_generation_begin", + "image_generation_end", "stream_error", "stream_info", "error", "warning", +} +_CODEX_ITEM_CONTEXT = { + "FunctionCallOutput", "HookPrompt", "Reasoning", "CommandExecution", "DynamicToolCall", + "CollabAgentToolCall", "SubAgentActivity", "WebSearch", "ImageView", "ImageGeneration", + "EnteredReviewMode", "ExitedReviewMode", "FileChange", "McpToolCall", "ContextCompaction", +} +_CODEX_SCAFFOLD_KINDS = { + "agents_md.instructions", "hooks.additional_context", "guardian.retained_instructions", +} +_CODEX_PHASES = {"commentary", "partial_answer", "final_answer"} +_CODEX_START = {"task_started", "turn_started"} +_CODEX_END = {"task_complete", "turn_complete"} +_CODEX_ANY = object() +_CODEX_WHITE_SPACE = "\t\n\v\f\r \u0085\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000" +_CodexCitation = tuple[tuple[tuple[str, int, int, str], ...], tuple[str, ...]] + + +@dataclass +class _CodexRecord: + kind: str + message: Message | None = field(default=None, repr=False) + role: str = "" + projection: str | None = field(default=None, repr=False) + plan_projection: str | None = field(default=None, repr=False) + projection_rewritten: bool = field(default=False, repr=False) + memory_citation: _CodexCitation | None = field(default=None, repr=False) + plan_body: str | None = field(default=None, repr=False) + plan_event: bool = field(default=False, repr=False) + frozen_plan_mode: bool = field(default=False, repr=False) + turn_id: str | None = field(default=None, repr=False) + item_id: str | None = field(default=None, repr=False) + phase: str | None = None + payload: dict = field(default_factory=dict, repr=False) + malformed: bool = False + unrecognized: bool = False + errors: set[str] = field(default_factory=set) + + def reject(self, category: str, *, malformed: bool = False) -> None: + self.errors.add(category) + if malformed: + self.malformed = True + else: + self.unrecognized = True + + +def _codex_attribution(value: dict) -> tuple[str, bool]: + """Return absent/user/excluded/unknown, validating even legacy selectors.""" + for key, expected in (("harness_injected", bool), ("source_tool_namespace", str)): + if key in value and value[key] is not None and type(value[key]) is not expected: + return "unknown", True + if "provenance" in value: + provenance = value["provenance"] + if not isinstance(provenance, dict) or not isinstance(provenance.get("type"), str): + return "unknown", True + kind = provenance["type"] + if kind == "developer_instructions": + if type(provenance.get("from_additional_requirements")) is not bool: + return "unknown", True + if kind == "harness" and type(provenance.get("hook", False)) is not bool: + return "unknown", True + if kind == "tool" and provenance.get("namespace") is not None: + if not isinstance(provenance["namespace"], str): + return "unknown", True + if kind == "user": + return "user", False + if kind in {"developer_instructions", "skills", "agents_md", "harness", "tool"}: + return "excluded", False + return "unknown", False + if value.get("source_tool_namespace") is not None or value.get("harness_injected") is True: + return "excluded", False + # Empty historical metadata and harness_injected:false are not positive + # provenance. They use only the documented older role/text compatibility. + return "absent", False + + +def _codex_optional_string(record: _CodexRecord, payload: dict, key: str) -> str | None: + value = payload.get(key) + if value is not None and not isinstance(value, str): + record.reject("codex_invalid_metadata", malformed=True) + return None + return value or None + + +def _codex_strip_citations(text: str) -> tuple[str, list[str]]: + """Pinned literal, nonnested citation projection, including open-at-EOF.""" + opening, closing = "", "" + visible: list[str] = [] + bodies: list[str] = [] + offset = 0 + while True: + start = text.find(opening, offset) + if start < 0: + visible.append(text[offset:]) + break + visible.append(text[offset:start]) + body_start = start + len(opening) + end = text.find(closing, body_start) + if end < 0: + bodies.append(text[body_start:]) + break + bodies.append(text[body_start:end]) + offset = end + len(closing) + return "".join(visible), bodies + + +def _codex_plan_parts(text: str) -> tuple[str, str | None, bool]: + """Pinned whole-LF-line tags; all surviving characters remain exact.""" + visible: list[str] = [] + active = False + saw_delimiter = False + plan_body: list[str] | None = None + lines = text.split("\n") + for index, body in enumerate(lines): + line = body + ("\n" if index + 1 < len(lines) else "") + slug = body.strip(_CODEX_WHITE_SPACE) + if slug in ("", ""): + saw_delimiter = True + if slug == "" and not active: + active = True + plan_body = [] + elif slug == "" and active: + active = False + elif not active: + visible.append(line) + elif plan_body is not None: + plan_body.append(line) + return "".join(visible), None if plan_body is None else "".join(plan_body), saw_delimiter + + +def _codex_strip_plan(text: str) -> tuple[str, bool]: + visible, _, saw_delimiter = _codex_plan_parts(text) + return visible, saw_delimiter + + +def _codex_rust_lines(text: str) -> Iterator[str]: + """Rust lines split only at LF, stripping its preceding CR.""" + lines = text.split("\n") + for index, line in enumerate(lines): + if index + 1 == len(lines) and not line: + continue + yield line[:-1] if index + 1 < len(lines) and line.endswith("\r") else line + + +def _codex_u32(text: str) -> int | None: + text = text.strip(_CODEX_WHITE_SPACE) + if text.startswith("+"): + text = text[1:] + if not text or any(character not in "0123456789" for character in text): + return None + digits = text.lstrip("0") or "0" + if len(digits) > 10 or (len(digits) == 10 and digits > "4294967295"): + return None + return int(digits) + + +def _codex_citation_body_block(text: str, opening: str, closing: str) -> str | None: + start = text.find(opening) + if start < 0: + return None + start += len(opening) + end = text.find(closing, start) + return None if end < 0 else text[start:end] + + +def _codex_derive_citation(bodies: list[str]) -> _CodexCitation | None: + entries: list[tuple[str, int, int, str]] = [] + rollout_ids: list[str] = [] + seen: set[str] = set() + for body in bodies: + block = _codex_citation_body_block(body, "", "") + if block is not None: + for line in _codex_rust_lines(block): + line = line.strip(_CODEX_WHITE_SPACE) + if "|note=[" not in line or not line.endswith("]"): + continue + location, note = line.rsplit("|note=[", 1) + if ":" not in location: + continue + path, span = location.rsplit(":", 1) + if "-" not in span: + continue + start, end = span.split("-", 1) + first, last = _codex_u32(start), _codex_u32(end) + if first is not None and last is not None: + entries.append((path.strip(_CODEX_WHITE_SPACE), first, last, + note[:-1].strip(_CODEX_WHITE_SPACE))) + ids = _codex_citation_body_block(body, "", "") + if ids is None: + ids = _codex_citation_body_block(body, "", "") + if ids is not None: + for line in _codex_rust_lines(ids): + value = line.strip(_CODEX_WHITE_SPACE) + if value and value not in seen: + seen.add(value) + rollout_ids.append(value) + return (tuple(entries), tuple(rollout_ids)) if entries or rollout_ids else None + + +def _codex_citation_object(value: object) -> _CodexCitation | None: + if not isinstance(value, dict) or set(value) != {"entries", "rolloutIds"}: + return None + entries, ids = value["entries"], value["rolloutIds"] + if not isinstance(entries, list) or not isinstance(ids, list) or any(not isinstance(item, str) for item in ids): + return None + result: list[tuple[str, int, int, str]] = [] + for item in entries: + if not isinstance(item, dict) or set(item) != {"path", "lineStart", "lineEnd", "note"}: + return None + if not isinstance(item["path"], str) or not isinstance(item["note"], str): + return None + if any(type(item[key]) is not int or not 0 <= item[key] <= 4294967295 + for key in ("lineStart", "lineEnd")): + return None + result.append((item["path"], item["lineStart"], item["lineEnd"], item["note"])) + return tuple(result), tuple(ids) + + +def _codex_decode_citation(record: _CodexRecord, value: object) -> None: + if value is not None: + record.memory_citation = _codex_citation_object(value) + if record.memory_citation is None: + record.reject("codex_unsupported_projection") + + +def _codex_context_mode(payload: dict) -> str | None: + mode = payload.get("collaboration_mode") + if not isinstance(mode, dict) or set(mode) != {"mode", "settings"}: + return None + if mode["mode"] not in ("plan", "default"): + return None + settings = mode["settings"] + if not isinstance(settings, dict) or set(settings) - {"model", "reasoning_effort", "developer_instructions"}: + return None + if not isinstance(settings.get("model"), str): + return None + effort, instructions = settings.get("reasoning_effort"), settings.get("developer_instructions") + if effort is not None and (not isinstance(effort, str) or not effort): + return None + if instructions is not None and not isinstance(instructions, str): + return None + return mode["mode"] + + +def _decode_codex_record(entry: dict) -> _CodexRecord | None: + """Decode exact native envelopes without interpreting arbitrary nested prose.""" + kind = entry.get("type") + native = kind in _CODEX_CONTEXT if isinstance(kind, str) else False + native |= kind in ("response_item", "event_msg", "realtime_item") + if not native: + generic_kind = kind or entry.get("role") + if ("payload" not in entry + or (isinstance(generic_kind, str) and generic_kind in _NON_CONVERSATION_TYPES) + or generic_kind in ("user", "human", "assistant", "tool_use", "tool_result", "system") + or (not kind and "role" in entry and "content" in entry)): + return None + record = _CodexRecord(kind=kind if isinstance(kind, str) else "unknown") + if not isinstance(kind, str): + record.reject("codex_invalid_record", malformed=True) + return record + if not isinstance(entry.get("timestamp", ""), str): + record.reject("codex_invalid_metadata", malformed=True) + payload = entry.get("payload") + if not isinstance(payload, dict): + record.reject("codex_invalid_record", malformed=True) + return record + record.payload = payload + if kind in _CODEX_CONTEXT: + return record + if kind == "event_msg": + return _decode_codex_event(record) + if kind != "response_item": + record.reject("codex_unrecognized_record") + return record + variant = payload.get("type") + if not isinstance(variant, str): + record.reject("codex_invalid_record", malformed=True) + return record + if variant in _CODEX_RESPONSE_CONTEXT: + return record + if variant != "message": + record.reject("codex_unrecognized_record") + return record + role, content = payload.get("role"), payload.get("content") + if not isinstance(role, str) or not isinstance(content, list): + record.reject("codex_invalid_record", malformed=True) + return record + if role in ("system", "developer", "tool"): + return record + if role not in ("user", "assistant"): + record.reject("codex_unrecognized_record") + return record + record.role = "human" if role == "user" else "assistant" + outer, metadata = entry.get("metadata"), payload.get("internal_chat_message_metadata_passthrough") + if any(value is not None and not isinstance(value, dict) for value in (outer, metadata)): + record.reject("codex_invalid_metadata", malformed=True) + return record + outer, metadata = outer or {}, metadata or {} + excluded = False + for key in ("compaction_output", "inherited_user_message"): + if key in outer: + if type(outer[key]) is not bool: + record.reject("codex_invalid_metadata", malformed=True) + return record + excluded |= outer[key] + if "delivered_assistant_message" in outer: + marker = outer["delivered_assistant_message"] + if not isinstance(marker, str): + record.reject("codex_invalid_metadata", malformed=True) + elif marker.startswith("codex:code-mode-delivery:v1:incomplete:"): + record.reject("codex_delivery_unavailable") + elif marker != "codex:code-mode-delivery:v1:complete": + record.reject("codex_unrecognized_record") + return record + if excluded: + return record + record.turn_id = _codex_optional_string(record, metadata, "turn_id") + record.item_id = _codex_optional_string(record, payload, "id") + record.phase = _codex_optional_string(record, payload, "phase") + if record.phase is not None and record.phase not in _CODEX_PHASES: + record.reject("codex_unrecognized_record") + annotations, kinds = metadata.get("content_item_metadata"), metadata.get("content_item_kinds") + for values, value_type in ((annotations, dict), (kinds, str)): + if values is not None and (not isinstance(values, list) or len(values) != len(content) + or any(not isinstance(value, value_type) for value in values)): + record.reject("codex_invalid_metadata", malformed=True) + return record + parts: list[str] = [] + output_parts: list[str] = [] + projection_allowed = True + for index, block in enumerate(content): + attribution, bad = _codex_attribution(annotations[index]) if annotations is not None else ("absent", False) + block_kind = kinds[index] if kinds is not None else "" + if bad: + record.reject("codex_invalid_metadata", malformed=True) + projection_allowed = False + continue + if attribution == "excluded": + # Proven attribution already covers generated context, including + # new fragment kinds. Kind fallback cannot re-authorize it. + projection_allowed = False + continue + if block_kind in _CODEX_SCAFFOLD_KINDS: + if attribution == "user": + record.reject("codex_unrecognized_provenance") + projection_allowed = False + continue + if block_kind not in ("", "unknown"): + record.reject("codex_unrecognized_provenance") + projection_allowed = False + continue + if attribution == "unknown" or (attribution == "user" and role != "user"): + record.reject("codex_unrecognized_provenance") + projection_allowed = False + continue + if not isinstance(block, dict) or not isinstance(block.get("type"), str): + record.reject("codex_invalid_content", malformed=True) + continue + if block["type"] not in ("input_text", "output_text"): + record.reject("codex_unrecognized_content") + continue + if not isinstance(block.get("text"), str): + record.reject("codex_invalid_content", malformed=True) + continue + parts.append(block["text"]) + if block["type"] == "output_text": + output_parts.append(block["text"]) + if role == "user" and block["type"] != "input_text": + projection_allowed = False + public_text = "\n".join(part for part in parts if part).strip() + if public_text: + timestamp = entry.get("timestamp", "") + record.message = Message(record.role, public_text, timestamp if isinstance(timestamp, str) else "") + if projection_allowed and not record.malformed and not record.unrecognized: + raw = "".join(parts) + record.projection = raw + if role == "assistant": + record.projection, citations = _codex_strip_citations(raw) + record.projection_rewritten = bool(citations) + record.memory_citation = _codex_derive_citation(citations) + output = "".join(output_parts) + if "" in output: + _, plan_body, _ = _codex_plan_parts(output) + if plan_body is not None: + record.plan_body, _ = _codex_strip_citations(plan_body) + if any(tag in record.projection for tag in ("", "")): + plan_projection, saw_delimiter = _codex_strip_plan(record.projection) + if saw_delimiter: + # A private candidate awaits positive same-turn mode. + record.plan_projection = plan_projection + return record + + +def _decode_codex_event(record: _CodexRecord) -> _CodexRecord: + payload = record.payload + kind = payload.get("type") + if not isinstance(kind, str): + record.reject("codex_invalid_record", malformed=True) + return record + record.kind = kind + if kind in _CODEX_START | _CODEX_END | {"turn_aborted"}: + record.turn_id = _codex_optional_string(record, payload, "turn_id") + if record.turn_id is None: + record.reject("codex_unsupported_lifecycle") + if kind in _CODEX_END: + summary = payload.get("last_agent_message") + if summary is not None: + if not isinstance(summary, str) or not summary.strip(_CODEX_WHITE_SPACE): + record.reject("codex_unmatched_terminal") + else: + record.projection = summary + return record + if kind in _CODEX_EVENT_CONTEXT: + return record + if kind in ("item_started", "item_completed"): + item = payload.get("item") + if not isinstance(item, dict) or not isinstance(item.get("type"), str): + record.reject("codex_invalid_record", malformed=True) + return record + item_kind = item["type"] + if item_kind == "Plan": + record.plan_event = True + record.turn_id = _codex_optional_string(record, payload, "turn_id") + record.item_id = _codex_optional_string(record, item, "id") + record.projection = item.get("text") if isinstance(item.get("text"), str) else None + permitted = {"type", "thread_id", "turn_id", "item", "started_at_ms", "completed_at_ms"} + if (set(item) != {"type", "id", "text"} + or set(payload) - permitted or record.turn_id is None + or record.item_id != record.turn_id + "-plan" or record.projection is None + or not isinstance(payload.get("thread_id"), str) or not payload["thread_id"]): + record.reject("codex_unsupported_projection") + for key in ("started_at_ms", "completed_at_ms"): + if key in payload and not (key == "started_at_ms" and payload[key] is None): + value = payload[key] + if type(value) is not int or not -(2**63) <= value < 2**63: + record.reject("codex_unsupported_projection") + if kind == "item_started" and (record.projection != "" or "completed_at_ms" in payload + or type(payload.get("started_at_ms")) is not int): + record.reject("codex_unsupported_projection") + return record + if item_kind in _CODEX_ITEM_CONTEXT: + return record + if item_kind not in ("UserMessage", "AgentMessage"): + record.reject("codex_unrecognized_record") + return record + if kind == "item_started": + return record + record.turn_id = _codex_optional_string(record, payload, "turn_id") + record.item_id = _codex_optional_string(record, item, "id") + if record.turn_id is None or record.item_id is None: + record.reject("codex_unmatched_conversation_event") + record.role = "human" if item_kind == "UserMessage" else "assistant" + record.phase = _codex_optional_string(record, item, "phase") + content = item.get("content") + expected_type = "text" if record.role == "human" else "Text" + if not isinstance(content, list) or any( + not isinstance(block, dict) or block.get("type") != expected_type + or not isinstance(block.get("text"), str) for block in content + ): + record.reject("codex_unmatched_conversation_event") + elif record.role == "assistant" and len(content) != 1: + record.reject("codex_unsupported_projection") + else: + record.projection = "".join(block["text"] for block in content) + if any(item.get(key) not in (None, []) for key in ("delivery", "questions")): + record.reject("codex_unsupported_projection") + if record.role == "assistant": + _codex_decode_citation(record, item.get("memory_citation")) + elif item.get("memory_citation") not in (None, []): + record.reject("codex_unsupported_projection") + return record + if kind not in ("user_message", "agent_message"): + record.reject("codex_unrecognized_record") + return record + record.role = "human" if kind == "user_message" else "assistant" + record.phase = _codex_optional_string(record, payload, "phase") + if not isinstance(payload.get("message"), str): + record.reject("codex_unmatched_conversation_event") + else: + record.projection = payload["message"] + # These native event fields carry additional conversation unavailable to + # the plain-text decoder. Empty producer defaults do not add evidence. + media = ("images", "image_details", "file_ids", "file_id_details", "image_order", "local_images", + "local_image_details", "audio", "local_audio", "delivery", "questions") + if any(payload.get(key) not in (None, []) for key in media): + record.reject("codex_unmatched_conversation_event") + if record.role == "assistant": + _codex_decode_citation(record, payload.get("memory_citation")) + elif payload.get("memory_citation") not in (None, []): + record.reject("codex_unmatched_conversation_event") + return record + + +def _codex_echo_index(credits: list[tuple[int, _CodexRecord]], turn_id: str | None) -> dict[tuple, list[int]]: + """Eight ordinary entries; citation evidence adds eight exact selectors.""" + index: dict[tuple, list[int]] = {} + for position, (_, candidate) in enumerate(credits): + projections: tuple = (candidate.projection,) + if candidate.memory_citation is not None: + projections += ((candidate.projection, candidate.memory_citation),) + for projection in projections: + for phase in (candidate.phase, _CODEX_ANY): + for turn in (turn_id or candidate.turn_id, _CODEX_ANY): + for item in (candidate.item_id, _CODEX_ANY): + key = (candidate.role, projection, phase, turn, item) + index.setdefault(key, []).append(position) + return index + + +def _codex_next_echo(index: dict[tuple, list[int]], event: _CodexRecord, cursor: int) -> int | None: + """Find the next compatible occurrence with at most two binary searches.""" + projection: str | tuple | None = event.projection + if event.memory_citation is not None: + projection = (event.projection, event.memory_citation) + prefix = (event.role, projection, event.phase if event.phase is not None else _CODEX_ANY, + event.turn_id if event.turn_id is not None else _CODEX_ANY) + # Missing canonical IDs are compatible with a supplied event ID. An event + # without an ID queries all occurrences, not only those missing an ID. + item_keys = (_CODEX_ANY,) if event.item_id is None else (event.item_id, None) + matches: list[int] = [] + for item in item_keys: + positions = index.get((*prefix, item), ()) + offset = bisect_left(positions, cursor) + if offset < len(positions): + matches.append(positions[offset]) + return min(matches) if matches else None + + +def _reconcile_codex(records: list[tuple[int, _CodexRecord]]) -> None: + """Reconcile supplied-byte evidence; events never create canonical turns.""" + scopes: dict[int, list[tuple[int, _CodexRecord]]] = {} + scope_ids: dict[int, str | None] = {} + scope_modes: dict[int, str | None] = {} + explicit_modes: set[int] = set() + rewritten_scopes: set[int] = set() + invalid_scopes: set[int] = set() + terminals: list[tuple[int, int, _CodexRecord]] = [] + scope = 0 + active = False + closed_ids: set[str] = set() + for index, record in records: + if record.kind == "session_meta": + scope += 1 + active = False + closed_ids.clear() + continue + if record.kind in _CODEX_START: + overlapping = active + if overlapping: + invalid_scopes.add(scope) + record.reject("codex_unsupported_lifecycle") + scope += 1 + active = True + scope_ids[scope] = record.turn_id + mode = record.payload.get("collaboration_mode_kind") + scope_modes[scope] = mode if mode in ("plan", "default") else None + if "collaboration_mode_kind" in record.payload: + explicit_modes.add(scope) + if overlapping or record.turn_id is None or record.malformed: + invalid_scopes.add(scope) + scopes.setdefault(scope, []) + continue + if record.kind in _CODEX_END | {"turn_aborted"}: + if active: + if record.turn_id != scope_ids.get(scope): + invalid_scopes.add(scope) + record.reject("codex_unsupported_lifecycle") + terminals.append((index, scope, record)) + active = False + elif record.turn_id in closed_ids or record.turn_id is None: + record.reject("codex_unsupported_lifecycle") + else: + # No start in this supplied segment: only explicit response + # turn metadata can explain a nonnull terminal summary. + terminals.append((index, scope, record)) + if record.turn_id is not None: + closed_ids.add(record.turn_id) + scope += 1 + continue + if active and record.turn_id is not None and record.turn_id != scope_ids.get(scope): + record.reject("codex_unsupported_lifecycle") + scopes.setdefault(scope, []).append((index, record)) + + for group, items in scopes.items(): + mode = scope_modes.get(group) + conflicting_mode = False + mode_conflicts: list[_CodexRecord] = [] + plan_records: list[_CodexRecord] = [] + for _, record in items: + if record.kind == "turn_context" and record.payload.get("collaboration_mode") is not None: + if group not in scope_ids or record.payload.get("turn_id") != scope_ids.get(group): + continue + context_mode = _codex_context_mode(record.payload) + if context_mode is None: + mode_conflicts.append(record) + conflicting_mode = True + elif mode is not None and context_mode != mode: + mode_conflicts.append(record) + conflicting_mode = True + elif group not in explicit_modes: + mode = context_mode + if record.kind == "response_item" and record.plan_projection is not None: + plan_records.append(record) + if mode == "plan" and not conflicting_mode and group not in invalid_scopes: + record.projection = record.plan_projection + record.projection_rewritten = True + else: + record.projection = None + record.reject("codex_unsupported_projection") + record.frozen_plan_mode = mode == "plan" and not conflicting_mode and group not in invalid_scopes + if conflicting_mode: + for _, record in items: + record.frozen_plan_mode = False + if plan_records or any(record.plan_event for _, record in items): + for record in mode_conflicts: + record.reject("codex_unsupported_projection") + for record in plan_records: + record.projection = None + record.reject("codex_unsupported_projection") + if any(record.projection_rewritten for _, record in items): + rewritten_scopes.add(group) + if group in invalid_scopes: + for _, record in items: + if record.role: + record.reject("codex_unsupported_lifecycle") + credits = [(index, record) for index, record in items if record.kind == "response_item" + and record.projection is not None and not record.malformed and not record.unrecognized] + credit_index = _codex_echo_index(credits, scope_ids.get(group)) + plan_index: dict[tuple, list[int]] = {} + for position, (_, candidate) in enumerate(items): + if (candidate.kind == "response_item" and candidate.role == "assistant" + and candidate.frozen_plan_mode and candidate.plan_body is not None + and not candidate.malformed and not candidate.unrecognized): + key = (scope_ids.get(group), candidate.plan_body) + plan_index.setdefault(key, []).append(position) + plan_cursor = 0 + plan_started = False + plan_completed = False + for _, plan in items: + if not plan.plan_event or plan.malformed or plan.unrecognized: + continue + if plan.kind == "item_started": + if (plan_started or plan_completed or not plan.frozen_plan_mode or group not in scope_ids + or not plan_index or plan.turn_id != scope_ids.get(group)): + plan.reject("codex_unsupported_projection") + else: + plan_started = True + continue + positions = plan_index.get((plan.turn_id, plan.projection), ()) + offset = bisect_left(positions, plan_cursor) + if not plan.frozen_plan_mode or group not in scope_ids or offset == len(positions): + plan.reject("codex_unsupported_projection") + else: + plan_cursor = positions[offset] + 1 + plan_completed = True + cursors: dict[str, int] = {} + for _, event in items: + if event.kind not in ("user_message", "agent_message", "item_completed") or not event.role: + continue + if event.malformed or event.unrecognized: + continue + position = _codex_next_echo(credit_index, event, cursors.get(event.role, 0)) + if position is None: + event.reject("codex_unmatched_conversation_event") + if group in rewritten_scopes or event.memory_citation is not None: + event.reject("codex_unsupported_projection") + else: + cursors[event.role] = position + 1 + for index, group, terminal in terminals: + if terminal.projection is None or terminal.malformed or terminal.unrecognized: + continue + # Terminal evidence is independent of echo credit consumption. Only + # earlier canonical occurrences in this lifecycle may explain it. + candidates = scopes.get(group, []) + explicit_ids = {candidate.turn_id for before, candidate in candidates + if before < index and candidate.kind == "response_item" and candidate.role + and candidate.turn_id is not None} + ambiguous = group not in scope_ids and explicit_ids != {terminal.turn_id} + if ambiguous or group in invalid_scopes or not any( + before < index and candidate.kind == "response_item" and candidate.role == "assistant" + and candidate.projection == terminal.projection and not candidate.malformed and not candidate.unrecognized + and terminal.turn_id == (scope_ids.get(group) or candidate.turn_id) + for before, candidate in reversed(candidates) + ): + terminal.reject("codex_unmatched_terminal") + if group in rewritten_scopes: + terminal.reject("codex_unsupported_projection") + + +def _transcript_path(source: str | Path) -> Path | None: + if isinstance(source, Path): + return source + try: + candidate = Path(source) + # exists()/is_file() suppress access errors on Python 3.14. A single + # stat keeps unreadable sources distinct from ordinary inline text. + if stat.S_ISREG(candidate.stat().st_mode): + return candidate + except ValueError: + pass + except OSError as error: + if (error.errno not in (errno.ENOENT, errno.ENOTDIR, errno.ENAMETOOLONG) + and getattr(error, "winerror", None) != 123): + raise + return None + + +def parse_transcript_outcome(source: str | Path) -> TranscriptOutcome: + """Parse with source coverage, retaining the legacy API separately. + + Explicit Paths are always files, including missing paths. String path + detection follows the existing parser; other strings are inline content. + Diagnostics contain categories and counts only. Stable means these + observations agreed; it does not lock the source against later changes. + """ + try: + path = _transcript_path(source) + except OSError: + return TranscriptOutcome([], "unreadable", error_categories=("source_unreadable",)) + + version = None + errors: list[str] = [] + if path is not None: + try: + with path.open("rb") as handle: + before = FileState.from_stat(os.fstat(handle.fileno())) + data = handle.read() + after = FileState.from_stat(os.fstat(handle.fileno())) + except (OSError, ValueError): + return TranscriptOutcome([], "unreadable", error_categories=("source_unreadable",)) + try: + # Windows stat() and fstat() can give ctime different meanings. + # Reopen the pathname to check identity using the same API. + with path.open("rb") as identity_handle: + path_after = FileState.from_stat(os.fstat(identity_handle.fileno())) + except OSError: + path_after = None + version = TranscriptFileVersion(len(data), hashlib.sha256(data).hexdigest(), before, after, path_after) + if not version.stable: + errors.append("source_changed_during_read") + try: + text = data.decode("utf-8") + except UnicodeDecodeError: + text = data.decode("utf-8", errors="replace") + errors.append("invalid_utf8") + else: + text = source + + parsed = _parse_transcript_text_outcome(text.strip(), jsonl=path is not None and path.suffix.lower() == ".jsonl") + errors.extend(parsed.error_categories) + status = parsed.status + if errors and status == "complete": + status = "partial" if parsed.messages else "malformed" + return TranscriptOutcome(parsed.messages, status, parsed.total_records, parsed.malformed_records, + tuple(errors), version, parsed.unrecognized_records) + + +def _covered_content_blocks(blocks: list, *, nested: bool = False) -> tuple[list, bool, bool]: + """Retain supported text without certifying silently omitted payloads.""" + covered: list = [] + malformed = unrecognized = False + for block in blocks: + if isinstance(block, str): + covered.append(block) + continue + if not isinstance(block, dict) or not isinstance(block.get("type"), str): + malformed = True + continue + kind = block["type"] + if kind == "thinking": + # Deliberately excluded private reasoning is not lost conversation. + covered.append(block) + elif kind == "text": + if isinstance(block.get("text"), str): + covered.append(block) + else: + malformed = True + elif kind == "tool_use" and not nested: + if isinstance(block.get("name", ""), str): + covered.append(block) + else: + malformed = True + elif kind == "tool_result" and not nested: + content = block.get("content") + if isinstance(content, list): + children, bad, unknown = _covered_content_blocks(content, nested=True) + malformed |= bad + unrecognized |= unknown + covered.append({**block, "content": children}) + elif content is None or isinstance(content, str): + covered.append(block) + else: + malformed = True + else: + # Images/audio and unknown block types need an explicit future + # decoder. Their text-looking keys are not a supported transcript. + unrecognized = True + return covered, malformed, unrecognized + + +def _parse_transcript_text_outcome(text: str, *, jsonl: bool = False) -> TranscriptOutcome: + if not text: + return TranscriptOutcome([], "complete") + if not jsonl and not text.startswith(("[", "{")): + messages = _parse_plain_text(text) + return TranscriptOutcome(messages, "complete", len(messages)) + if text.startswith("["): + try: + entries = json.loads(text) + except json.JSONDecodeError: + messages = _parse_plain_text(text) + return TranscriptOutcome(messages, "salvaged", 1, 1, ("invalid_json_array",)) + allow_strings = True + else: + entries = [] + allow_strings = False + for line in text.splitlines(): + if not line.strip(): + continue + try: + entries.append(json.loads(line)) + except json.JSONDecodeError: + entries.append(None) + # None also represents a syntactically valid but unusable JSONL null; + # both count as one malformed record in the loop below. + + messages: list[Message] = [] + malformed = unrecognized = 0 + native_records: list[tuple[int, _CodexRecord]] = [] + for index, entry in enumerate(entries): + if isinstance(entry, str) and allow_strings: + messages.append(Message(role="unknown", content=entry)) + continue + if not isinstance(entry, dict): + malformed += 1 + continue + native = _decode_codex_record(entry) + if native is not None: + native_records.append((index, native)) + if native.message is not None: + messages.append(native.message) + continue + top_type = entry.get("type") or entry.get("role") or "unknown" + if not isinstance(top_type, str): + malformed += 1 + continue + if isinstance(top_type, str) and top_type in _NON_CONVERSATION_TYPES: + continue + # The supported older tool-call format has name/input instead of + # content. Keep its existing _extract_message behavior intact. + if top_type != "tool_use": + inner = entry.get("message") + payload = inner if isinstance(inner, dict) else entry + if "content" not in payload or not isinstance(payload["content"], (str, list)): + malformed += 1 + continue + if isinstance(payload["content"], list): + blocks, bad, unknown = _covered_content_blocks(payload["content"]) + malformed += int(bad) + unrecognized += int(unknown) + payload = {**payload, "content": blocks} + entry = {**entry, "message": payload} if isinstance(inner, dict) else payload + try: + message = _extract_message(entry) + except (TypeError, AttributeError, ValueError): + malformed += 1 + continue + if message is not None: + messages.append(message) + _reconcile_codex(native_records) + malformed += sum(record.malformed for _, record in native_records) + unrecognized += sum(record.unrecognized for _, record in native_records) + status = ("partial" if messages else "malformed") if malformed else "partial" if unrecognized else "complete" + errors = (("malformed_records",) if malformed else ()) + (("unrecognized_content",) if unrecognized else ()) + errors += tuple(sorted({category for _, record in native_records for category in record.errors})) + return TranscriptOutcome(messages, status, len(entries), malformed, errors, + unrecognized_records=unrecognized) + + def parse_transcript(source: str | Path) -> list[Message]: """ Parse a transcript file or string into structured messages. @@ -45,7 +998,7 @@ def parse_transcript(source: str | Path) -> list[Message]: This avoids the fragile "len < 500 = path" heuristic from earlier versions, which mis-classified long absolute paths as content. - Once we commit to path mode (``candidate.exists() and is_file()``), + Once we commit to path mode (a stat identifies a regular file), any read failure (``PermissionError``, ``OSError``) is treated as a real error and returns an empty list — **never** silently fall back to interpreting the path string itself as content. Silently re-parsing @@ -55,36 +1008,22 @@ def parse_transcript(source: str | Path) -> list[Message]: """ text: str - if isinstance(source, Path): - # Explicit Path — always a file + try: + path = _transcript_path(source) + except OSError: + log.error("Cannot read transcript", extra={"error_category": "source_unreadable"}) + return [] + + if path is not None: try: - text = source.read_text(encoding="utf-8", errors="replace") + text = path.read_text(encoding="utf-8", errors="replace") except FileNotFoundError: return [] - except (PermissionError, OSError) as e: - log.error("Cannot read transcript file %s: %s", source, e) + except (OSError, ValueError): + log.error("Cannot read transcript", extra={"error_category": "source_unreadable"}) return [] else: - # String: figure out whether it's a path or inline content. - # Interpreting a string as a Path can itself raise (embedded nulls, - # ValueError on some platforms) — that's the only case where we - # fall back to treating the string as content. - candidate: Path | None = None - try: - candidate = Path(source) - except (OSError, ValueError): - candidate = None - - if candidate is not None and candidate.exists() and candidate.is_file(): - # Commit to path mode. A read failure here is a real error — - # do NOT silently reinterpret the path string as content. - try: - text = candidate.read_text(encoding="utf-8", errors="replace") - except (PermissionError, OSError) as e: - log.error("Cannot read transcript file %s: %s", candidate, e) - return [] - else: - text = source + text = source text = text.strip() if not text: @@ -180,6 +1119,10 @@ def _extract_message(entry: dict) -> Message | None: Non-conversation entries like ``file-history-snapshot`` and ``progress`` are filtered out. """ + native = _decode_codex_record(entry) + if native is not None: + return native.message + # Handle different field names across formats top_type = entry.get("type") or entry.get("role") or "unknown" diff --git a/truememory/maintenance.py b/truememory/maintenance.py new file mode 100644 index 00000000..425da96d --- /dev/null +++ b/truememory/maintenance.py @@ -0,0 +1,2712 @@ +"""Database maintenance ownership, source revisions and durable layer scheduling. + +Importing this module loads no models and starts no worker or polling loop. +""" + +import hashlib +import importlib +import importlib.metadata +import importlib.util +import json +import logging +import os +import re +import sqlite3 +import sys +import threading +import time +import uuid +import weakref +from collections.abc import Callable, Iterator +from contextlib import AbstractContextManager, ExitStack, contextmanager, nullcontext +from pathlib import Path +from types import ModuleType +from typing import NamedTuple + +from truememory._platform import try_file_lock +from truememory.storage import ( + DEFAULT_BUSY_TIMEOUT_MS, DatabaseOpenError, _MAINTENANCE_SOURCE_FIELDS, + _STYLE_OUTPUT_INVALIDATE_SQL, _STYLE_OUTPUT_TRIGGERS, + _maintenance_trigger_definitions, _messages_have_rowid_id, + _prepare_style_maintenance, _style_output_tracking_ready, create_db, +) + + +logger = logging.getLogger(__name__) + + +class MaintenanceUnavailableError(RuntimeError): + """Source tracking is unavailable; a freshness token cannot be issued.""" + + +class MaintenanceBusyError(RuntimeError): + """Another operation owns maintenance for this database.""" + + +class LayerUnavailableError(MaintenanceUnavailableError): + """A builder could not use a dependency advertised by its cheap probe.""" + + +class LayerDeferredError(RuntimeError): + """A narrowly categorized transient condition establishes no attempt baseline.""" + + def __init__(self, message: str, *, category: str = "ModelBusy") -> None: + if category not in {"ModelBusy", "StyleCancelled", "StyleSourceChanged", "StyleWriterBusy", "StyleTriggersUnsupported"}: + raise ValueError("Unsupported layer deferral category") + self.category = category + super().__init__(message) + + +class StyleAccumulatorUnsupported(LayerUnavailableError): + """Tracked publication cannot replace a newer accumulator format.""" + + +class _StyleSQLInterrupted(LayerDeferredError): + """Only the owned progress callback can attest SQLite's automatic rollback.""" + + def __init__(self, *, automatic_rollback: bool) -> None: + super().__init__("Style work cancelled", category="StyleCancelled") + self.automatic_rollback = automatic_rollback + + +def _style_sqlite_deferral(error: sqlite3.OperationalError, *, cancelled: bool = False) -> str | None: + # SQLite's primary result codes are stable; Python 3.10 exposes neither + # these named constants nor sqlite_errorcode on native exceptions. + code = getattr(error, "sqlite_errorcode", None) + if isinstance(code, int): + primary = code & 255 + if primary in {5, 6}: # SQLITE_BUSY / SQLITE_LOCKED, including extended codes. + return "StyleWriterBusy" + if primary == 9 and cancelled: # SQLITE_INTERRUPT. + return "StyleCancelled" + return None + message = str(error) + if cancelled and message == "interrupted": + return "StyleCancelled" + if (message in {"database is locked", "database table is locked", "database schema is locked"} + or message.startswith(("database table is locked: ", "database schema is locked: "))): + return "StyleWriterBusy" + return None + + +class MaintenanceOwnership(NamedTuple): + path: Path | None + process_id: int + generation: str + + +class SourceRevision(NamedTuple): + epoch: str + revision: int + insert_count: int + correction_count: int + max_seen_message_id: int + nonappend_revision: int + + def is_append_only_since(self, previous: "SourceRevision") -> bool: + """Certify stable IDs at/below the earlier high watermark, not time order.""" + return ( + self.epoch == previous.epoch + and self.revision >= previous.revision + and self.insert_count >= previous.insert_count + and self.correction_count == previous.correction_count + and self.max_seen_message_id >= previous.max_seen_message_id + and self.nonappend_revision <= previous.revision + ) + + +def read_source_revision(conn: sqlite3.Connection) -> SourceRevision: + """Read in the caller's snapshot without starting or committing a transaction.""" + try: + row = conn.execute( + "SELECT epoch, revision, insert_count, correction_count, max_seen_message_id, " + "nonappend_revision, tracking_ready FROM maintenance_source_state WHERE singleton = 1" + ).fetchone() + except sqlite3.OperationalError as error: + if str(error) == "no such table: maintenance_source_state": + raise MaintenanceUnavailableError("Committed source revision tracking is unavailable") from error + raise + if row is None or not row[6]: + raise MaintenanceUnavailableError("Committed source revision tracking is unavailable") + return SourceRevision(*row[:6]) + + +def canonical_database_path(db_path: str | os.PathLike[str]) -> Path | None: + """A private in-memory database cannot be reopened by a worker connection.""" + if os.fspath(db_path) == ":memory:": + return None + return Path(db_path).expanduser().resolve() + + +def connection_database_path(conn: sqlite3.Connection) -> Path | None: + """Identify the main database without changing connection/transaction state.""" + for _, name, path in conn.execute("PRAGMA database_list"): + if name == "main": + return canonical_database_path(path) if path else None + raise MaintenanceUnavailableError("The connection has no main database") + + +_registry_lock = threading.Lock() +_coordinators: weakref.WeakValueDictionary = weakref.WeakValueDictionary() +_held_fds: set[int] = set() +_held_paths: dict[Path, int] = {} +_owner_waiters: dict[Path, tuple[int, int, "MaintenanceCoordinator"]] = {} +_thread_owners = threading.local() + + +class _AutomaticAdmission(NamedTuple): + coordinator: weakref.ReferenceType + path: Path + process_id: int + descriptor: int + launch: str + released: threading.Event + cancelled: threading.Event + + +def _automatic_admission_locked(path: Path) -> _AutomaticAdmission | None: + """Read only under registry ownership, in registry/coordinator order.""" + coordinator = _coordinators.get(path) + if coordinator is None: + return None + if not coordinator._mutex.acquire(blocking=False): + return None + try: + origin = coordinator._admission_owner + if (origin is not None and origin.coordinator() is coordinator + and origin.process_id == os.getpid() == coordinator._pid + and origin.path == path and _held_paths.get(path) == origin.descriptor + and coordinator._active and not coordinator._cancel.is_set() + and not origin.cancelled.is_set() and not origin.released.is_set()): + return origin + finally: + coordinator._mutex.release() + return None + + +def _acquire_owner(path: Path, on_busy: Callable[[], None] | None = None) -> int | None: + with _registry_lock: + if path in _held_paths: + if hasattr(_thread_owners, "admission_failure"): + _thread_owners.admission_failure = _automatic_admission_locked(path) + if on_busy is not None: + on_busy() + return None + fd = try_file_lock(Path(str(path) + ".maintenance.lock")) + if fd is not None: + _held_fds.add(fd) + _held_paths[path] = fd + launch = getattr(_thread_owners, "automatic_launch", None) + if launch is not None: + coordinator, identity = launch + with coordinator._mutex: + if (coordinator is _coordinators.get(path) and coordinator.path == path + and coordinator._pid == os.getpid() and coordinator._active + and not coordinator._cancel.is_set()): + coordinator._admission_owner = _AutomaticAdmission( + weakref.ref(coordinator), path, os.getpid(), fd, identity, + threading.Event(), threading.Event()) + elif on_busy is not None: + on_busy() + return fd + + +def _release_owner(fd: int) -> None: + notify = None + with _registry_lock: + origins = [] + for path, claimed_fd in tuple(_held_paths.items()): + if claimed_fd == fd: + coordinator = _coordinators.get(path) + if coordinator is not None: + with coordinator._mutex: + origin = coordinator._admission_owner + if origin is not None and origin.descriptor == fd and origin.process_id == os.getpid(): + coordinator._admission_owner = None + origins.append(origin) + os.close(fd) + _held_fds.discard(fd) + for path, claimed_fd in tuple(_held_paths.items()): + if claimed_fd == fd: + del _held_paths[path] + waiter = _owner_waiters.get(path) + if waiter is not None and waiter[0] == fd: + del _owner_waiters[path] + notify = waiter[2] + else: + notify = _coordinators.get(path) + for origin in origins: + origin.released.set() + if notify is not None: + notify._owner_released() + + +@contextmanager +def _bind_owner(path: Path | None) -> Iterator[MaintenanceOwnership]: + owners = getattr(_thread_owners, "owners", None) + if owners is None: + owners = {} + _thread_owners.owners = owners + token = MaintenanceOwnership(path, os.getpid(), uuid.uuid4().hex) + owners[path] = token + try: + yield token + finally: + if token.process_id == os.getpid(): + del owners[path] + + +@contextmanager +def maintenance_owner(db_path: str | os.PathLike[str] | None) -> Iterator[MaintenanceOwnership]: + """Own synchronous maintenance, nesting within the same thread's worker. + + In-memory callers still own serialization of their original connection; + no additional connection or cross-process lock is created for that case. + """ + path = canonical_database_path(db_path) if db_path is not None else None + current = getattr(_thread_owners, "owners", {}).get(path) + if current is not None and current.process_id == os.getpid(): + yield current + return + previous = getattr(_thread_owners, "admission_failure", None) + _thread_owners.admission_failure = None + try: + fd = _acquire_owner(path) if path is not None else None + failed_owner = _thread_owners.admission_failure + finally: + _thread_owners.admission_failure = previous + if path is not None and fd is None: + error = MaintenanceBusyError("Maintenance is already running for this database") + error._automatic_owner = failed_owner + raise error + owner_pid = os.getpid() + try: + with _bind_owner(path) as token: + yield token + finally: + # Fork cleanup already closed inherited descriptors. The same number + # may now refer to an unrelated descriptor opened by the child. + if fd is not None and owner_pid == os.getpid(): + _release_owner(fd) + + +def wait_for_automatic_owner(error: MaintenanceBusyError, *, deadline: float) -> None: + """Wait only for the exact automatic owner observed at failed acquisition.""" + origin = getattr(error, "_automatic_owner", None) + if (type(origin) is not _AutomaticAdmission or origin.process_id != os.getpid() + or origin.cancelled.is_set()): + raise error + if not _registry_lock.acquire(blocking=False): + raise error + try: + if not origin.released.is_set() and _automatic_admission_locked(origin.path) is not origin: + raise error + finally: + _registry_lock.release() + remaining = deadline - time.monotonic() + if remaining <= 0 or not origin.released.wait(min(remaining, threading.TIMEOUT_MAX)): + raise error + if origin.cancelled.is_set() or origin.process_id != os.getpid() or time.monotonic() >= deadline: + raise error + + +def _valid_initialization_receipt(receipt: object, path: Path | None) -> bool: + def bounded(value: object, limit: int) -> bool: + if type(value) is not str or len(value) > limit: + return False + try: + return len(value.encode()) <= limit + except UnicodeError: + return False + + if type(receipt) is not tuple or len(receipt) != 10 or path is None: + return False + if (not bounded(receipt[0], 4096) or receipt[0] != str(path) + or any(type(value) is not int or value < 0 for value in receipt[1:4]) + or type(receipt[4]) is not tuple or len(receipt[4]) != 5 + or any(not bounded(value, 512) for value in receipt[4]) + or receipt[4][0] != "legacy" + or type(receipt[5]) is not tuple or len(receipt[5]) != 2 + or type(receipt[6]) is not tuple or len(receipt[6]) > 3 + or type(receipt[7]) is not bool or type(receipt[8]) is not bool + or type(receipt[9]) is not str or len(receipt[9]) > 32): + return False + tables = {"vec_messages", "vec_messages_sep", "vec_messages_edge", "vec_messages_sep_edge", + "vec_messages_basepro", "vec_messages_sep_basepro"} + if any(type(table) is not str or table not in tables for table in receipt[5]): + return False + for row in receipt[6]: + if (type(row) is not tuple or len(row) != 9 + or any(not bounded(value, 512) for value in row[:3]) + or row[1] not in tables or row[2] not in tables + or (row[3] is not None and not bounded(row[3], 512)) + or any(value is not None and type(value) not in (int, float) for value in row[4:])): + return False + return True + + +@contextmanager +def _active_initialization_receipt(path: Path, *, expected: tuple | None = None) -> Iterator[tuple | None]: + """Guard a live automatic owner's evidence; the body must perform no I/O.""" + with _registry_lock: + coordinator = _coordinators.get(path) + if coordinator is None: + yield None + return + with coordinator._mutex: + record = coordinator._active_initialization + current = (record is not None and coordinator._pid == os.getpid() + and coordinator._active and coordinator._automatic_initialization + and not coordinator._cancel.is_set() and coordinator._thread is not None + and record[0][0] == os.getpid() and _held_paths.get(path) == record[0][1]) + if expected is not None: + current = current and expected[0]() is coordinator and expected[1] == record[0] + yield (weakref.ref(coordinator), record[0], record[1]) if current else None + + +def _after_fork() -> None: + global _registry_lock, _coordinators, _held_fds, _held_paths, _owner_waiters, _thread_owners + # The child must not prolong its parent's ownership by retaining an + # inherited descriptor. Inherited coordinator objects reject child use. + for fd in _held_fds: + os.close(fd) + _held_fds = set() + _held_paths = {} + _owner_waiters = {} + _thread_owners = threading.local() + for coordinator in tuple(_coordinators.values()): + coordinator._admission_owner = None + coordinator._active_initialization = None + coordinator._pending_initialization = None + coordinator._reserved_initialization = None + coordinator._automatic_initialization = False + _coordinators = weakref.WeakValueDictionary() + _registry_lock = threading.Lock() + + +if hasattr(os, "register_at_fork"): + os.register_at_fork( + before=lambda: _registry_lock.acquire(), + after_in_parent=lambda: _registry_lock.release(), + after_in_child=_after_fork, + ) + + +class MaintenanceRequest(NamedTuple): + generation: int + threshold: int + include_layers: bool = True + include_style: bool = False + + def merge(self, older: "MaintenanceRequest | None") -> "MaintenanceRequest": + if older is None: + return self + latest = self if self.generation >= older.generation else older + return latest._replace(include_layers=self.include_layers or older.include_layers, + include_style=self.include_style or older.include_style) + + +class MaintenanceCoordinator: + """One worker per canonical file, with ownership lasting through teardown.""" + + def __init__(self, path: Path | None) -> None: + self.path = path + self._pid = os.getpid() + self._mutex = threading.Lock() + self._active = False + self._thread: threading.Thread | None = None + self._cancel = threading.Event() + self._done = threading.Event() + self._done.set() + self._status = "pending" + self._error_category: str | None = None + self._notification_generation = 0 + self._pending: MaintenanceRequest | None = None + self._last_report = None + self._observation_generation = 0 + self._capability_epoch = 0 + self._capability_versions: tuple[tuple[str, str | None], ...] | None = None + self._extension_failure: LayerDependency | None = None + self._clustering_warning: tuple | None = None + self._pending_initialization: tuple | None = None + self._reserved_initialization: tuple | None = None + self._active_initialization: tuple | None = None + self._automatic_initialization = False + self._admission_owner: _AutomaticAdmission | None = None + + def _check_process(self) -> None: + if self._pid != os.getpid(): + raise RuntimeError("Get a new maintenance coordinator after process fork") + + @property + def status(self) -> tuple[str, str | None]: + self._check_process() + with self._mutex: + return self._status, self._error_category + + def snapshot(self) -> dict: + self._check_process() + with self._mutex: + return {"status": self._status, "error_category": self._error_category, + "active": self._active, "pending_generations": int(self._pending is not None), + "capability_epoch": self._capability_epoch, + "last_results": self._last_report.results if self._last_report is not None else ()} + + def _begin_observation(self) -> int: + self._check_process() + with self._mutex: + self._observation_generation += 1 + return self._observation_generation + + def _finish_observation(self, token: int, report: "MaintenanceReport | None", outcome: str, + error_category: str | None, *, borrowed: bool = False) -> None: + self._check_process() + with self._mutex: + if token != self._observation_generation or borrowed: + return + self._status = outcome + self._error_category = _style_error_category(error_category) + if report is not None: + self._last_report = report + + def style_observation(self) -> tuple["LayerResult | None", str | None]: + self._check_process() + with self._mutex: + return (self._last_report.style_result if self._last_report is not None else None, + self._error_category) + + def refresh_capabilities(self, *, reset_extension: bool = True) -> None: + """Refresh three package records at open/manual setup, never per add.""" + self._check_process() + versions = tuple((module, _installed_dependency(module, distribution)) for module, distribution in ( + ("numpy", "numpy"), ("hdbscan", "hdbscan"), ("sqlite_vec", "sqlite-vec"), + )) + with self._mutex: + self._capability_epoch += 1 + if reset_extension or versions != self._capability_versions: + self._extension_failure = None + self._capability_versions = versions + + def capability_snapshot(self) -> tuple[int, tuple, "LayerDependency | None"]: + self._check_process() + with self._mutex: + return self._capability_epoch, self._capability_versions or (), self._extension_failure + + def _publish_extension_evidence(self, epoch: int, dependency: "LayerDependency | None") -> None: + with self._mutex: + if epoch == self._capability_epoch: + self._extension_failure = dependency + + def observe_clustering_outcome(self, outcome: str, error_category: str | None = None) -> None: + """Warn once per failure transition, only after an actual work attempt.""" + self._check_process() + if outcome in {"success", "success_empty"}: + with self._mutex: + self._clustering_warning = None + return + if outcome not in {"unavailable", "failed"}: + return + category = _clustering_error_category(error_category) or "ClusteringUnavailable" + missing = _missing_clustering_dependencies() + key = (outcome, category, missing) + with self._mutex: + if key == self._clustering_warning: + return + self._clustering_warning = key + guidance = _clustering_install_guidance(missing) + logger.warning("Clustering %s (%s).%s", outcome, category, + " " + guidance if guidance else "") + + def clustering_failure(self) -> tuple[str, str] | None: + """Last failure observed in this process, cleared by successful work.""" + self._check_process() + with self._mutex: + return self._clustering_warning[:2] if self._clustering_warning is not None else None + + def _reserve_locked(self) -> None: + self._active = True + self._done.clear() + self._cancel.clear() + self._status = "starting" + self._error_category = None + self._active_initialization = None + self._reserved_initialization = None + self._automatic_initialization = False + + def request_layers(self, *, threshold: int = 25, include_layers: bool = True, + include_style: bool = False, _initialization_receipt: tuple | None = None) -> bool: + """Coalesce a real foreground wake; no callback captures its engine.""" + self._check_process() + if type(threshold) is not int or threshold < 1: + raise ValueError("Maintenance threshold must be a positive integer") + if type(include_layers) is not bool or type(include_style) is not bool or not (include_layers or include_style): + raise ValueError("Maintenance request requires explicit work kinds") + valid_receipt = _valid_initialization_receipt(_initialization_receipt, self.path) + with self._mutex: + if self.path is None: + self._pending_initialization = None + self._status = "pending_in_memory" + return False + if self._active and self._cancel.is_set(): + self._pending_initialization = None + return False + self._notification_generation += 1 + self._pending_initialization = ( + (self._notification_generation, _initialization_receipt) if valid_receipt else None) + self._pending = MaintenanceRequest(self._notification_generation, threshold, + include_layers, include_style).merge(self._pending) + pending = self._take_pending_locked() + return self._launch_layers(pending) if pending is not None else False + + def _take_pending_locked(self) -> MaintenanceRequest | None: + if self._active or self._pending is None: + return None + pending, self._pending = self._pending, None + evidence, self._pending_initialization = self._pending_initialization, None + self._reserve_locked() + if evidence is not None and evidence[0] == pending.generation: + self._reserved_initialization = evidence + return pending + + def _launch_layers(self, pending: MaintenanceRequest) -> bool: + with self._mutex: + evidence, self._reserved_initialization = self._reserved_initialization, None + receipt = evidence[1] if evidence is not None and evidence[0] == pending.generation else None + def work(conn: sqlite3.Connection, cancel: threading.Event) -> "MaintenanceReport": + return run_engine_maintenance(conn, self, threshold=pending.threshold, cancel=cancel, + include_layers=pending.include_layers, include_style=pending.include_style, connection_owned=True) + receipt_kwargs = {} if receipt is None else {"_initialization_receipt": receipt} + if not pending.include_layers: + return self._launch(work, pending, style_only=True, **receipt_kwargs) + return self._launch(work, pending, **receipt_kwargs) + + def _owner_released(self) -> None: + self._check_process() + with self._mutex: + pending = self._take_pending_locked() + if pending is not None: + try: + self._launch_layers(pending) + except Exception: + # The launch path records a bounded failure. Notification must + # not replace a synchronous owner's unrelated operation error. + pass + + def request(self, work: Callable[[sqlite3.Connection, threading.Event], None]) -> bool: + """Start one owned worker, or report busy/in-memory pending without retry.""" + self._check_process() + with self._mutex: + if self.path is None: + self._status = "pending_in_memory" + return False + if self._active: + return False + self._reserve_locked() + return self._launch(work) + + def _launch(self, work: Callable, pending: MaintenanceRequest | None = None, *, style_only: bool = False, + _initialization_receipt: tuple | None = None) -> bool: + def busy() -> None: + # Registry -> coordinator ordering closes the release/admission + # race. No coordinator-mutex holder acquires the registry lock. + with self._mutex: + self._active = False + self._active_initialization = self._reserved_initialization = None + self._automatic_initialization = False + self._status = "cancelled" if self._cancel.is_set() else "busy" + if pending is not None and not self._cancel.is_set(): + self._pending = pending.merge(self._pending) + if (self._cancel.is_set() or self._pending is None + or (self._pending_initialization is not None + and self._pending_initialization[0] != self._pending.generation)): + self._pending_initialization = None + if self._pending is not None and self.path in _held_paths: + _owner_waiters[self.path] = (_held_paths[self.path], self._pending[0], self) + self._done.set() + + fd = None + handoff = None + handoff_lock = threading.Lock() + with self._mutex: + launch_token = self._observation_generation + try: + # Serializing descriptor acquisition with fork registration makes + # every inherited owner descriptor visible to the child cleanup. + previous_launch = getattr(_thread_owners, "automatic_launch", None) + _thread_owners.automatic_launch = ( + (self, uuid.uuid4().hex) if pending is not None + and _valid_initialization_receipt(_initialization_receipt, self.path) else None) + try: + fd = _acquire_owner(self.path, on_busy=busy) + finally: + _thread_owners.automatic_launch = previous_launch + if fd is None: + return False + launch_token = self._begin_observation() + handoff = {"fd": fd, "started": False} + + def run() -> None: + with handoff_lock: + handoff["started"] = True + worker_fd = handoff.pop("fd", None) + if worker_fd is not None: + if style_only: + self._run(worker_fd, work, style_only=True) + else: + self._run(worker_fd, work) + + worker = threading.Thread(target=run, daemon=True, name="truememory-maintenance") + with self._mutex: + self._thread = worker + self._status = "running" + self._automatic_initialization = pending is not None + if (pending is not None and not self._cancel.is_set() + and _valid_initialization_receipt(_initialization_receipt, self.path)): + self._active_initialization = ((os.getpid(), fd, uuid.uuid4().hex), _initialization_receipt) + worker.start() + return True + except BaseException: + started = False + if handoff is not None: + with handoff_lock: + started = handoff["started"] + fd = handoff.pop("fd", None) + if started: + # start() can be interrupted after launching the target. That + # worker owns cleanup until it really finishes, even on error. + with self._mutex: + if self._active and self._thread is worker: + if self._admission_owner is not None: + self._admission_owner.cancelled.set() + self._admission_owner.released.set() + self._pending = None + self._active_initialization = self._pending_initialization = self._reserved_initialization = None + self._automatic_initialization = False + self._cancel.set() + raise + self._finish_observation(launch_token, None, "failed", "worker_start") + with self._mutex: + self._active_initialization = self._reserved_initialization = None + self._automatic_initialization = False + if (self._cancel.is_set() or self._pending is None + or (self._pending_initialization is not None + and self._pending_initialization[0] != self._pending.generation)): + self._pending_initialization = None + if fd is not None: + _release_owner(fd) + with self._mutex: + self._active = False + self._thread = None + if self._pending is not None and pending is not None: + self._pending = self._pending.merge(pending) + if (self._pending_initialization is not None and (self._pending is None + or self._pending_initialization[0] != self._pending.generation)): + self._pending_initialization = None + successor = self._take_pending_locked() + if successor is None: + self._done.set() + if successor is not None: + try: + self._launch_layers(successor) + except Exception: + pass + raise + + def _run(self, fd: int, work: Callable[[sqlite3.Connection, threading.Event], None], *, style_only: bool = False) -> None: + conn = None + outcome = "cancelled" + error_category = None + report = None + token = None + try: + token = self._begin_observation() + with _bind_owner(self.path): + try: + if not self._cancel.is_set(): + if style_only: + conn = _open_style_database(self.path, self._cancel) + else: + from truememory.tier_switch.runtime import open_serving_connection + conn = open_serving_connection(self.path, create_db) + report = work(conn, self._cancel) + if conn.in_transaction: + conn.rollback() + raise RuntimeError("Maintenance work left an unfinished transaction") + outcome = "cancelled" if self._cancel.is_set() else "success" + if isinstance(report, MaintenanceReport): + outcome, error_category = maintenance_report_status(report, self._cancel) + finally: + try: + if conn is not None: + conn.close() + finally: + conn = None + except BaseException as error: + # Thread boundary: expose only a category, never source-bearing + # exception text or a traceback through threading.excepthook. + if isinstance(error, _StyleOpenRejected): + report = MaintenanceReport((), "SKIPPED (style-only)", error.result) + outcome, error_category = maintenance_report_status(report, self._cancel) + else: + outcome = "cancelled" if isinstance(error, (KeyboardInterrupt, SystemExit)) else "failed" + error_category = type(error).__name__[:64] + report = None + finally: + if token is not None: + self._finish_observation(token, report if isinstance(report, MaintenanceReport) else None, + outcome, error_category) + with self._mutex: + self._active_initialization = None + self._automatic_initialization = False + _release_owner(fd) + with self._mutex: + self._active = False + self._thread = None + if self._cancel.is_set(): + self._pending = None + self._pending_initialization = self._reserved_initialization = None + pending = self._take_pending_locked() + if pending is None: + self._done.set() + if pending is not None: + try: + self._launch_layers(pending) + except Exception: + pass # Launch already recorded the categorical failure. + + def cancel(self) -> None: + """Signal a phase boundary; never release ownership of active work.""" + self._check_process() + with _registry_lock, self._mutex: + if self._admission_owner is not None: + self._admission_owner.cancelled.set() + self._admission_owner.released.set() + self._pending = None + self._active_initialization = self._pending_initialization = self._reserved_initialization = None + self._automatic_initialization = False + self._cancel.set() + waiter = _owner_waiters.get(self.path) + if waiter is not None and waiter[2] is self: + del _owner_waiters[self.path] + + def wait(self, timeout: float | None = None) -> bool: + """Wait for this worker if requested; report whether teardown completed.""" + self._check_process() + return self._done.wait(timeout) + + +def get_coordinator(db_path: str | os.PathLike[str]) -> MaintenanceCoordinator: + path = canonical_database_path(db_path) + if path is None: + return MaintenanceCoordinator(None) + with _registry_lock: + coordinator = _coordinators.get(path) + if coordinator is None: + coordinator = MaintenanceCoordinator(path) + _coordinators[path] = coordinator + return coordinator + + +@contextmanager +def maintenance_observation(coordinator: MaintenanceCoordinator, *, borrowed: bool = False) -> Iterator[list]: + """Publish explicit work after caller cleanup and before releasing ownership.""" + with maintenance_owner(coordinator.path): + token = coordinator._begin_observation() + reports = [] + try: + yield reports + except BaseException as error: + coordinator._finish_observation(token, None, "failed", type(error).__name__, borrowed=borrowed) + raise + else: + report = reports[-1] if reports else None + outcome, category = maintenance_report_status(report) if report is not None else ("failed", "MissingReport") + coordinator._finish_observation(token, report, outcome, category, borrowed=borrowed) + + +class LayerDependency(NamedTuple): + key: str + builder_version: int + available: bool + error_category: str | None = None + deferred: bool = False + + +def make_layer_dependency( + builder_version: int, parameters: dict | None = None, *, + available: bool = True, error_category: str | None = None, +) -> LayerDependency: + """Version each provenance key independently; parameters must be nonpersonal.""" + if type(builder_version) is not int or builder_version < 1: + raise ValueError("Builder version must be a positive integer") + if error_category is not None and re.fullmatch(r"[A-Za-z][A-Za-z0-9_]{0,63}", error_category) is None: + raise ValueError("Dependency errors must be bounded categories") + key = json.dumps({ + "builder_version": builder_version, "parameters": parameters or {}, + "available": available, "error_category": error_category, + "python": list(sys.version_info[:3]), + }, sort_keys=True, separators=(",", ":"), allow_nan=False) + return LayerDependency(key, builder_version, available, error_category) + + +class LayerSpec(NamedTuple): + layer: str + result_key: str + resolve_dependency: Callable[[], LayerDependency] + build: Callable[[sqlite3.Connection], object] + count_output: Callable[[sqlite3.Connection], int] + publication_guard: Callable[[sqlite3.Connection], AbstractContextManager[Callable[[], None]]] | None = None + read_coverage: Callable[[sqlite3.Connection], str] | None = None + borrowed_publication_guard: Callable[[sqlite3.Connection], AbstractContextManager[Callable[[], None]]] | None = None + connection: sqlite3.Connection | None = None + execution_guard: Callable[[sqlite3.Connection, bool], AbstractContextManager[None]] | None = None + + +class LayerState(NamedTuple): + layer: str + source: SourceRevision + dependency: LayerDependency + outcome: str + successful_epoch: str | None + successful_revision: int | None + successful_dependency: str | None + attempted_epoch: str | None + attempted_revision: int | None + attempted_dependency: str | None + attempted_insert_count: int | None + full_rebuild_required: bool + output_count: int | None + run_generation: str | None + error_category: str | None + successful_coverage: str = "unverified" + + +class LayerResult(NamedTuple): + layer: str + result_key: str + outcome: str + output_count: int | None + elapsed_seconds: float + error_category: str | None + attempted: bool + coverage: str = "unverified" + pending_caller_commit: bool = False + + +_LAYER_COVERAGE = frozenset({"unverified", "complete", "legacy_contacts_unowned", "vector_generation_unverified"}) + + +class _LayerRollbackFailed(RuntimeError): + """Do not continue publication or diagnostics after a rejected rollback.""" + + +def _validate_layer(layer: str) -> None: + if re.fullmatch(r"[a-z][a-z0-9_]{0,63}", layer) is None: + raise ValueError("Invalid maintenance layer name") + + +def layer_freshness(source: SourceRevision, state: LayerState, dependency: LayerDependency) -> str: + """Separate a prior valid success from the latest attempt's outcome.""" + if dependency.deferred: + return "dependency_deferred" + if (state.successful_epoch != source.epoch or state.successful_revision is None + or state.full_rebuild_required or state.output_count is None): + return "untrusted" + if state.successful_dependency != dependency.key or not dependency.available: + return "dependency_pending" + if state.successful_revision == source.revision: + return "current" + if state.successful_revision > source.revision or source.nonappend_revision > state.successful_revision: + return "correction_pending" + return "append_pending" + + +def read_layer_states(conn: sqlite3.Connection, specs: tuple[LayerSpec, ...]) -> dict[str, LayerState]: + """Read source and checkpoint rows in one SQLite snapshot; never commit.""" + if not specs: + return {} + dependencies = {} + for spec in specs: + _validate_layer(spec.layer) + if spec.connection is not None and spec.connection is not conn: + raise ValueError("Layer adapters belong to another connection") + if spec.layer in dependencies: + raise ValueError("Duplicate maintenance layer") + dependencies[spec.layer] = spec.resolve_dependency() + placeholders = ",".join("?" for _ in dependencies) + try: + rows = conn.execute( + "SELECT s.epoch,s.revision,s.insert_count,s.correction_count,s.max_seen_message_id," + "s.nonappend_revision,s.tracking_ready,l.layer,l.outcome,l.successful_epoch," + "l.successful_revision,l.successful_dependency,l.attempted_epoch,l.attempted_revision," + "l.attempted_dependency,l.attempted_insert_count,l.full_rebuild_required,l.output_count," + "l.run_generation,l.error_category,l.successful_coverage FROM maintenance_source_state s LEFT JOIN maintenance_layers l " + f"ON l.layer IN ({placeholders}) WHERE s.singleton=1", tuple(dependencies), + ).fetchall() + except sqlite3.OperationalError as error: + if str(error).startswith(("no such table:", "no such column:")): + raise MaintenanceUnavailableError("Maintenance tracking schema is unavailable") from error + raise + if not rows or not rows[0][6]: + raise MaintenanceUnavailableError("Committed source revision tracking is unavailable") + source = SourceRevision(*rows[0][:6]) + stored = {row[7]: row for row in rows if row[7] is not None} + states = {} + for layer, dependency in dependencies.items(): + row = stored.get(layer) + values = row[8:] if row is not None else ("pending", None, None, None, None, None, None, None, 1, None, None, None, "unverified") + states[layer] = LayerState(layer, source, dependency, *values) + return states + + +def _read_layer_run_baseline( + conn: sqlite3.Connection, specs: tuple[LayerSpec, ...], *, _canonical_source: bool = False, +) -> dict[str, LayerState] | None: + """Return no style observation only for a read lock with unchanged ownership. + + This boundary is used before running diagnostics and for cancelled reads, + never around builders or publication transactions. + Canonical style readiness and checkpoint values share one read snapshot. + """ + borrowed = conn.in_transaction + try: + if not _canonical_source: + return read_layer_states(conn, specs) + if not borrowed: + conn.execute("BEGIN") + try: + _validate_routed_style_source(conn) + states = read_layer_states(conn, specs) + state = states.get("style_vectors") + if state is not None: + # Canonical preparation already established enrollment. A lost + # proof here is repairable, not a completed unavailable attempt. + if state.dependency.deferred or not state.dependency.available: + raise _StyleAppendChanged(state.dependency.error_category or "StyleMetadataChanged") + if state.successful_coverage == "complete": + try: + dependency, _schema, _marker = _style_append_metadata(conn) + if dependency != state.dependency: + raise _StyleAppendChanged("StyleMetadataChanged") + except _StyleAppendChanged: + if state.outcome not in {"failed", "unavailable"}: + raise + # Keep a real builder's durable retry baseline. Its old + # coverage no longer certifies this read's metadata. + states[state.layer] = state._replace(successful_coverage="unverified") + if not conn.in_transaction: + raise _LayerRollbackFailed("Canonical style read lost its transaction") + return states + finally: + if not borrowed: + if not conn.in_transaction: + raise _LayerRollbackFailed("Canonical style read ended without rollback") + try: + conn.rollback() + except sqlite3.Error as error: + raise _LayerRollbackFailed("Canonical style read rollback failed") from error + if conn.in_transaction: + raise _LayerRollbackFailed("Canonical style read rollback left a transaction") + except sqlite3.OperationalError as error: + if (len(specs) == 1 and specs[0].layer == "style_vectors" and conn.in_transaction == borrowed + and _style_sqlite_deferral(error) == "StyleWriterBusy"): + return None + raise + + +def _style_baseline_busy_result(spec: LayerSpec, borrowed: bool) -> LayerResult: + return LayerResult(spec.layer, spec.result_key, "deferred", None, 0.0, + "StyleWriterBusy", False, "unverified", borrowed) + + +@contextmanager +def layer_read_snapshot( + conn: sqlite3.Connection, layer: str, dependency: LayerDependency, +) -> Iterator[LayerState]: + """A read-only getter can read its row and provenance in the same snapshot.""" + owned = not conn.in_transaction + if owned: + conn.execute("BEGIN") + try: + spec = LayerSpec(layer, layer, lambda: dependency, lambda _conn: None, lambda _conn: 0) + yield read_layer_states(conn, (spec,))[layer] + finally: + if owned and conn.in_transaction: + conn.rollback() + + +def _eligible(state: LayerState, threshold: int, force: bool) -> bool: + if state.dependency.deferred: + return False + if state.outcome == "running": + return False + if force or state.outcome == "abandoned": + return True + source = state.source + if layer_freshness(source, state, state.dependency) == "current": + return False + if (state.attempted_epoch != source.epoch or state.attempted_revision is None + or state.attempted_insert_count is None or state.attempted_dependency != state.dependency.key): + return True + terminal_failure = state.outcome in {"failed", "unavailable"} + if terminal_failure and state.attempted_revision == source.revision: + return False + # A failed initial build still has a durable scheduling baseline even + # though it has never established successful output provenance. + if not terminal_failure and layer_freshness(source, state, state.dependency) == "untrusted": + return True + if source.nonappend_revision > state.attempted_revision: + return True + return source.insert_count - state.attempted_insert_count >= threshold + + +def plan_layers( + conn: sqlite3.Connection, specs: tuple[LayerSpec, ...], *, threshold: int = 25, force: bool = False, +) -> tuple[LayerSpec, ...]: + """Plan from durable attempt counts, including first/empty bootstrap work.""" + if type(threshold) is not int or threshold < 1: + raise ValueError("Maintenance threshold must be a positive integer") + states = read_layer_states(conn, specs) + # A previous process may have died while recording "running". Let the + # real ownership claim distinguish active work from an abandoned record. + return tuple(spec for spec in specs if not states[spec.layer].dependency.deferred + and (states[spec.layer].outcome == "running" or _eligible(states[spec.layer], threshold, force))) + + +def record_layer_success_in_transaction( + conn: sqlite3.Connection, *, layer: str, dependency: LayerDependency, + source: SourceRevision, output_count: int, run_generation: str | None = None, + coverage: str = "complete", +) -> None: + """Publish provenance beside completed output, without owning caller commit.""" + _validate_layer(layer) + if not conn.in_transaction: + raise RuntimeError("Layer publication requires an existing transaction") + if not dependency.available or type(output_count) is not int or output_count < 0: + raise ValueError("Successful layer publication requires available output") + if coverage not in _LAYER_COVERAGE: + raise ValueError("Invalid layer coverage") + if read_source_revision(conn) != source: + raise sqlite3.OperationalError("Maintenance source changed before publication") + conn.execute( + "INSERT INTO maintenance_layers(layer,builder_version,outcome,successful_epoch,successful_revision," + "successful_dependency,attempted_epoch,attempted_revision,attempted_dependency,attempted_insert_count," + "full_rebuild_required,output_count,run_generation,error_category,successful_coverage) VALUES (?,?,?,?,?,?,?,?,?,?,0,?,?,NULL,?) " + "ON CONFLICT(layer) DO UPDATE SET builder_version=excluded.builder_version,outcome=excluded.outcome," + "successful_epoch=excluded.successful_epoch,successful_revision=excluded.successful_revision," + "successful_dependency=excluded.successful_dependency,attempted_epoch=excluded.attempted_epoch," + "attempted_revision=excluded.attempted_revision,attempted_dependency=excluded.attempted_dependency," + "attempted_insert_count=excluded.attempted_insert_count,full_rebuild_required=0," + "output_count=excluded.output_count,run_generation=excluded.run_generation,error_category=NULL," + "successful_coverage=excluded.successful_coverage", + (layer, dependency.builder_version, "success" if output_count else "success_empty", source.epoch, + source.revision, dependency.key, source.epoch, source.revision, dependency.key, + source.insert_count, output_count, run_generation, coverage), + ) + + +@contextmanager +def _owned_transaction( + conn: sqlite3.Connection, *, on_rollback: Callable[[], None] | None = None, immediate: bool = False, +) -> Iterator[None]: + if conn.in_transaction: + raise RuntimeError("Maintenance runner requires a clean transaction boundary") + conn.execute("BEGIN IMMEDIATE" if immediate else "BEGIN") + completed = False + automatic_rollback = False + try: + yield + conn.execute("COMMIT") + completed = True + except _StyleSQLInterrupted as error: + automatic_rollback = error.automatic_rollback + raise + finally: + if not completed: + if conn.in_transaction: + try: + conn.rollback() + except BaseException as error: + if on_rollback is not None: + raise _LayerRollbackFailed("Output rollback failed") from error + raise + if on_rollback is not None: + on_rollback() + elif on_rollback is not None: + if automatic_rollback: + on_rollback() + else: + raise _LayerRollbackFailed("Output transaction ended without a confirmed rollback") + + +@contextmanager +def _borrowed_transaction( + conn: sqlite3.Connection, *, on_rollback: Callable[[], None] | None = None, +) -> Iterator[None]: + if not conn.in_transaction: + raise RuntimeError("Borrowed maintenance requires the caller's transaction") + name = "truememory_layer_" + uuid.uuid4().hex + conn.execute(f"SAVEPOINT {name}") + completed = False + try: + yield + conn.execute(f"RELEASE {name}") + completed = True + finally: + if not completed and conn.in_transaction: + try: + conn.execute(f"ROLLBACK TO {name}") + except BaseException as error: + raise _LayerRollbackFailed("Caller must roll back the failed maintenance transaction") from error + if on_rollback is not None: + on_rollback() + conn.execute(f"RELEASE {name}") + elif not completed and on_rollback is not None: + raise _LayerRollbackFailed("Caller transaction ended without a confirmed rollback") + + +def _record_attempt( + conn: sqlite3.Connection, state: LayerState, outcome: str, generation: str, error_category: str | None, + *, borrowed: bool = False, canonical_source: bool = False, +) -> bool | None: + source, dependency = state.source, state.dependency + if dependency.deferred: + raise ValueError("Transient deferral cannot be recorded as a durable attempt") + transaction = _borrowed_transaction if borrowed else _owned_transaction + with (transaction(conn, on_rollback=lambda: None) if canonical_source else transaction(conn)): + if state.layer == "style_vectors": + conn.execute("UPDATE maintenance_layers SET layer=layer WHERE 0") + if canonical_source: + _validate_routed_style_source(conn) + if _style_extra_triggers(conn): + return False + conn.execute( + "INSERT INTO maintenance_layers(layer,builder_version,outcome,attempted_epoch,attempted_revision," + "attempted_dependency,attempted_insert_count,run_generation,error_category) VALUES (?,?,?,?,?,?,?,?,?) " + "ON CONFLICT(layer) DO UPDATE SET builder_version=excluded.builder_version,outcome=excluded.outcome," + "attempted_epoch=excluded.attempted_epoch,attempted_revision=excluded.attempted_revision," + "attempted_dependency=excluded.attempted_dependency,attempted_insert_count=excluded.attempted_insert_count," + "run_generation=excluded.run_generation,error_category=excluded.error_category", + (state.layer, dependency.builder_version, outcome, source.epoch, source.revision, + dependency.key, source.insert_count, generation, error_category), + ) + + +class _LayerCancelled(Exception): + """Internal cooperative cancellation before publication.""" + + +_LAYER_ROW_COLUMNS = ( + "layer", "builder_version", "outcome", "successful_epoch", "successful_revision", "successful_dependency", + "successful_coverage", "attempted_epoch", "attempted_revision", "attempted_dependency", "attempted_insert_count", + "full_rebuild_required", "output_count", "run_generation", "error_category", +) + + +def _read_layer_row(conn: sqlite3.Connection, layer: str) -> tuple | None: + row = conn.execute("SELECT " + ",".join(_LAYER_ROW_COLUMNS) + " FROM maintenance_layers WHERE layer=?", (layer,)).fetchone() + return tuple(row) if row is not None else None + + +def _restore_deferred_attempt( + conn: sqlite3.Connection, layer: str, previous: tuple | None, running: tuple, + owner: MaintenanceOwnership, *, protect_rollback: bool = False, canonical_source: bool = False, +) -> None: + """Restore only this owner's unchanged diagnostic, after output rollback.""" + if conn.in_transaction: + raise _LayerRollbackFailed("Output rollback is not complete") + current_owner = getattr(_thread_owners, "owners", {}).get(owner.path) + if current_owner != owner or owner.process_id != os.getpid(): + raise MaintenanceBusyError("Maintenance ownership changed before diagnostic restoration") + with _owned_transaction(conn, on_rollback=(lambda: None) if protect_rollback else None): + conn.execute("UPDATE maintenance_layers SET layer=layer WHERE 0") + if canonical_source: + _validate_routed_style_source(conn) + if layer == "style_vectors" and _style_extra_triggers(conn): + raise _StyleAppendChanged("StyleTriggersUnsupported") + current = _read_layer_row(conn, layer) + if (current != running or current is None or current[2] != "running" + or current[_LAYER_ROW_COLUMNS.index("run_generation")] != owner.generation): + raise MaintenanceBusyError("Maintenance diagnostic changed before restoration") + if previous is None: + conn.execute("DELETE FROM maintenance_layers WHERE layer=? AND outcome='running' AND run_generation=?", + (layer, owner.generation)) + else: + assignments = ",".join(column + "=?" for column in _LAYER_ROW_COLUMNS[1:]) + conn.execute("UPDATE maintenance_layers SET " + assignments + + " WHERE layer=? AND outcome='running' AND run_generation=?", + (*previous[1:], layer, owner.generation)) + + +def _check_layer_dependency(current: LayerDependency, expected: LayerDependency) -> None: + if current.deferred: + if current.error_category == "StyleTriggersUnsupported": + raise LayerDeferredError("Style triggers prevent publication", category="StyleTriggersUnsupported") + raise LayerDeferredError("Embedding runtime is temporarily busy") + if current != expected: + raise MaintenanceUnavailableError("Layer dependency changed before publication") + + +@contextmanager +def _maintenance_embedding_scope(conn: sqlite3.Connection, enabled: bool, + cancel: threading.Event | None, *, connection_lock: object | None = None, + deadline: float | None = None) -> Iterator[bool]: + if not enabled or cancel is not None and cancel.is_set(): + yield True + return + from truememory.tier_switch.runtime import maintenance_serving_operation + with maintenance_serving_operation(conn, cancelled=cancel, deadline=deadline, + connection_lock=connection_lock) as supported: + yield supported + + +def _selected_cluster_deferred(conn: sqlite3.Connection) -> LayerResult: + return LayerResult("clusters", "cluster_messages", "deferred", None, 0.0, + "SelectedVectorLayerUnsupported", False, "unverified", conn.in_transaction) + + +def run_layers( + conn: sqlite3.Connection, specs: tuple[LayerSpec, ...], *, force: bool = False, + threshold: int = 25, cancel: threading.Event | None = None, + allow_caller_transaction: bool = False, _canonical_source: bool = False, +) -> tuple[LayerResult, ...]: + """Run eligible adapters once; borrowing requires explicit caller opt-in.""" + if type(allow_caller_transaction) is not bool: + raise ValueError("Caller transaction opt-in must be a boolean") + borrowed = conn.in_transaction and allow_caller_transaction + if conn.in_transaction and not borrowed: + raise RuntimeError("Maintenance runner requires a clean transaction boundary") + if type(threshold) is not int or threshold < 1: + raise ValueError("Maintenance threshold must be a positive integer") + if any(spec.connection is not None and spec.connection is not conn for spec in specs): + raise ValueError("Layer adapters belong to another connection") + results = [] + with maintenance_owner(connection_database_path(conn)) as owner, _maintenance_embedding_scope( + conn, any(spec.layer == "clusters" for spec in specs), cancel, + ) as supported: + if not supported: + results.append(_selected_cluster_deferred(conn)) + specs = tuple(spec for spec in specs if spec.layer != "clusters") + if not specs: + return tuple(results) + def record_attempt(state: LayerState, outcome: str, category: str | None) -> bool: + if _canonical_source: + return _record_attempt(conn, state, outcome, owner.generation, category, + borrowed=borrowed, canonical_source=True) is not False + if borrowed: + return _record_attempt(conn, state, outcome, owner.generation, category, borrowed=True) is not False + return _record_attempt(conn, state, outcome, owner.generation, category) is not False + + # Validate uniqueness before any builder is invoked. + baseline_options = {"_canonical_source": True} if _canonical_source else {} + if _read_layer_run_baseline(conn, specs, **baseline_options) is None: + return (_style_baseline_busy_result(specs[0], borrowed),) + for spec in specs: + if cancel is not None and cancel.is_set(): + break + observed = _read_layer_run_baseline(conn, (spec,), **baseline_options) + if observed is None: + results.append(_style_baseline_busy_result(spec, borrowed)) + continue + state = observed[spec.layer] + if state.dependency.deferred: + results.append(LayerResult(spec.layer, spec.result_key, "deferred", state.output_count, 0.0, + state.dependency.error_category, False, state.successful_coverage, borrowed)) + continue + if state.outcome == "running" and state.run_generation != owner.generation: + # Ownership proves abandonment. Do not acquire a caller's + # writer merely to persist a diagnostic before computation. + state = state._replace(outcome="abandoned") + if not _eligible(state, threshold, force): + results.append(LayerResult(spec.layer, spec.result_key, + layer_freshness(state.source, state, state.dependency), state.output_count, 0.0, + state.error_category, False, state.successful_coverage, borrowed)) + continue + started = time.monotonic() + baseline_state = state + if not state.dependency.available: + recorded = record_attempt(state, "unavailable", state.dependency.error_category) + results.append(LayerResult(spec.layer, spec.result_key, "unavailable" if recorded else "deferred", + state.output_count, time.monotonic() - started, + state.dependency.error_category if recorded else "StyleTriggersUnsupported", + recorded, state.successful_coverage, borrowed)) + continue + # This diagnostic commits before the read/compute snapshot starts. + previous_row = running_row = None + if not borrowed: + diagnostic_rolled_back = False + + def confirm_diagnostic_rollback() -> None: + nonlocal diagnostic_rolled_back + diagnostic_rolled_back = True + + try: + with _owned_transaction(conn, on_rollback=( + confirm_diagnostic_rollback if spec.layer == "style_vectors" else None + )): + if spec.layer == "style_vectors": + conn.execute("UPDATE maintenance_layers SET layer=layer WHERE 0") + if _canonical_source: + _validate_routed_style_source(conn) + if _style_extra_triggers(conn): + raise _StyleAppendChanged("StyleTriggersUnsupported") + previous_row = _read_layer_row(conn, spec.layer) + conn.execute( + "INSERT INTO maintenance_layers(layer,builder_version,outcome,run_generation,error_category) " + "VALUES (?,?,'running',?,NULL) ON CONFLICT(layer) DO UPDATE SET " + "builder_version=excluded.builder_version,outcome='running'," + "run_generation=excluded.run_generation,error_category=NULL", + (spec.layer, state.dependency.builder_version, owner.generation), + ) + running_row = _read_layer_row(conn, spec.layer) + except (sqlite3.OperationalError, _StyleAppendChanged) as error: + category = error.category if isinstance(error, _StyleAppendChanged) else _style_sqlite_deferral(error) + if (spec.layer != "style_vectors" or not diagnostic_rolled_back + or category not in {"StyleWriterBusy", "StyleTriggersUnsupported", "StyleSourceChanged"}): + raise + results.append(LayerResult(spec.layer, spec.result_key, "deferred", state.output_count, + time.monotonic() - started, category, False, state.successful_coverage)) + continue + count = None + coverage = state.successful_coverage + built = output_started = rolled_back = False + + def confirm_rollback() -> None: + nonlocal rolled_back + rolled_back = True + + try: + # Factories capture identity before the attempt's read snapshot. + # The deferred guard enters only at publication, then outlives + # the transaction so both COMMIT and rollback retain ownership. + factory = spec.borrowed_publication_guard if borrowed else spec.publication_guard + if borrowed and spec.publication_guard is not None and factory is None: + raise LayerUnavailableError("The adapter has no borrowed publication contract") + guard = factory(conn) if factory is not None else None + with ExitStack() as publication: + transaction = _borrowed_transaction if borrowed else _owned_transaction + with transaction(conn, on_rollback=confirm_rollback), ( + spec.execution_guard(conn, not borrowed) if spec.execution_guard is not None else nullcontext() + ): + output_started = True + state = read_layer_states(conn, (spec,))[spec.layer] + if (_canonical_source and spec.layer == "style_vectors" + and baseline_state.successful_coverage == "unverified"): + state = state._replace(successful_coverage="unverified") + if state.dependency.deferred: + _check_layer_dependency(state.dependency, state.dependency) + if not state.dependency.available: + raise MaintenanceUnavailableError("Layer dependency became unavailable") + built = True + spec.build(conn) + if not conn.in_transaction: + raise RuntimeError("Layer builder committed the runner transaction") + count = spec.count_output(conn) + coverage = spec.read_coverage(conn) if spec.read_coverage is not None else "complete" + if cancel is not None and cancel.is_set(): + raise _LayerCancelled() + _check_layer_dependency(spec.resolve_dependency(), state.dependency) + record_layer_success_in_transaction(conn, layer=spec.layer, dependency=state.dependency, + source=state.source, output_count=count, run_generation=owner.generation, coverage=coverage) + _check_layer_dependency(spec.resolve_dependency(), state.dependency) + if guard is not None: + # Register release before validation can fail, so + # validation errors also roll back under ownership. + validate_publication = publication.enter_context(guard) + validate_publication() + if cancel is not None and cancel.is_set(): + raise _LayerCancelled() + except BaseException as error: + if isinstance(error, _LayerRollbackFailed): + raise + runtime = sys.modules.get("truememory.tier_switch.runtime") + reject = getattr(runtime, "raise_if_serving_rejection", None) + if reject is not None: + reject(error) + if isinstance(error, LayerDeferredError): + if output_started and not rolled_back: + raise _LayerRollbackFailed("Deferred output has no confirmed rollback") from error + if not borrowed: + try: + if _canonical_source: + _restore_deferred_attempt(conn, spec.layer, previous_row, running_row, owner, + protect_rollback=True, canonical_source=True) + elif spec.layer == "style_vectors": + _restore_deferred_attempt(conn, spec.layer, previous_row, running_row, owner, + protect_rollback=True) + else: + _restore_deferred_attempt(conn, spec.layer, previous_row, running_row, owner) + except _StyleAppendChanged as restore_error: + if spec.layer != "style_vectors" or conn.in_transaction: + raise + error = LayerDeferredError("Style triggers prevent diagnostic restoration", + category=restore_error.category) + except sqlite3.OperationalError as restore_error: + if (spec.layer != "style_vectors" or conn.in_transaction + or _style_sqlite_deferral(restore_error) != "StyleWriterBusy"): + raise + # Another writer still owns SQLite. Keep the + # committed running row for later owner recovery; + # do not claim restoration or retry under this owner. + results.append(LayerResult(spec.layer, spec.result_key, "deferred", baseline_state.output_count, + time.monotonic() - started, error.category, built, baseline_state.successful_coverage, borrowed)) + if cancel is not None and cancel.is_set(): + break + continue + interrupted = not isinstance(error, Exception) or isinstance(error, _LayerCancelled) + unavailable = not state.dependency.available or isinstance(error, LayerUnavailableError) + outcome = "abandoned" if interrupted else ("unavailable" if unavailable else "failed") + category = "Cancelled" if interrupted else type(error).__name__[:64] + if re.fullmatch(r"[A-Za-z][A-Za-z0-9_]{0,63}", category) is None: + category = "Error" + if not record_attempt(state, outcome, category): + outcome, category = "deferred", "StyleTriggersUnsupported" + results.append(LayerResult(spec.layer, spec.result_key, outcome, state.output_count, + time.monotonic() - started, category, True, state.successful_coverage, borrowed)) + if not isinstance(error, Exception): + raise + if interrupted: + break + else: + results.append(LayerResult(spec.layer, spec.result_key, "success" if count else "success_empty", + count, time.monotonic() - started, None, True, coverage, borrowed)) + return tuple(results) + + +def prepare_style_maintenance( + conn: sqlite3.Connection, *, _on_safe_failure: Callable[[sqlite3.OperationalError], None] | None = None, +) -> None: + """Explicit lazy enrollment; preserve a caller's final commit/rollback.""" + borrowed = conn.in_transaction + try: + read_source_revision(conn) + if _style_extra_triggers(conn): + raise _StyleAppendChanged("StyleTriggersUnsupported") + except sqlite3.OperationalError as error: + if _on_safe_failure is not None and conn.in_transaction == borrowed: + _on_safe_failure(error) + raise + class GuardedPreparation: + def __getattr__(self, name: str) -> object: + return getattr(conn, name) + + def execute(self, sql: str, *args: object) -> sqlite3.Cursor: + cursor = conn.execute(sql, *args) + # Storage upgrades the writer with this zero-row statement before + # enrollment or invalidation. Its existing rollback boundary owns + # failures here, including a trigger installed after the first probe. + if sql == "UPDATE maintenance_layers SET layer=layer WHERE 0" and _style_extra_triggers(conn): + raise _StyleAppendChanged("StyleTriggersUnsupported") + return cursor + + _prepare_style_maintenance(GuardedPreparation(), _on_safe_failure=_on_safe_failure) + + +class _StyleAppendChanged(RuntimeError): + """A fixed metadata or checkpoint proof no longer permits publication.""" + + def __init__(self, category: str) -> None: + self.category = category + super().__init__(category) + + +def _style_extra_triggers(conn: sqlite3.Connection) -> bool: + names = tuple(_STYLE_OUTPUT_TRIGGERS) + main = conn.execute( + "SELECT 1 FROM main.sqlite_master WHERE type='trigger' " + "AND lower(tbl_name) IN ('entity_style_vectors','maintenance_layers','metadata') " + "AND NOT (lower(tbl_name)='entity_style_vectors' AND name IN (?,?,?)) LIMIT 1", names, + ).fetchone() + temporary = conn.execute( + "SELECT 1 FROM temp.sqlite_master WHERE type='trigger' " + "AND lower(tbl_name) IN ('entity_style_vectors','maintenance_layers','metadata') LIMIT 1", + ).fetchone() + return main is not None or temporary is not None + + +def _style_error_category(category: str | None) -> str | None: + if category is None: + return None + return category if re.fullmatch(r"[A-Za-z][A-Za-z0-9_]{0,63}", category) else "Error" + + +def _style_source_ready(conn: sqlite3.Connection) -> None: + """Observe current canonical tracking, never repair or infer its history.""" + required = { + "messages": set(_MAINTENANCE_SOURCE_FIELDS), + "maintenance_source_state": {"singleton", "epoch", "revision", "insert_count", "correction_count", + "max_seen_message_id", "nonappend_revision", "tracking_ready"}, + "maintenance_layers": set(_LAYER_ROW_COLUMNS), + "metadata": {"key", "value"}, + } + shadows = (*required, "entity_style_vectors") + placeholders = ",".join("?" for _ in shadows) + if conn.execute("SELECT 1 FROM temp.sqlite_master WHERE type IN ('table','view') " + f"AND lower(name) IN ({placeholders}) LIMIT 1", shadows).fetchone() is not None: + raise MaintenanceUnavailableError("Canonical style tables are shadowed") + for table, columns in required.items(): + actual = {row[1].lower() for row in conn.execute(f"PRAGMA main.table_xinfo({table})")} + if not columns.issubset(actual): + raise MaintenanceUnavailableError("Canonical source schema is unavailable") + if not _messages_have_rowid_id(conn) or not _valid_style_source(read_source_revision(conn)): + raise MaintenanceUnavailableError("Canonical source identity is unavailable") + definitions = _maintenance_trigger_definitions(conn) + placeholders = ",".join("?" for _ in definitions) + current = {row[0]: " ".join(row[1].strip().rstrip(";").split()) for row in conn.execute( + "SELECT name,sql FROM main.sqlite_master WHERE type='trigger' " + f"AND name IN ({placeholders})", tuple(definitions), + )} + expected = {name: " ".join(sql.split()) for name, sql in definitions.items()} + shadow = conn.execute("SELECT 1 FROM temp.sqlite_master WHERE type='trigger' " + f"AND lower(name) IN ({placeholders}) LIMIT 1", tuple(definitions)).fetchone() + if current != expected or shadow is not None: + raise MaintenanceUnavailableError("Canonical source triggers are unavailable") + + +class StyleObservation(NamedTuple): + state: LayerState | None + checkpoint: tuple | None + eligible: bool + health: dict + + +def observe_style(conn: sqlite3.Connection | None = None, *, threshold: int = 25, + pending_reason: str | None = None) -> StyleObservation: + """Bounded native-free readiness; no profile/source scans or schema writes.""" + if type(threshold) is not int or threshold < 1: + raise ValueError("Maintenance threshold must be a positive integer") + health = {"available": False, "outcome": "pending", "freshness": "unknown", "coverage": "unverified", + "error_category": None, "output_count": None, "pending_reason": pending_reason or "not_connected", + "pending_caller_commit": False, "hash_ready": False, "format_ready": False} + if conn is None: + return StyleObservation(None, None, False, health) + borrowed = conn.in_transaction + health["pending_caller_commit"] = borrowed + owned = False + state = checkpoint = None + eligible = False + try: + if not borrowed: + conn.execute("BEGIN") + owned = True + if _style_extra_triggers(conn): + health.update(outcome="deferred", error_category="StyleTriggersUnsupported", pending_reason="unsupported_triggers") + return StyleObservation(None, None, False, health) + _style_source_ready(conn) + spec = style_layer_spec(conn) + state = read_layer_states(conn, (spec,))[spec.layer] + checkpoint = _read_layer_row(conn, spec.layer) + marker = conn.execute("SELECT value FROM metadata WHERE key='style_vec_hash_version'").fetchone() + hash_ready = marker == ("2",) + health["hash_ready"] = hash_ready + default = next((row[4] for row in conn.execute("PRAGMA main.table_info(entity_style_vectors)") + if row[1] == "accumulator_version"), "unsupported") + format_ready = default is None or default.strip(" ()'\"") in {"0", "1", "NULL", "null"} + health["format_ready"] = format_ready + ready = state.dependency.available + freshness = layer_freshness(state.source, state, state.dependency) + same_failure = (state.outcome in {"failed", "unavailable"} + and state.attempted_epoch == state.source.epoch and state.attempted_revision == state.source.revision + and state.attempted_dependency == state.dependency.key) + scheduled = _eligible(state, threshold, False) + # A terminal attempt owns its retry budget even without successful + # coverage or hash2. Readiness shortcuts cannot bypass that baseline. + eligible = scheduled if state.outcome in {"failed", "unavailable"} else ( + not ready or not hash_ready or not format_ready or state.successful_coverage != "complete" + or state.outcome == "running" or scheduled) + reason = (("tracking_repair" if not ready else "hash_migration" if not hash_ready else + "accumulator_pending" if not format_ready else "eligible") if eligible else + "attempt_unchanged" if same_failure else "current" if freshness == "current" else "awaiting_threshold") + health.update(available=ready and format_ready, outcome=state.outcome, + freshness=freshness if hash_ready and format_ready else "dependency_pending", + coverage=state.successful_coverage if ready and hash_ready and format_ready else "unverified", + error_category=_style_error_category(state.error_category or state.dependency.error_category + or (None if format_ready else "StyleAccumulatorUnsupported")), + output_count=state.output_count, pending_reason=reason) + if borrowed: + eligible = False + health.update(outcome="pending", pending_reason="pending_caller_commit") + except MaintenanceUnavailableError: + state = checkpoint = None + eligible = False + health.update(outcome="unavailable", error_category="StyleSourceUnavailable", pending_reason="source_unavailable") + except sqlite3.Error as error: + state = checkpoint = None + eligible = False + category = _style_sqlite_deferral(error) if isinstance(error, sqlite3.OperationalError) else None + health.update(outcome="deferred" if category else "failed", error_category=category or type(error).__name__, + pending_reason="inspection_failed") + finally: + if owned: + try: + conn.rollback() + except sqlite3.Error as error: + state = checkpoint = None + eligible = False + health.update(available=False, outcome="failed", freshness="unknown", coverage="unverified", + error_category=type(error).__name__, pending_reason="inspection_rollback_failed") + return StyleObservation(state, checkpoint, eligible, health) + + +def style_health(conn: sqlite3.Connection | None = None, coordinator: MaintenanceCoordinator | None = None, + *, pending_reason: str | None = None) -> dict: + health = observe_style(conn, pending_reason=pending_reason).health + if coordinator is not None: + previous, category = coordinator.style_observation() + current = (not health["pending_caller_commit"] and health["freshness"] == "current" + and health["coverage"] == "complete" and health["outcome"] in {"success", "success_empty"}) + if not current: + health["last_error"] = category or (previous.error_category if previous is not None else None) + return health + + +class _StyleOpenRejected(RuntimeError): + def __init__(self, result: LayerResult) -> None: + self.result = result + super().__init__(result.error_category) + + +def _style_unready_result(health: dict) -> LayerResult: + return LayerResult("style_vectors", "style_vectors", health["outcome"], health["output_count"], 0.0, + health["error_category"], False, health["coverage"], health["pending_caller_commit"]) + + +def _open_style_database(path: Path, cancel: threading.Event | None = None) -> sqlite3.Connection: + """Open an existing canonical file without general initialization or migrations.""" + def cancelled() -> bool: + return cancel is not None and cancel.is_set() + + def deferred(category: str) -> _StyleOpenRejected: + return _StyleOpenRejected(LayerResult("style_vectors", "style_vectors", "deferred", None, 0.0, + category, False, "unverified")) + + if cancelled(): + raise deferred("StyleCancelled") + if not isinstance(path, Path) or not path.is_absolute(): + raise ValueError("Style worker requires a canonical file path") + conn = None + progress = False + try: + conn = sqlite3.connect(path.as_uri() + "?mode=rw", uri=True) + conn.row_factory = None + for sql in (f"PRAGMA busy_timeout={DEFAULT_BUSY_TIMEOUT_MS}", "PRAGMA foreign_keys=ON", + "PRAGMA synchronous=NORMAL", "PRAGMA cache_size=-64000", "PRAGMA mmap_size=268435456"): + conn.execute(sql) + if _style_extra_triggers(conn): + raise deferred("StyleTriggersUnsupported") + try: + _style_source_ready(conn) + except MaintenanceUnavailableError as error: + raise _StyleOpenRejected(LayerResult("style_vectors", "style_vectors", "unavailable", None, 0.0, + "StyleSourceUnavailable", False, "unverified")) from error + observation = observe_style(conn) + if observation.state is None: + raise _StyleOpenRejected(_style_unready_result(observation.health)) + if cancelled(): + raise deferred("StyleCancelled") + if cancel is not None: + conn.set_progress_handler(lambda: int(cancelled()), 1000) + progress = True + try: + checked = conn.execute("PRAGMA quick_check(1)").fetchone() + finally: + if progress: + conn.set_progress_handler(None, 0) + progress = False + if cancelled(): + raise deferred("StyleCancelled") + if checked != ("ok",): + raise DatabaseOpenError("Database integrity check failed; restore a known-good backup before retrying") + return conn + except BaseException as error: + if conn is not None: + try: + if progress: + conn.set_progress_handler(None, 0) + if conn.in_transaction: + conn.rollback() + finally: + conn.close() + if isinstance(error, sqlite3.OperationalError): + category = _style_sqlite_deferral(error, cancelled=cancelled()) + if category: + raise deferred(category) from error + if isinstance(error, sqlite3.DatabaseError) and not isinstance(error, DatabaseOpenError): + message = str(error).lower() + if "i/o error" in message: + raise DatabaseOpenError("Database I/O failed; close all TrueMemory processes and retry") from error + raise DatabaseOpenError("Database could not be validated; check access or restore a known-good backup") from error + raise + + +def _validate_routed_style_source(conn: sqlite3.Connection) -> None: + try: + _style_source_ready(conn) + except MaintenanceUnavailableError as error: + raise _StyleAppendChanged("StyleSourceChanged") from error + + +def _prepare_routed_style_maintenance(conn: sqlite3.Connection) -> None: + """Validate canonical tracking under writer ownership before enrollment.""" + borrowed = conn.in_transaction + entered = rolled_back = False + + def confirmed_rollback() -> None: + nonlocal rolled_back + rolled_back = True + + transaction = (_borrowed_transaction(conn, on_rollback=confirmed_rollback) if borrowed else + _owned_transaction(conn, on_rollback=confirmed_rollback, immediate=True)) + try: + if _style_output_tracking_ready(conn): + return + with transaction: + entered = True + if borrowed: + conn.execute("UPDATE maintenance_layers SET layer=layer WHERE 0") + _validate_routed_style_source(conn) + prepare_style_maintenance(conn) + except sqlite3.OperationalError as error: + if (conn.in_transaction == borrowed and (not entered or rolled_back) + and _style_sqlite_deferral(error) == "StyleWriterBusy"): + raise LayerDeferredError("Style preparation writer busy", category="StyleWriterBusy") from error + raise + + +def run_routed_style(conn: sqlite3.Connection, *, force: bool = False, threshold: int = 25, + cancel: threading.Event | None = None, allow_caller_transaction: bool = False, + connection_owned: bool = False) -> LayerResult: + """Canonical engine route; explicit low-level style APIs retain their contract.""" + if cancel is not None and cancel.is_set(): + return LayerResult("style_vectors", "style_vectors", "deferred", None, 0.0, "StyleCancelled", False, + "unverified", conn.in_transaction) + with maintenance_owner(connection_database_path(conn)): + observation = observe_style(conn, threshold=threshold) + if observation.state is None: + return _style_unready_result(observation.health) + if conn.in_transaction and not allow_caller_transaction: + return _style_unready_result(observation.health) + # A wrong marker cannot leave a previously certified checkpoint current. + # Compare the complete old row under writer ownership; preserve failures. + old = observation.checkpoint + if (old is not None and old[2] in {"success", "success_empty"} + and (not observation.health["hash_ready"] or not observation.health["format_ready"] + or observation.state.successful_coverage != "complete")): + borrowed = conn.in_transaction + transaction = _borrowed_transaction if borrowed else _owned_transaction + rolled_back = False + + def confirmed_rollback() -> None: + nonlocal rolled_back + rolled_back = True + + try: + with transaction(conn, on_rollback=confirmed_rollback): + conn.execute("UPDATE maintenance_layers SET layer=layer WHERE 0") + if _style_extra_triggers(conn): + raise LayerDeferredError("Style triggers changed", category="StyleTriggersUnsupported") + try: + _style_source_ready(conn) + except MaintenanceUnavailableError as error: + raise LayerDeferredError("Canonical source readiness changed", category="StyleSourceChanged") from error + if _read_layer_row(conn, "style_vectors") != old: + raise LayerDeferredError("Style checkpoint changed", category="StyleSourceChanged") + conn.execute(_STYLE_OUTPUT_INVALIDATE_SQL) + except (sqlite3.OperationalError, LayerDeferredError) as error: + category = error.category if isinstance(error, LayerDeferredError) else _style_sqlite_deferral(error) + if not rolled_back or conn.in_transaction != borrowed or category is None: + raise + return LayerResult("style_vectors", "style_vectors", "deferred", None, 0.0, + category, False, "unverified", borrowed) + return run_style_maintenance(conn, force=force, threshold=threshold, cancel=cancel, + allow_caller_transaction=allow_caller_transaction, connection_owned=connection_owned, + _canonical_source=True) + + +def _style_append_metadata(conn: sqlite3.Connection) -> tuple: + dependency = style_layer_spec(conn).resolve_dependency() + if dependency.error_category == "StyleTriggersUnsupported": + raise _StyleAppendChanged("StyleTriggersUnsupported") + if not dependency.available: + raise _StyleAppendChanged("StyleMetadataChanged") + schema = tuple(tuple(row) for row in conn.execute("PRAGMA main.table_info(entity_style_vectors)")) + default = next((row[4] for row in schema if row[1] == "accumulator_version"), "unsupported") + if default is not None and default.strip(" ()'\"") not in {"0", "1", "NULL", "null"}: + raise _StyleAppendChanged("StyleAccumulatorUnsupported") + marker = conn.execute("SELECT value FROM metadata WHERE key='style_vec_hash_version'").fetchone() + if marker is None or marker[0] != "2": + raise _StyleAppendChanged("StyleHashPending") + return dependency, schema, tuple(marker) + + +def _style_checkpoint_projection(row: tuple, **changes: object) -> tuple: + fields = dict(zip(_LAYER_ROW_COLUMNS, row)) + fields.update(changes) + return tuple(fields[name] for name in _LAYER_ROW_COLUMNS) + + +def _style_invalidated_checkpoint(row: tuple) -> tuple: + return _style_checkpoint_projection( + row, outcome="pending", full_rebuild_required=1, successful_coverage="unverified", + attempted_epoch=None, attempted_revision=None, attempted_dependency=None, + attempted_insert_count=None, run_generation=None, error_category=None, + ) + + +class _StyleAppendSnapshot(NamedTuple): + connection: sqlite3.Connection + checkpoint: tuple | None + state: LayerState | None + error_category: str | None + failed: bool = False + metadata: tuple | None = None + + +def _valid_style_source(source: SourceRevision) -> bool: + return (isinstance(source.epoch, str) and bool(source.epoch) + and all(type(value) is int and value >= 0 for value in source[1:]) + and source.revision == source.insert_count + source.correction_count + and source.nonappend_revision <= source.revision) + + +def _style_append_error_category(error: Exception) -> str: + category = type(error).__name__[:64] + return category if re.fullmatch(r"[A-Za-z][A-Za-z0-9_]{0,63}", category) is not None else "Error" + + +def _capture_style_append(conn: sqlite3.Connection) -> _StyleAppendSnapshot: + """Freeze metadata before one source insert, under its existing writer.""" + if not conn.in_transaction: + raise RuntimeError("Style append capture requires the source transaction") + checkpoint = None + state = None + try: + checkpoint = _read_layer_row(conn, "style_vectors") + spec = style_layer_spec(conn) + state = read_layer_states(conn, (spec,))[spec.layer] + if state.dependency.error_category == "StyleTriggersUnsupported": + return _StyleAppendSnapshot(conn, checkpoint, state, "StyleTriggersUnsupported") + if (checkpoint is None or checkpoint[1] != state.dependency.builder_version + or state.outcome not in {"success", "success_empty"} + or state.successful_coverage != "complete" + or layer_freshness(state.source, state, state.dependency) != "current" + or type(state.output_count) is not int or state.output_count < 0 + or (state.outcome == "success_empty") != (state.output_count == 0) + or not _valid_style_source(state.source) + or (state.attempted_epoch, state.attempted_revision, state.attempted_dependency, + state.attempted_insert_count) != (state.source.epoch, state.source.revision, + state.dependency.key, state.source.insert_count)): + return _StyleAppendSnapshot(conn, checkpoint, state, "StyleCoveragePending") + metadata = _style_append_metadata(conn) + except _StyleAppendChanged as error: + return _StyleAppendSnapshot(conn, checkpoint, state, error.category) + except (sqlite3.Error, MaintenanceUnavailableError, StopIteration) as error: + if not conn.in_transaction: + raise _LayerRollbackFailed("Style capture lost the source transaction") from error + category = _style_sqlite_deferral(error) if isinstance(error, sqlite3.OperationalError) else None + return _StyleAppendSnapshot(conn, checkpoint, state, category or _style_append_error_category(error), category is None) + return _StyleAppendSnapshot(conn, checkpoint, state, None, metadata=metadata) + + +@contextmanager +def _style_append_savepoint(conn: sqlite3.Connection) -> Iterator[None]: + """Keep source writes when style fails, only after complete savepoint cleanup.""" + name = "truememory_style_coverage_" + uuid.uuid4().hex + conn.execute(f"SAVEPOINT {name}") + try: + yield + conn.execute(f"RELEASE {name}") + except BaseException: + if not conn.in_transaction: + raise _LayerRollbackFailed("Style append lost the source transaction") + try: + conn.execute(f"ROLLBACK TO {name}") + conn.execute(f"RELEASE {name}") + except BaseException as error: + raise _LayerRollbackFailed("Style append rollback or release failed") from error + raise + + +def _publish_style_append( + conn: sqlite3.Connection, previous: _StyleAppendSnapshot, message_id: int, *, + sender: str, content: str, directive: bool, precomputed: bool, publish: Callable[[], None], +) -> LayerResult: + """Advance one proven append without committing or rebuilding for its caller.""" + if previous.connection is not conn or not conn.in_transaction: + raise RuntimeError("Style append publication requires its original source transaction") + state = previous.state + + def pending(category: str, *, failed: bool = False) -> LayerResult: + # Keep a prior failed attempt's retry baseline. A newly missed append + # supersedes successful/running provenance, never a newer invalidation. + if previous.checkpoint is not None and previous.checkpoint[2] in {"success", "success_empty", "running"}: + try: + with _style_append_savepoint(conn): + if _style_extra_triggers(conn): + category = "StyleTriggersUnsupported" + elif _read_layer_row(conn, "style_vectors") == previous.checkpoint: + conn.execute(_STYLE_OUTPUT_INVALIDATE_SQL) + expected = _style_invalidated_checkpoint(previous.checkpoint) + # The invalidation itself may invoke SQL triggers. Set + # its category only if every field still belongs to it. + if _read_layer_row(conn, "style_vectors") == expected: + predicate = " AND ".join(name + " IS ?" for name in _LAYER_ROW_COLUMNS) + conn.execute("UPDATE maintenance_layers SET error_category=? WHERE " + predicate, + (category, *expected)) + except _LayerRollbackFailed: + raise + except sqlite3.Error as error: + if not conn.in_transaction: + raise _LayerRollbackFailed("Style pending report lost the source transaction") from error + category, failed = _style_append_error_category(error), True + return LayerResult("style_vectors", "style_vectors", "failed" if failed else "deferred", + state.output_count if state is not None else None, 0.0, category, + False, "unverified", True) + + if previous.error_category is not None: + return pending(previous.error_category, failed=previous.failed) + try: + if _read_layer_row(conn, "style_vectors") != previous.checkpoint: + return pending("StyleCoverageChanged") + if _style_append_metadata(conn) != previous.metadata: + return pending("StyleMetadataChanged") + source = read_source_revision(conn) + before = state.source + if (not _valid_style_source(source) or source.epoch != before.epoch + or source.revision != before.revision + 1 or source.insert_count != before.insert_count + 1 + or source.correction_count != before.correction_count + or source.nonappend_revision != before.nonappend_revision + or type(message_id) is not int or message_id <= before.max_seen_message_id + or source.max_seen_message_id != message_id): + return pending("StyleAppendUnproven") + row = conn.execute("SELECT sender,content,directive FROM messages WHERE id=?", (message_id,)).fetchone() + if (not isinstance(sender, str) or not isinstance(content, str) or not content.strip() + or row is None or tuple(row) != (sender, content, int(bool(directive)))): + return pending("StyleAppendUnproven") + included = bool(sender) and not directive + if included and not precomputed: + return pending("StylePrecomputeFailed", failed=True) + with _style_append_savepoint(conn): + count = state.output_count + if included: + exists = conn.execute("SELECT 1 FROM entity_style_vectors WHERE entity=?", (sender.lower(),)).fetchone() + publish() + count += int(exists is None) + expected = _style_invalidated_checkpoint(previous.checkpoint) if included else previous.checkpoint + if _read_layer_row(conn, "style_vectors") != expected: + raise _StyleAppendChanged("StyleCoverageChanged") + if _style_append_metadata(conn) != previous.metadata: + raise _StyleAppendChanged("StyleMetadataChanged") + if read_source_revision(conn) != source: + raise LayerDeferredError("Style source changed during append", category="StyleSourceChanged") + record_layer_success_in_transaction(conn, layer="style_vectors", dependency=state.dependency, + source=source, output_count=count) + success = _style_checkpoint_projection( + previous.checkpoint, builder_version=state.dependency.builder_version, + outcome="success" if count else "success_empty", successful_epoch=source.epoch, + successful_revision=source.revision, successful_dependency=state.dependency.key, + successful_coverage="complete", attempted_epoch=source.epoch, attempted_revision=source.revision, + attempted_dependency=state.dependency.key, attempted_insert_count=source.insert_count, + full_rebuild_required=0, output_count=count, run_generation=None, error_category=None, + ) + if _read_layer_row(conn, "style_vectors") != success: + raise _StyleAppendChanged("StyleCoverageChanged") + if _style_append_metadata(conn) != previous.metadata or read_source_revision(conn) != source: + raise _StyleAppendChanged("StyleMetadataChanged") + except _LayerRollbackFailed: + raise + except Exception as error: + if not conn.in_transaction: + raise _LayerRollbackFailed("Style append lost the source transaction") from error + category = (error.category if isinstance(error, (LayerDeferredError, _StyleAppendChanged)) else + _style_sqlite_deferral(error) if isinstance(error, sqlite3.OperationalError) else None) + return pending(category or _style_append_error_category(error), failed=category is None) + return LayerResult("style_vectors", "style_vectors", "success" if count else "success_empty", + count, 0.0, None, True, "complete", True) + + +def style_layer_spec( + conn: sqlite3.Connection, *, cancel: threading.Event | None = None, connection_owned: bool = False, + _canonical_source: bool = False, +) -> LayerSpec: + """Describe private style work without installing schema or reading profiles.""" + if type(connection_owned) is not bool: + raise ValueError("Connection ownership opt-in must be a boolean") + parameters = { + "accumulator_version": 1, "hash_version": 2, "dimension": 256, + "ngrams": [3, 4, 5], "sum_format": "<256d", + } + + def resolve_dependency() -> LayerDependency: + if _style_extra_triggers(conn): + return make_layer_dependency(1, parameters, available=False, + error_category="StyleTriggersUnsupported")._replace(deferred=True) + ready = _style_output_tracking_ready(conn) + return make_layer_dependency(1, parameters, available=ready, + error_category=None if ready else "StyleTrackingUnprepared") + + def check_cancel() -> None: + if cancel is not None and cancel.is_set(): + raise LayerDeferredError("Style work cancelled", category="StyleCancelled") + + @contextmanager + def execution_guard(connection: sqlite3.Connection, owned: bool) -> Iterator[None]: + style = importlib.import_module("truememory.personality_style_vec") + progress = connection_owned and owned and cancel is not None + interrupted_by_callback = False + + def progress_handler() -> int: + nonlocal interrupted_by_callback + if cancel.is_set(): + interrupted_by_callback = True + return 1 + return 0 + + if progress: + connection.set_progress_handler(progress_handler, 1000) + try: + check_cancel() + yield + except _LayerCancelled as error: + raise LayerDeferredError("Style work cancelled", category="StyleCancelled") from error + except style._StyleSourceChanged as error: + raise LayerDeferredError("Style source changed", category="StyleSourceChanged") from error + except sqlite3.OperationalError as error: + category = _style_sqlite_deferral(error, cancelled=cancel is not None and cancel.is_set()) + if category == "StyleCancelled": + code = getattr(error, "sqlite_errorcode", None) + native_interrupt = (isinstance(code, int) and (code & 255) == 9) or ( + code is None and not hasattr(sqlite3, "SQLITE_INTERRUPT") and str(error) == "interrupted" + ) + raise _StyleSQLInterrupted(automatic_rollback=bool( + progress and interrupted_by_callback and native_interrupt and not connection.in_transaction + )) from error + if category == "StyleWriterBusy": + raise LayerDeferredError("Style writer busy", category="StyleWriterBusy") from error + raise + finally: + # Exit before the runner rolls back or commits. Borrowed handlers + # cannot be recovered through SQLite's API, so never replace them. + if progress: + connection.set_progress_handler(None, 0) + + def compatible(connection: sqlite3.Connection) -> None: + # A custom future schema can be empty. Do not downgrade its declared + # accumulator format merely because no row demonstrates it yet. + default = next(row[4] for row in connection.execute("PRAGMA table_info(entity_style_vectors)") + if row[1] == "accumulator_version") + if default is not None: + literal = default.strip(" ()'\"") + if literal not in {"0", "1", "NULL", "null"}: + raise StyleAccumulatorUnsupported("Unsupported style accumulator default") + if connection.execute( + "SELECT 1 FROM entity_style_vectors WHERE accumulator_version > 1 LIMIT 1" + ).fetchone(): + raise StyleAccumulatorUnsupported("Unsupported style accumulator version") + + def build(connection: sqlite3.Connection) -> object: + style = importlib.import_module("truememory.personality_style_vec") + source = read_source_revision(connection) + schema, query, rows = style._capture_style_source(connection, check_cancel) + result, stored_rows = style._compute_entity_style_vectors(rows, check_cancel=check_cancel) + check_cancel() + # The runner owns final commit. Upgrade before both format and source + # validation; a stale reader cannot replace a newer generation. + connection.execute("UPDATE messages SET sender=sender WHERE 0") + if _canonical_source: + try: + _validate_routed_style_source(connection) + except _StyleAppendChanged as error: + raise LayerDeferredError("Canonical source readiness changed", category=error.category) from error + if _style_extra_triggers(connection): + raise LayerDeferredError("Style triggers prevent tracked publication", category="StyleTriggersUnsupported") + style._publish_style_vectors(connection, schema, query, rows, stored_rows, + check_cancel=check_cancel, before_replace=compatible) + if read_source_revision(connection) != source: + raise style._StyleSourceChanged("Style source changed before publication") + connection.execute("INSERT INTO metadata(key,value) VALUES ('style_vec_hash_version','2') " + "ON CONFLICT(key) DO UPDATE SET value=excluded.value") + return result + + return LayerSpec("style_vectors", "style_vectors", resolve_dependency, build, + lambda connection: connection.execute("SELECT count(*) FROM entity_style_vectors").fetchone()[0], + connection=conn, execution_guard=execution_guard) + + +def run_style_maintenance( + conn: sqlite3.Connection, *, force: bool = False, threshold: int = 25, + cancel: threading.Event | None = None, allow_caller_transaction: bool = False, + connection_owned: bool = False, _canonical_source: bool = False, +) -> LayerResult: + """Explicit tracked style rebuild, separate from all eight public adapters.""" + if type(allow_caller_transaction) is not bool: + raise ValueError("Caller transaction opt-in must be a boolean") + if type(connection_owned) is not bool: + raise ValueError("Connection ownership opt-in must be a boolean") + borrowed = conn.in_transaction + if borrowed and not allow_caller_transaction: + raise RuntimeError("Maintenance runner requires a clean transaction boundary") + if type(threshold) is not int or threshold < 1: + raise ValueError("Maintenance threshold must be a positive integer") + with maintenance_owner(connection_database_path(conn)): + started = time.monotonic() + + def preparation_failure(error: sqlite3.OperationalError) -> None: + if _style_sqlite_deferral(error) == "StyleWriterBusy": + raise LayerDeferredError("Style preparation writer busy", category="StyleWriterBusy") from error + + try: + if _canonical_source: + _prepare_routed_style_maintenance(conn) + else: + prepare_style_maintenance(conn, _on_safe_failure=preparation_failure) + except _StyleAppendChanged as error: + return LayerResult("style_vectors", "style_vectors", "deferred", None, + time.monotonic() - started, error.category, False, "unverified", borrowed) + except LayerDeferredError as error: + return LayerResult("style_vectors", "style_vectors", "deferred", None, + time.monotonic() - started, error.category, False, "unverified", borrowed) + routed = {"_canonical_source": True} if _canonical_source else {} + spec = style_layer_spec(conn, cancel=cancel, connection_owned=connection_owned, **routed) + try: + results = run_layers(conn, (spec,), force=force, threshold=threshold, cancel=cancel, + allow_caller_transaction=allow_caller_transaction, **routed) + if results: + return results[0] + observed = _read_layer_run_baseline(conn, (spec,), **routed) + except _StyleAppendChanged as error: + if not _canonical_source or error.category not in { + "StyleSourceChanged", "StyleTrackingUnprepared", "StyleTriggersUnsupported", + "StyleMetadataChanged", "StyleAccumulatorUnsupported", "StyleHashPending", + }: + raise + if conn.in_transaction != borrowed: + raise _LayerRollbackFailed("Canonical readiness rejection changed transaction ownership") from error + return LayerResult("style_vectors", "style_vectors", "deferred", None, + time.monotonic() - started, error.category, False, "unverified", borrowed) + if observed is None: + return _style_baseline_busy_result(spec, borrowed) + state = observed[spec.layer] + return LayerResult(spec.layer, spec.result_key, "deferred", state.output_count, 0.0, + "StyleCancelled", False, state.successful_coverage, borrowed) + + +def nonvector_layer_specs() -> tuple[LayerSpec, ...]: + """The original six standard-library adapters, with unchanged defaults.""" + definitions = ( + ("summaries", "build_summaries", "consolidation", "build_summaries", + "SELECT count(*) FROM summaries WHERE period IN ('monthly','entity_monthly')", {}), + ("structured_facts", "structured_facts", "consolidation", "build_structured_facts", + "SELECT count(*) FROM summaries WHERE period='structured_fact'", {}), + ("contradictions", "detect_contradictions", "consolidation", "detect_contradictions", + "SELECT count(*) FROM fact_timeline", {}), + ("surprise", "build_surprise_index", "predictive", "build_surprise_index", + "SELECT count(*) FROM surprise_scores", {}), + ("episodes", "detect_episodes", "temporal", "detect_episodes", "SELECT count(*) FROM episodes", {"gap_hours": 6}), + ("landmarks", "detect_landmarks", "temporal", "detect_landmark_events", + "SELECT count(*) FROM landmark_events", {}), + ) + specs = [] + for layer, result_key, module_name, function_name, count_sql, parameters in definitions: + def resolve(module_name: str = module_name, function_name: str = function_name, + parameters: dict = parameters) -> LayerDependency: + try: + getattr(importlib.import_module("truememory." + module_name), function_name) + except (ImportError, AttributeError) as error: + return make_layer_dependency(1, parameters, available=False, error_category=type(error).__name__) + return make_layer_dependency(1, parameters) + + def build(conn: sqlite3.Connection, module_name: str = module_name, + function_name: str = function_name, parameters: dict = parameters) -> object: + return getattr(importlib.import_module("truememory." + module_name), function_name)(conn, **parameters) + + def count(conn: sqlite3.Connection, sql: str = count_sql) -> int: + return conn.execute(sql).fetchone()[0] + + specs.append(LayerSpec(layer, result_key, resolve, build, count)) + return tuple(specs) + + +def _installed_dependency(module: str, distribution: str) -> str | None: + """Inspect package metadata without importing a native dependency.""" + try: + if importlib.util.find_spec(module) is None: + return None + return importlib.metadata.version(distribution) + except (ImportError, ValueError, importlib.metadata.PackageNotFoundError): + return None + + +def _cluster_schedule_dependency( + conn: sqlite3.Connection, *, versions: dict[str, str | None] | None = None, +) -> LayerDependency: + from truememory.tier_switch.runtime import _read_selection, _read_policy + if _read_selection(conn) is not None or _read_policy(conn) is not None: + return LayerDependency("", 1, False, "SelectedVectorLayerUnsupported", deferred=True) + parameters = {"min_cluster_size": 10, "min_samples": 5, "metric": "euclidean", + "cluster_selection_method": "eom"} + if versions is None: + versions = {module: _installed_dependency(module, distribution) for module, distribution in ( + ("numpy", "numpy"), ("hdbscan", "hdbscan"), ("sqlite_vec", "sqlite-vec"), + )} + parameters["versions"] = versions + + def unavailable(category: str) -> LayerDependency: + return make_layer_dependency(1, parameters, available=False, error_category=category) + + if any(version is None for version in versions.values()): + return unavailable("DependencyMissing") + vector = sys.modules.get("truememory.vector_search") + if vector is None: + return unavailable("VectorRuntimeUnavailable") + if not vector._lock.acquire(blocking=False): + # There is no safe stable-key observation while the model is owned. + # This ephemeral value must never become a durable version or attempt. + return LayerDependency("", 1, False, "ModelBusy", deferred=True) + owned = not conn.in_transaction + try: + if owned: + conn.execute("BEGIN") + group = vector._active_tier_group() + table = vector._active_vec_table(conn) + schema = [tuple(row) for row in conn.execute( + "SELECT type,name,rootpage,sql FROM sqlite_master WHERE name=?", (table,), + )] + registry = [tuple(row) for row in conn.execute( + "SELECT tier_group,vec_table,sep_table,model_name,embedding_dim " + "FROM vector_cache_registry WHERE tier_group=?", (group,), + )] + metadata = [tuple(row) for row in conn.execute( + "SELECT key,value FROM metadata WHERE key IN ('embed_model','embed_dim',?,?) ORDER BY key", + (f"vec_build_state:{table}", f"vec_source_v1:{table}"), + )] + # Timestamp/count rewrites do not mean a new embedding space. Keep + # them in the publication fence, not in the durable scheduling key. + identity = [vector.EMBEDDING_MODEL, vector._embedding_dim, group, table, schema, registry, metadata] + try: + encoded = json.dumps(identity, sort_keys=True, separators=(",", ":"), allow_nan=False).encode() + except (TypeError, ValueError): + return unavailable("VectorMetadataInvalid") + parameters["identity_sha256"] = hashlib.sha256(encoded).hexdigest() + if not schema: + return unavailable("VectorIndexMissing") + if any("USING VEC0" in (row[3] or "").upper() for row in schema): + if "vec0" not in {row[0] for row in conn.execute("PRAGMA module_list")}: + return unavailable("VectorExtensionUnavailable") + if (f"vec_build_state:{table}", "in_progress") in metadata: + return unavailable("VectorRebuildInProgress") + descriptor = dict(metadata).get(f"vec_source_v1:{table}") + if descriptor is not None: + try: + manifest = json.loads(descriptor) + except (TypeError, ValueError): + return unavailable("VectorManifestInvalid") + if (not isinstance(manifest, dict) or type(manifest.get("version")) is not int + or manifest["version"] != 1 or type(manifest.get("complete")) is not bool): + return unavailable("VectorManifestInvalid") + if not manifest["complete"]: + return unavailable("VectorRebuildIncomplete") + return make_layer_dependency(1, parameters) + except sqlite3.OperationalError: + return unavailable("VectorSchemaUnavailable") + finally: + try: + if owned and conn.in_transaction: + conn.rollback() + finally: + vector._lock.release() + + +def _cluster_module() -> ModuleType: + try: + return importlib.import_module("truememory.clustering") + except ImportError as error: + raise LayerUnavailableError("Clustering dependency import failed") from error + + +def _cluster_guard(conn: sqlite3.Connection) -> AbstractContextManager[Callable[[], None]]: + module = _cluster_module() + guard = module.cluster_publication_guard(conn) + + @contextmanager + def hold() -> Iterator[Callable[[], None]]: + try: + with guard as validate: + yield validate + except module.ClusterModelBusyError as error: + raise LayerDeferredError("Embedding runtime is temporarily busy") from error + + return hold() + + +def _borrowed_cluster_guard(conn: sqlite3.Connection) -> AbstractContextManager[Callable[[], None]]: + """Fence a borrowed RELEASE, never the caller's later outer COMMIT.""" + module = _cluster_module() + vector = sys.modules.get("truememory.vector_search") + if vector is None: + raise LayerUnavailableError("Embedding runtime is unavailable") + if not vector._lock.acquire(blocking=False): + raise LayerDeferredError("Embedding runtime is temporarily busy") + try: + expected = module._cluster_dependency_identity(conn, vector) + finally: + vector._lock.release() + + @contextmanager + def hold() -> Iterator[Callable[[], None]]: + conn.execute("UPDATE messages SET id=id WHERE 0") + if not vector._lock.acquire(blocking=False): + raise LayerDeferredError("Embedding runtime is temporarily busy") + try: + def validate() -> None: + if module._cluster_dependency_identity(conn, vector) != expected: + raise MaintenanceUnavailableError("Clustering dependency changed before caller savepoint release") + yield validate + finally: + vector._lock.release() + + return hold() + + +def _build_clusters(conn: sqlite3.Connection) -> int: + module = _cluster_module() + try: + return module.cluster_messages(conn) + except module.ClusterModelBusyError as error: + raise LayerDeferredError("Embedding runtime is temporarily busy") from error + except ImportError as error: + raise LayerUnavailableError("Clustering dependency import failed") from error + + +def _build_dunbar(conn: sqlite3.Connection) -> object: + # run_layers supplies the read snapshot, including primary selection. + row = conn.execute( + "SELECT sender,COUNT(*) AS cnt FROM messages WHERE sender != '' AND sender IS NOT NULL " + "GROUP BY sender ORDER BY cnt DESC LIMIT 1" + ).fetchone() + primary = row[0] if row and row[0] and row[0].strip() else None + return importlib.import_module("truememory.personality").build_dunbar_hierarchy(conn, primary_entity=primary) + + +def _dunbar_coverage(conn: sqlite3.Connection) -> str: + coverage = importlib.import_module("truememory.personality").read_dunbar_coverage(conn) + return {"managed_only": "complete", "partial": "legacy_contacts_unowned", "untracked": "unverified"}[coverage["status"]] + + +def all_layer_specs(conn: sqlite3.Connection) -> tuple[LayerSpec, ...]: + """Eight persisted adapters; no imports of native packages or model loads. + + These adapters are bound to this connection. Vector users supply sqlite-vec + and the initialized vector_search runtime. The cheap dependency resolver + reports unavailable otherwise. Preferences remain a separate nonpersisted call. + """ + specs = {spec.layer: spec for spec in nonvector_layer_specs()} + + def dunbar_dependency() -> LayerDependency: + try: + module = importlib.import_module("truememory.personality") + getattr(module, "build_dunbar_hierarchy") + getattr(module, "read_dunbar_coverage") + except (ImportError, AttributeError) as error: + return make_layer_dependency(1, available=False, error_category=type(error).__name__) + return make_layer_dependency(1) + + specs["clusters"] = LayerSpec( + "clusters", "cluster_messages", lambda: _cluster_schedule_dependency(conn), _build_clusters, + lambda connection: connection.execute("SELECT count(*) FROM cluster_centroids").fetchone()[0], + _cluster_guard, lambda _conn: "vector_generation_unverified", _borrowed_cluster_guard, + conn, + ) + specs["dunbar"] = LayerSpec( + "dunbar", "dunbar_hierarchy", dunbar_dependency, _build_dunbar, + lambda connection: importlib.import_module("truememory.personality").read_dunbar_coverage(connection)["managed_rows"], + read_coverage=_dunbar_coverage, + ) + return tuple(specs[name] for name in ( + "clusters", "summaries", "contradictions", "structured_facts", "surprise", "episodes", "landmarks", "dunbar", + )) + + +SCHEDULED_PREFERENCES_UNAVAILABLE = "UNAVAILABLE (ScheduledPreferencesUnsupported)" + + +class MaintenanceReport(NamedTuple): + results: tuple[LayerResult, ...] + preferences: str + style_result: LayerResult | None = None + + +def _dependency_parameters(dependency: LayerDependency) -> dict | None: + if dependency.deferred: + return None + return json.loads(dependency.key)["parameters"] + + +def engine_layer_specs( + conn: sqlite3.Connection, coordinator: MaintenanceCoordinator | None, *, evidence: tuple | None = None, +) -> tuple[LayerSpec, ...]: + """Small live probes plus one bounded worker-capability evidence record.""" + if evidence is None: + if coordinator is None: + raise ValueError("Maintenance capability evidence is required") + evidence = coordinator.capability_snapshot() + _, versions, failure = evidence + + def resolve() -> LayerDependency: + dependency = _cluster_schedule_dependency(conn, versions=dict(versions)) + if (failure is not None and not dependency.deferred + and _dependency_parameters(dependency) == _dependency_parameters(failure)): + return failure + return dependency + + return tuple(spec._replace(resolve_dependency=resolve) if spec.layer == "clusters" else spec + for spec in all_layer_specs(conn)) + + +def _clustering_error_category(value: str | None) -> str | None: + if value is None: + return None + return value if re.fullmatch(r"[A-Za-z][A-Za-z0-9_]{0,63}", value) else "UnknownError" + + +def _missing_clustering_dependencies() -> tuple[str, ...]: + """Only an absent top-level module warrants missing-package guidance.""" + missing = [] + for module, distribution in (("numpy", "numpy"), ("hdbscan", "hdbscan"), ("sqlite_vec", "sqlite-vec")): + try: + absent = importlib.util.find_spec(module) is None + except (ImportError, ValueError): + absent = False + if absent: + missing.append(distribution) + return tuple(missing) + + +def _clustering_install_guidance(missing: tuple[str, ...]) -> str | None: + if "hdbscan" in missing: + return "Install the optional clustering extra: pip install 'truememory[clustering]'." + return None + + +def clustering_health( + conn: sqlite3.Connection | None = None, coordinator: MaintenanceCoordinator | None = None, + *, pending_reason: str | None = None, +) -> dict: + """Inspect existing evidence without opening a connection or loading models. + + Callers serialize their existing connection. Package probes inspect only + top-level specs and distribution metadata; installed packages do not prove + that their native imports or the database's published clusters are usable. + """ + health = { + "status": "degraded", "state": "unknown", "availability": "unknown", "outcome": None, + "freshness": "unknown", "coverage": "unverified", "output_count": None, + "last_error": None, "dependency_error": None, "missing_dependencies": [], + "process_error": None, + "guidance": None, "pending_reason": pending_reason or "not_connected", + "pending_caller_commit": False, + } + owned = False + try: + missing = _missing_clustering_dependencies() + versions = tuple((module, None if distribution in missing else _installed_dependency(module, distribution)) + for module, distribution in ( + ("numpy", "numpy"), ("hdbscan", "hdbscan"), ("sqlite_vec", "sqlite-vec"), + )) + health["missing_dependencies"] = list(missing) + health["guidance"] = _clustering_install_guidance(missing) + if missing or any(version is None for _, version in versions): + health.update(state="degraded", availability="unavailable", dependency_error="DependencyMissing") + elif pending_reason is not None: + health["state"] = "deferred" + if conn is None or pending_reason is not None: + return health + health["pending_reason"] = None + health["pending_caller_commit"] = conn.in_transaction + process_failure = coordinator.clustering_failure() if coordinator is not None else None + if process_failure is not None: + health["process_error"] = process_failure[1] + owned = not conn.in_transaction + if owned: + conn.execute("BEGIN") + failure = coordinator.capability_snapshot()[2] if coordinator is not None else None + spec = next(spec for spec in engine_layer_specs(conn, coordinator, evidence=(0, versions, failure)) + if spec.layer == "clusters") + state = read_layer_states(conn, (spec,))["clusters"] + dependency = state.dependency + freshness = layer_freshness(state.source, state, dependency) + health.update( + availability="deferred" if dependency.deferred else "available" if dependency.available else "unavailable", + outcome=state.outcome, freshness=freshness, coverage=state.successful_coverage, + output_count=state.output_count, last_error=_clustering_error_category(state.error_category), + dependency_error=_clustering_error_category(dependency.error_category), + ) + if dependency.deferred: + health.update(state="deferred", pending_reason="model_busy") + elif not dependency.available or state.outcome in {"failed", "unavailable"} or process_failure is not None: + health["state"] = "degraded" + elif conn.in_transaction and not owned: + health.update(state="pending", pending_reason="pending_caller_commit") + elif freshness != "current" or state.outcome not in {"success", "success_empty"}: + health.update(state="pending", pending_reason="maintenance_pending") + elif state.successful_coverage != "complete": + health.update(state="degraded", pending_reason="coverage_unverified") + else: + health.update(state="ready", status="ok") + return health + except Exception as error: + health.update(state="unknown", last_error=_clustering_error_category(type(error).__name__), + pending_reason="inspection_unavailable") + return health + finally: + if owned and conn is not None: + try: + if conn.in_transaction: + conn.rollback() + except sqlite3.Error as error: + health.update(status="degraded", state="unknown", + last_error=_clustering_error_category(type(error).__name__), + pending_reason="inspection_rollback_failed") + + +def _prepare_worker_extensions(conn: sqlite3.Connection, coordinator: MaintenanceCoordinator) -> tuple: + coordinator.refresh_capabilities(reset_extension=False) + epoch, versions, previous_failure = coordinator.capability_snapshot() + failed = False + enabled = False + try: + if dict(versions).get("sqlite_vec") is None: + failed = True + else: + module = importlib.import_module("sqlite_vec") + conn.enable_load_extension(True) + enabled = True + module.load(conn) + except Exception: + failed = True + finally: + if enabled: + conn.enable_load_extension(False) + dependency = _cluster_schedule_dependency(conn, versions=dict(versions)) + failure = previous_failure if failed else None + if failed and not dependency.deferred: + parameters = _dependency_parameters(dependency) + if parameters is not None and "identity_sha256" in parameters: + failure = make_layer_dependency(dependency.builder_version, parameters, + available=False, error_category="VectorExtensionUnavailable") + elif dependency.error_category == "VectorExtensionUnavailable": + failure = dependency + coordinator._publish_extension_evidence(epoch, failure) + return epoch, versions, failure + + +@contextmanager +def _maintenance_completion(conn: sqlite3.Connection, allow_caller_transaction: bool) -> Iterator[None]: + """Validate successful owned work before its connection mutation lock leaves.""" + if type(allow_caller_transaction) is not bool: + raise ValueError("Caller transaction opt-in must be a boolean") + borrowed = conn.in_transaction + if borrowed and not allow_caller_transaction: + raise RuntimeError("Maintenance runner requires a clean transaction boundary") + yield + if not borrowed and conn.in_transaction: + conn.rollback() + raise RuntimeError("Maintenance work left an unfinished transaction") + + +def run_engine_maintenance( + conn: sqlite3.Connection, coordinator: MaintenanceCoordinator, *, threshold: int = 25, + force: bool = False, cancel: threading.Event | None = None, + prepare_extensions: bool = True, allow_caller_transaction: bool = False, + include_layers: bool = True, include_style: bool = False, connection_owned: bool = False, + _connection_lock: object | None = None, deadline: float | None = None, +) -> MaintenanceReport: + """Use only the supplied owned worker/original explicitly borrowed handle.""" + if cancel is not None and cancel.is_set(): + # Keep this cancellation snapshot terminal even if another caller + # clears the event before the optional style result is constructed. + style = (LayerResult("style_vectors", "style_vectors", "deferred", None, 0.0, + "StyleCancelled", False, "unverified", conn.in_transaction) + if include_style else None) + return MaintenanceReport((), "CANCELLED", style) + from truememory.tier_switch.runtime import _connection_read + with _connection_read(conn, _connection_lock, deadline, cancel): + path = connection_database_path(conn) + with maintenance_owner(path), _maintenance_embedding_scope( + conn, include_layers, cancel, connection_lock=_connection_lock, deadline=deadline, + ) as supported, _connection_read(conn, _connection_lock, deadline, cancel), \ + _maintenance_completion(conn, allow_caller_transaction): + style_result = None + if include_style: + style_result = run_routed_style(conn, force=force, threshold=threshold, cancel=cancel, + allow_caller_transaction=allow_caller_transaction, connection_owned=connection_owned) + if not include_layers or (cancel is not None and cancel.is_set()): + return MaintenanceReport((), "CANCELLED" if cancel is not None and cancel.is_set() else + "SKIPPED (style-only)", style_result) + evidence = _prepare_worker_extensions(conn, coordinator) if prepare_extensions and supported else None + specs = (engine_layer_specs(conn, coordinator, evidence=evidence) if supported else + tuple(spec for spec in all_layer_specs(conn) if spec.layer != "clusters")) + results = run_layers(conn, specs, threshold=threshold, force=force, cancel=cancel, + allow_caller_transaction=allow_caller_transaction) + if not supported: + results = (_selected_cluster_deferred(conn), *results) + for result in results: + if result.layer == "clusters" and result.attempted: + # RELEASE only publishes into the caller's still-rollbackable + # transaction. It cannot clear a previously observed failure. + if not (result.pending_caller_commit and result.outcome in {"success", "success_empty"}): + coordinator.observe_clustering_outcome(result.outcome, result.error_category) + preferences = "SKIPPED (no maintenance attempt)" + if cancel is not None and cancel.is_set(): + preferences = "CANCELLED" + elif force or any(result.attempted for result in results): + preferences = SCHEDULED_PREFERENCES_UNAVAILABLE + return MaintenanceReport(results, preferences, style_result) + + +def maintenance_report_status(report: MaintenanceReport, cancel: threading.Event | None = None) -> tuple[str, str | None]: + if report.preferences == "CANCELLED" or cancel is not None and cancel.is_set(): + return "cancelled", "Cancelled" + results = report.results + ((report.style_result,) if report.style_result is not None else ()) + for outcome in ("failed", "unavailable", "deferred", "abandoned"): + for result in results: + if result.outcome == outcome: + return outcome, result.error_category + preferences_unsupported = report.preferences == SCHEDULED_PREFERENCES_UNAVAILABLE + if report.preferences.startswith(("ERROR", "UNAVAILABLE")) and not preferences_unsupported: + return "failed", "PreferencesUnavailable" + if any(result.pending_caller_commit or result.outcome not in {"success", "success_empty", "current"} for result in results): + return "pending", None + if preferences_unsupported or any(result.coverage != "complete" for result in results): + return "completed_with_limits", None + return "success", None + + +_MAINTENANCE_RESULT_KEYS = ( + "cluster_messages", "build_summaries", "detect_contradictions", "structured_facts", + "build_surprise_index", "detect_episodes", "detect_landmarks", "dunbar_hierarchy", "extract_preferences", +) + + +def maintenance_busy_result() -> dict[str, str]: + return dict.fromkeys(_MAINTENANCE_RESULT_KEYS, "BUSY (maintenance owner active)") + + +def format_maintenance_report(report: MaintenanceReport) -> dict[str, str]: + counts = {"clusters": "clusters", "structured_facts": "facts", "episodes": "episodes", + "landmarks": "events", "dunbar": "relationships"} + output = {} + for result in report.results: + if result.outcome in {"success", "success_empty"}: + prefix = f"{result.output_count} {counts[result.layer]} in " if result.layer in counts else "" + value = prefix + f"{result.elapsed_seconds:.3f}s" + if result.coverage != "complete": + value += " (" + result.coverage + ")" + else: + label = "ERROR" if result.outcome == "failed" else result.outcome.upper() + value = label + (" (" + result.error_category + ")" if result.error_category else "") + if result.pending_caller_commit: + value += " (pending caller commit)" + output[result.result_key] = value + output["extract_preferences"] = report.preferences + return output diff --git a/truememory/mcp_server.py b/truememory/mcp_server.py index a4288e39..a4c02eb8 100644 --- a/truememory/mcp_server.py +++ b/truememory/mcp_server.py @@ -30,6 +30,9 @@ import sys import threading import time +from collections.abc import Iterator +from contextlib import contextmanager +from typing import TYPE_CHECKING try: import resource as _resource_mod @@ -40,8 +43,13 @@ from mcp.server.fastmcp import FastMCP +from truememory.tier_switch.projection import CONFIG_WRITE_LOCK, ConfigFileLock + log = logging.getLogger(__name__) +if TYPE_CHECKING: + from truememory.tier_switch.runtime import RuntimeChild, ServingOperation + # --------------------------------------------------------------------------- # Config management # --------------------------------------------------------------------------- @@ -58,7 +66,7 @@ _config_cache_time: float = 0.0 # M7 (#504) — Serialize config writes from concurrent MCP handlers / tier-switch. -_config_write_lock = threading.Lock() +_config_write_lock = CONFIG_WRITE_LOCK # M-58 (#641) — Cross-process advisory lock for config writes. _config_write_lock # above only serializes threads inside ONE process; a separate `truememory-mcp`, @@ -68,60 +76,11 @@ _CONFIG_LOCK_PATH = _TRUEMEMORY_DIR / "config.json.lock" -class _config_file_lock: - """Context manager taking a cross-process exclusive lock on config writes. - - Reuses the same fcntl/msvcrt pattern as - ``tier_switch.manager.run_rebuild_sync`` (#641). Best-effort: if the lock - cannot be acquired (e.g. a filesystem without flock support) we proceed - anyway rather than fail the write — the in-process ``_config_write_lock`` - plus the atomic ``os.replace`` still bound the damage. Held in BLOCKING mode - (LK_LOCK / LOCK_EX) so concurrent writers serialize instead of failing. - """ - - def __init__(self) -> None: - self._fd = None +class _config_file_lock(ConfigFileLock): + """Preserve the old best-effort entry point and injectable config paths.""" - def __enter__(self): - try: - _CONFIG_LOCK_PATH.parent.mkdir(parents=True, exist_ok=True) - self._fd = open(_CONFIG_LOCK_PATH, "w") - if os.name == "nt": - import msvcrt - msvcrt.locking(self._fd.fileno(), msvcrt.LK_LOCK, 1) - else: - import fcntl - fcntl.flock(self._fd, fcntl.LOCK_EX) - except OSError: - # Could not open or lock — proceed unlocked (best effort). - if self._fd is not None: - try: - self._fd.close() - except OSError: - pass - self._fd = None - return self - - def __exit__(self, *exc): - if self._fd is None: - return False - try: - if os.name == "nt": - import msvcrt - try: - msvcrt.locking(self._fd.fileno(), msvcrt.LK_UNLCK, 1) - except OSError: - pass - else: - import fcntl - fcntl.flock(self._fd, fcntl.LOCK_UN) - finally: - try: - self._fd.close() - except OSError: - pass - self._fd = None - return False + def __init__(self, *, strict: bool = False) -> None: + super().__init__(_CONFIG_LOCK_PATH, strict=strict) class _ConfigShapeError(ValueError): @@ -732,7 +691,7 @@ def _get_deepsearch_llm_fn(): return None -def _resolve_deepsearch_llm(): +def _resolve_deepsearch_llm(served_tier: str | None = None): """Resolve the LLM function for deep search queries. Returns the deepsearch-specific override if configured, otherwise @@ -742,7 +701,7 @@ def _resolve_deepsearch_llm(): if ds_fn is not None: return ds_fn # Fall back to default behavior: Pro tier only - if _load_config().get("tier", "edge") == "pro": + if (served_tier if served_tier is not None else _load_config().get("tier", "edge")) == "pro": return _get_llm_fn() return None @@ -835,6 +794,8 @@ def _set_reranker(model_name: str): f"ImportError: {e} — reinstall truememory to restore reranker support" ) except Exception as e: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(e) _record_reranker_error(f"{type(e).__name__}: {e}") @@ -920,9 +881,9 @@ def _build_model_server_health() -> dict: def _build_health_payload() -> dict: """Return the `stats['health']` dict exposed by `truememory_stats`. - Each subsystem reports `{status, last_error, ...}`. ``status`` is one of - ``"ok"`` or ``"degraded"`` — the latter means a writer (F05 / F06 / F08) - stored a non-None error during the current process lifetime. + Every subsystem retains its ``ok`` / ``degraded`` status contract. + Clustering's separate ``state`` distinguishes unknown, pending and deferred + inspection; package presence never certifies a current published index. """ # Reranker — written by F06's _set_reranker. with _reranker_error_lock: @@ -948,8 +909,27 @@ def _build_health_payload() -> dict: eg_err = _encoding_gate_last_error eg_count = _encoding_gate_degradation_count + # Never construct Memory or open its database for this health observation. + try: + memory = _memory + if memory is None: + from truememory.maintenance import clustering_health + cluster_info = clustering_health() + else: + cluster_info = memory._engine.get_clustering_health() + except Exception as error: + category = type(error).__name__ + cluster_info = { + "status": "degraded", "state": "unknown", "availability": "unknown", "outcome": None, + "freshness": "unknown", "coverage": "unverified", "output_count": None, + "last_error": category if re.fullmatch(r"[A-Za-z][A-Za-z0-9_]{0,63}", category) else "UnknownError", + "dependency_error": None, "process_error": None, "missing_dependencies": [], "guidance": None, + "pending_reason": "inspection_unavailable", "pending_caller_commit": False, + } + return { "model_server": model_server_info, + "clustering": cluster_info, "reranker": { "status": "ok" if reranker_err is None else "degraded", "last_error": reranker_err, @@ -971,33 +951,67 @@ def _build_health_payload() -> dict: } -def _parallel_search(queries, user_id, internal_limit, llm_fn, output_limit): +@contextmanager +def _search_operation(*, reranker_id: str | None = None) -> Iterator[tuple[Memory, ServingOperation]]: + """Pin database policy before choosing reranking, HyDE or child searches.""" + from truememory.tier_switch.runtime import engine_serving_operation + + memory = _get_memory() + with engine_serving_operation(memory._engine, reranker_override=reranker_id) as operation: + yield memory, operation + + +def _parallel_search(queries, user_id, internal_limit, llm_fn, output_limit, *, + memory: Memory | None = None, operation: ServingOperation | None = None): """Run multiple agentic searches in parallel, merge and deduplicate.""" - db_path = _get_memory()._engine.db_path - - def _run_query(q): - # context manager ensures the sqlite connection is - # closed even if `KeyboardInterrupt` lands between construction - # and entry into the old explicit `try:` block. `Memory.__exit__` - # already handles close; this form is just interrupt-safe. - with Memory(path=db_path) as thread_m: - return thread_m.search_deep( - q, user_id=user_id, limit=internal_limit, llm_fn=llm_fn, - ) + if operation is None: + with _search_operation() as (memory, operation): + return _parallel_search(queries, user_id, internal_limit, llm_fn, output_limit, + memory=memory, operation=operation) + if memory is None: + raise ValueError("Parallel serving requires its admitted Memory instance") + db_path = memory._engine.db_path + opening_lock = threading.Lock() + + def _run_query(q: str, reservation: RuntimeChild) -> list[dict]: + try: + with Memory(path=db_path) as thread_m: + # Legacy handle creation owns nonblocking maintenance admission. + with opening_lock: + thread_m._engine._open_connection_handle() + with reservation.join(conn=thread_m._engine.conn): + return thread_m.search_deep( + q, user_id=user_id, limit=internal_limit, llm_fn=llm_fn, + ) + finally: + reservation.close() - with ThreadPoolExecutor(max_workers=min(len(queries), 5)) as pool: - futures = [pool.submit(_run_query, q) for q in queries] - merged = [] - seen_ids = set() - for f in futures: - try: - for r in f.result(timeout=60): - rid = r.get("id") - if rid not in seen_ids: - merged.append(r) - seen_ids.add(rid) - except Exception as e: - log.debug("parallel search query failed: %s", e) + reservations = [] + merged = [] + seen_ids = set() + try: + with ThreadPoolExecutor(max_workers=min(len(queries), 5)) as pool: + futures = [] + for q in queries: + child = operation.fork_child() + reservations.append(child) + futures.append(pool.submit(_run_query, q, child)) + for f in futures: + try: + for r in f.result(timeout=60): + rid = r.get("id") + if rid not in seen_ids: + merged.append(r) + seen_ids.add(rid) + except Exception as e: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(e) + log.debug("parallel search query failed: %s", e) + finally: + # Executor shutdown has joined running children before releasing any + # reservation whose task was never submitted or admitted. + for child in reservations: + child.close() merged.sort(key=lambda x: -x.get("score", 0)) return merged[:output_limit] @@ -1102,6 +1116,7 @@ def truememory_search( user_id: str = "", limit: int = 10, queries: list[str] | None = None, + include_temporal_resolution: bool = False, ) -> str: """Search memories using the full agentic retrieval pipeline (HyDE query expansion, cross-encoder reranking, multi-round retrieval). @@ -1115,6 +1130,8 @@ def truememory_search( user_id: Filter results to this user (optional). limit: Maximum number of results to return. queries: Optional list of queries (preferred over pipe-separated string). + include_temporal_resolution: Return an envelope with results and date/landmark + diagnostics, including when no memories match. Default returns a list. """ MAX_QUERY_LENGTH = 2000 if queries: @@ -1123,26 +1140,32 @@ def truememory_search( query = query[:MAX_QUERY_LENGTH] query_list = [q.strip() for q in query.split("|") if q.strip()] else: - return json.dumps([]) + return json.dumps({"results": [], "temporal_resolutions": []} if include_temporal_resolution else []) _touch_search_time() limit = max(1, min(limit, 200)) - _set_reranker(_current_reranker()) - llm_fn = _get_llm_fn() if _load_config().get("tier", "edge") == "pro" else None uid = user_id or None if not query_list: - return json.dumps([]) + return json.dumps({"results": [], "temporal_resolutions": []} if include_temporal_resolution else []) if len(query_list) > 10: query_list = query_list[:10] - if len(query_list) == 1: - m = _get_memory() - results = m.search_deep( - query_list[0], user_id=uid, limit=_SEARCH_INTERNAL_LIMIT, llm_fn=llm_fn, - ) - return json.dumps(results[:limit], indent=2) + with _search_operation() as (m, operation): + _set_reranker(operation.reranker_id) + llm_fn = _get_llm_fn() if operation.tier == "pro" else None + if len(query_list) == 1: + results = m.search_deep( + query_list[0], user_id=uid, limit=_SEARCH_INTERNAL_LIMIT, llm_fn=llm_fn, + ) + response = {"results": results[:limit], "temporal_resolution": + m.resolve_temporal_query(query_list[0], user_id=uid)} if include_temporal_resolution else results[:limit] + return json.dumps(response, indent=2) - results = _parallel_search(query_list, uid, _SEARCH_INTERNAL_LIMIT, llm_fn, limit) - return json.dumps(results, indent=2) + results = _parallel_search(query_list, uid, _SEARCH_INTERNAL_LIMIT, llm_fn, limit, + memory=m, operation=operation) + response = {"results": results, "temporal_resolutions": [ + {"query": q, "resolution": m.resolve_temporal_query(q, user_id=uid)} for q in query_list + ]} if include_temporal_resolution else results + return json.dumps(response, indent=2) @mcp.tool(meta={"anthropic/alwaysLoad": True}) @@ -1152,6 +1175,7 @@ def truememory_search_deep( user_id: str = "", limit: int = 10, queries: list[str] | None = None, + include_temporal_resolution: bool = False, ) -> str: """Maximum-depth memory search (top_k=500, multi-round, full reranking). Uses a heavier cross-encoder (BAAI/bge-reranker-v2-m3, 568M params) than @@ -1166,6 +1190,8 @@ def truememory_search_deep( user_id: Filter results to this user (optional). limit: Maximum number of results to return. queries: Optional list of queries (preferred over pipe-separated string). + include_temporal_resolution: Return an envelope with results and date/landmark + diagnostics, including when no memories match. Default returns a list. """ MAX_QUERY_LENGTH = 2000 if queries: @@ -1174,26 +1200,32 @@ def truememory_search_deep( query = query[:MAX_QUERY_LENGTH] query_list = [q.strip() for q in query.split("|") if q.strip()] else: - return json.dumps([]) + return json.dumps({"results": [], "temporal_resolutions": []} if include_temporal_resolution else []) _touch_search_time() limit = max(1, min(limit, 200)) - _set_reranker(_DEEP_RERANKER) - llm_fn = _resolve_deepsearch_llm() uid = user_id or None if not query_list: - return json.dumps([]) + return json.dumps({"results": [], "temporal_resolutions": []} if include_temporal_resolution else []) if len(query_list) > 10: query_list = query_list[:10] - if len(query_list) == 1: - m = _get_memory() - results = m.search_deep( - query_list[0], user_id=uid, limit=_DEEP_INTERNAL_LIMIT, llm_fn=llm_fn, - ) - return json.dumps(results[:limit], indent=2) + with _search_operation(reranker_id=_DEEP_RERANKER) as (m, operation): + _set_reranker(operation.reranker_id) + llm_fn = _resolve_deepsearch_llm(operation.tier) + if len(query_list) == 1: + results = m.search_deep( + query_list[0], user_id=uid, limit=_DEEP_INTERNAL_LIMIT, llm_fn=llm_fn, + ) + response = {"results": results[:limit], "temporal_resolution": + m.resolve_temporal_query(query_list[0], user_id=uid)} if include_temporal_resolution else results[:limit] + return json.dumps(response, indent=2) - results = _parallel_search(query_list, uid, _DEEP_INTERNAL_LIMIT, llm_fn, limit) - return json.dumps(results, indent=2) + results = _parallel_search(query_list, uid, _DEEP_INTERNAL_LIMIT, llm_fn, limit, + memory=m, operation=operation) + response = {"results": results, "temporal_resolutions": [ + {"query": q, "resolution": m.resolve_temporal_query(q, user_id=uid)} for q in query_list + ]} if include_temporal_resolution else results + return json.dumps(response, indent=2) @mcp.tool() @@ -1320,7 +1352,7 @@ def truememory_configure( "max" = every exchange evaluated regardless of depth. Empty string = no change (preserves current setting). """ - global _memory + global _config_cache, _memory tier = tier.lower().strip() if tier not in ("edge", "base", "pro", "custom"): return json.dumps({"error": "tier must be 'edge', 'base', 'pro', or 'custom'"}) @@ -1360,6 +1392,7 @@ def truememory_configure( # Save to persistent config (tier change deferred to _finalize_rebuild) config = _load_config() + old_config = config.copy() old_tier = config.get("tier", "edge") # Store API key if provided @@ -1388,12 +1421,20 @@ def truememory_configure( except Exception: pass - # Always persist the tier selection — even on first run with no api_key - # or email. Without this, Edge tier on first run (old_tier defaults to - # "edge", so old_tier == tier, skipping the tier-switch block) was never - # written to config.json, leaving setup_required=True forever (#497). - config["tier"] = tier - _save_config(config) + from truememory.tier_switch.projection import patch_config_fields + fields = {key: value for key, value in config.items() + if key not in {"tier", "tier_activation_generation"} + and (key not in old_config or old_config[key] != value)} + config = patch_config_fields(fields, config_path=_CONFIG_PATH, lock_path=_CONFIG_LOCK_PATH) + # Completing the default Edge onboarding changes no serving model space. + if tier == "edge" and "tier" not in old_config: + from truememory.tier_switch.projection import _read_config, _write_config + with _config_file_lock(strict=True), _config_write_lock: + fresh = _read_config(_CONFIG_PATH) + if "tier" not in fresh: + fresh["tier"] = "edge" + _write_config(_CONFIG_PATH, fresh) + _config_cache = None # Issue #645 (M-35): search_intensity is part of the recall cache key # (it changes both the memory limit and the payload budget). A config @@ -1419,58 +1460,38 @@ def truememory_configure( # Clear stored LLM errors for the provider we just re-keyed _clear_llm_error(api_provider) - # Apply model change — temporarily allow downloads for tier switch - # (the new model may not be cached yet). rebuild_error: str | None = None rebuild_action: str | None = None rebuild_status_id: int = 0 - os.environ.pop("HF_HUB_OFFLINE", None) - os.environ.pop("TRANSFORMERS_OFFLINE", None) + switch_status = "configured" + from truememory.tier_switch.manager import RebuildManager + from truememory.tier_switch.cache import get_transition_action + manager = RebuildManager.get_instance() + rebuild_action = get_transition_action(old_tier, tier) try: - os.environ["TRUEMEMORY_EMBED_MODEL"] = tier - from truememory.vector_search import set_embedding_model - set_embedding_model(tier) - - from truememory.reranker import set_active_tier as _set_active_tier - _set_active_tier(tier) - _set_reranker(_current_reranker()) - - # Tier-switch: determine transition action and handle accordingly. - # Base↔Pro = instant (same embedding model). Cross-group = async rebuild. - if old_tier != tier: - try: - from truememory.tier_switch.cache import ( - get_transition_action, - ) - action = get_transition_action(old_tier, tier) - rebuild_action = action - - if action == "config_only": - # Base↔Pro share the same embedding space (qwen3_256), so - # there is nothing to re-embed. The tier selection must - # still be persisted here: unlike the delta_or_full path - # (where RebuildManager writes the tier only once the new - # vectors exist), config_only has no rebuild step, so - # without this write the tier change is lost on restart and - # the runtime/config tiers diverge. - config["tier"] = tier - _save_config(config) - - elif action == "delta_or_full": - from truememory.tier_switch.manager import RebuildManager - manager = RebuildManager.get_instance() - rebuild_status_id = manager.start_rebuild( - target_tier=tier, - ) - except Exception as e: - rebuild_error = f"{type(e).__name__}: {e}" - log.exception("truememory_configure tier-switch failed") - finally: - with _memory_lock: - _memory = None + rebuild_status_id = manager.start_rebuild(target_tier=tier, db_path=Path(_DB_PATH), + config_path=_CONFIG_PATH, config_lock_path=_CONFIG_LOCK_PATH) + if rebuild_status_id: + switch_status = "building" + elif manager._last_outcome == "activation_pending": + switch_status = "activation_pending" + elif manager._last_outcome == "preparing": + switch_status = "preparing" + except Exception as error: + rebuild_error = type(error).__name__ + ": " + str(error) + switch_status = "activation_pending" if manager._last_outcome == "activation_pending" else "switch_failed" + log.exception("truememory_configure tier activation did not complete") finally: os.environ["HF_HUB_OFFLINE"] = "1" os.environ["TRANSFORMERS_OFFLINE"] = "1" + # Existing users of the object retain their reference and serving lease. + # A subsequent request must reopen and reconcile the committed journal. + with _memory_lock: + _memory = None + _config_cache = None + effective_config = _load_config() + served_tier = getattr(manager, "_served_tier", effective_config.get("tier", old_tier)) + effective_tier = served_tier or old_tier _tier_descriptions = { "edge": "Edge: Model2Vec embeddings (8M params), MiniLM reranker", @@ -1479,11 +1500,14 @@ def truememory_configure( "custom": "Custom: user-provided embedding model + reranker via TRUEMEMORY_CUSTOM_* env vars", } result = { - "status": "configured", + "status": switch_status, "tier": tier, - "description": _tier_descriptions.get(tier, f"Tier: {tier}"), + "requested_tier": tier, + "served_tier": served_tier, + "description": (_tier_descriptions.get(effective_tier, f"Tier: {effective_tier}") + if served_tier is not None else "Serving state awaits authoritative readback."), } - if rebuild_action == "config_only": + if rebuild_action == "config_only" and rebuild_error is None and switch_status == "configured": result["note"] = "Tier switched instantly (same embedding model)." elif rebuild_status_id: result["note"] = ( @@ -1491,12 +1515,13 @@ def truememory_configure( f"Query progress with truememory_status({rebuild_status_id})." ) result["status_id"] = rebuild_status_id + elif switch_status == "preparing": + result["note"] = "Tier preparation is in progress; no rebuild status ID has been allocated yet." if rebuild_error is not None: result["rebuild_error"] = rebuild_error result["warning"] = ( - "Tier switch succeeded but memory re-embedding failed. Re-run " - "truememory_configure() to retry, or delete ~/.truememory/memories.db " - "to start fresh." + "Tier activation is incomplete. The committed serving decision remains authoritative; " + "check rebuild status and retry reconciliation." ) if api_key: result["api_key_saved"] = f"{api_provider} key stored" @@ -1516,7 +1541,7 @@ def truememory_configure( or config.get("openrouter_api_key") or config.get("openai_api_key") ) - if tier != "pro": + if effective_tier != "pro": result["hyde_search"] = "disabled (HyDE is Pro-only)" elif has_key: result["hyde_search"] = "enabled" @@ -1528,10 +1553,18 @@ def truememory_configure( _onboarded = Path.home() / ".truememory" / ".onboarded" try: _onboarded.parent.mkdir(parents=True, exist_ok=True) - _onboarded.write_text(f"tier={tier}\n", encoding="utf-8") + _onboarded.write_text(f"tier={effective_tier}\n", encoding="utf-8") except OSError: pass + if switch_status != "configured": + result["next_steps"] = ( + "Configuration is saved. Start a new session to admit the requested tier; runtime readiness remains unconfirmed." + if getattr(manager, "_last_action", None) == "onboarding" else + "The requested tier is not ready. Existing serving state remains guarded until certified activation completes." + ) + return json.dumps(result, indent=2) + # Onboarding: usage examples and next steps result["next_steps"] = ( "TrueMemory is ready. Here's how it works:\n" @@ -1569,8 +1602,10 @@ def truememory_status(status_id: int = 0) -> str: Returns a JSON object with: - ``rebuild``: tier-switch re-embedding progress (if any). - ``degradation``: per-subsystem health — model server, reranker, - HyDE LLM, encoding gate, and sqlite-vec. Each entry has a - ``status`` of ``"ok"`` or ``"degraded"`` plus diagnostic detail. + HyDE LLM, encoding gate, sqlite-vec, and clustering. Clustering + includes availability, freshness, coverage and the last attempt. + Every status is ``ok`` or ``degraded``; clustering's separate state + distinguishes ``unknown``, ``pending`` and ``deferred`` readiness. When everything is healthy every subsystem shows ``"ok"``. When a component silently degrades (reranker OOM, HyDE outage, @@ -1708,8 +1743,9 @@ def _unload_models() -> None: if unloaded_any: try: + from truememory.mps_utils import get_mps_memory_budget import torch - if hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): + if get_mps_memory_budget() is not None and hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): torch.mps.empty_cache() torch.mps.synchronize() except Exception: @@ -1752,19 +1788,17 @@ def _preload_models(): def _load_embedding_model_and_db(): try: - from truememory.vector_search import get_model - get_model() - except Exception: - pass - try: - _get_memory() + with _search_operation(): + from truememory.vector_search import get_model + get_model() except Exception: pass def _load_reranker(): try: - from truememory.reranker import get_reranker - get_reranker(model_name=_current_reranker()) + with _search_operation() as (_, operation): + from truememory.reranker import get_reranker + get_reranker(model_name=operation.reranker_id) except Exception: pass @@ -2327,15 +2361,8 @@ def main(): os.environ.setdefault("MKL_NUM_THREADS", "1") os.environ.setdefault("OPENBLAS_NUM_THREADS", "1") os.environ.setdefault("NUMEXPR_MAX_THREADS", "1") - if not os.environ.get("PYTORCH_MPS_HIGH_WATERMARK_RATIO"): - try: - import psutil - total_gb = psutil.virtual_memory().total / (1024**3) - ratio = str(min(0.08, 2.5 / total_gb)) if total_gb >= 16 else "0.19" - except Exception: - ratio = "0.19" - os.environ["PYTORCH_MPS_HIGH_WATERMARK_RATIO"] = ratio - os.environ.setdefault("PYTORCH_MPS_LOW_WATERMARK_RATIO", "0.0") + # Model factories calibrate an MPS byte budget in the process that owns + # the models. Do not export a generated ratio as a child operator override. # Initialize telemetry (fire-and-forget, opt-out via TRUEMEMORY_TELEMETRY=off) # Update check now runs in background thread inside telemetry.init() @@ -2356,10 +2383,20 @@ def main(): pass # Start shared model server (loads models once for all processes). - # Falls back to per-process loading if server can't start. - from truememory.model_client import ensure_server_running - if not ensure_server_running(): + from truememory.model_client import ensure_server_running, use_model_server + if not use_model_server(): _preload_models() + else: + try: + shared_ready = ensure_server_running() + except (ConnectionError, OSError) as error: + shared_ready = False + log.warning("Shared model-server readiness failed: %s", error) + if not shared_ready: + log.warning( + "Shared model server unavailable; inference will retry it. " + "Inspect ~/.truememory/model_server.stderr if this persists." + ) # Start background backlog drainer — processes queued session # transcripts every 60s while the MCP server is alive, respecting diff --git a/truememory/model_client.py b/truememory/model_client.py index 0eb47072..77cb5063 100644 --- a/truememory/model_client.py +++ b/truememory/model_client.py @@ -6,8 +6,8 @@ Auto-starts the server on first request if not running. -Falls back to local model loading if the server cannot be reached. -Set TRUEMEMORY_NO_MODEL_SERVER=1 to force local loading. +An unavailable server raises an availability error without loading local copies. +Set TRUEMEMORY_NO_MODEL_SERVER=1 at process startup for explicit local loading. """ import base64 @@ -16,6 +16,7 @@ import os import platform import plistlib +import secrets import shutil import socket import struct @@ -23,14 +24,22 @@ import sys import time from pathlib import Path +from typing import TYPE_CHECKING import numpy as np +if TYPE_CHECKING: + from truememory.embedding_target import EmbeddingTarget + from truememory._platform import ( _LOOPBACK_HOST, + _MODEL_SERVER_GENERATION_ENV, _USE_UNIX, pid_is_alive, + process_birth, + read_start_claim, spawn_kwargs, + try_file_lock, ) log = logging.getLogger(__name__) @@ -40,6 +49,9 @@ PID_PATH = _TRUEMEMORY_DIR / "model_server.pid" PORT_PATH = _TRUEMEMORY_DIR / "model_server.port" TOKEN_PATH = _TRUEMEMORY_DIR / "model_server.token" +LOCK_PATH = _TRUEMEMORY_DIR / "model_server.lock" +START_LOCK_PATH = _TRUEMEMORY_DIR / "model_server.start.lock" +START_STATE_PATH = _TRUEMEMORY_DIR / "model_server.start.json" _HEADER_FMT = ">I" _HEADER_SIZE = struct.calcsize(_HEADER_FMT) @@ -54,6 +66,16 @@ class ProtocolMismatchError(ConnectionError): """Raised when the server speaks an incompatible/foreign protocol.""" + +class ServingIdentityMismatchError(ProtocolMismatchError): + """A selected request was rejected for effective identity or shape drift.""" + + +class ModelServerBusyError(RuntimeError): + """Capacity rejection: do not restart the daemon or load another model.""" + + retry_after_ms = 250 + _SERVER_START_TIMEOUT = 30.0 _REQUEST_TIMEOUT = 120.0 @@ -91,7 +113,7 @@ def _json_object_hook(obj): ) -def _ensure_app_bundle() -> str | None: +def _ensure_app_bundle(deadline: float | None = None) -> str | None: """Create a macOS .app bundle so Activity Monitor shows our icon. Returns the path to the .app executable, or None on failure. @@ -108,6 +130,10 @@ def _ensure_app_bundle() -> str | None: except OSError: pass + # Cosmetic setup must not consume a short recall/startup deadline. + if deadline is not None and _remaining(deadline) < 10: + return None + try: if _APP_BUNDLE_PATH.exists(): shutil.rmtree(_APP_BUNDLE_PATH) @@ -151,7 +177,7 @@ def _ensure_app_bundle() -> str | None: [_LSREGISTER, "-f", str(_APP_BUNDLE_PATH)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - timeout=10, + timeout=min(10, _remaining(deadline)) if deadline is not None else 10, ) return str(_APP_EXECUTABLE) @@ -209,90 +235,136 @@ def _server_ready() -> bool: return PORT_PATH.exists() and TOKEN_PATH.exists() -def _start_server(wait_timeout: float | None = None) -> bool: - """Start the model server as a detached subprocess. - - *wait_timeout* caps how long to wait for the spawned server to become - ready (defaults to ``_SERVER_START_TIMEOUT``). Deadline-bound callers - (issue #577) pass their remaining budget: the spawn still happens, so - the server warms up for subsequent requests even when this call - returns False. - """ - if os.environ.get("TRUEMEMORY_NO_MODEL_SERVER", "") == "1": +def _probe_server(deadline: float) -> bool: + """Validate transport/auth/protocol without renewing the caller's budget.""" + if not _server_ready(): return False - # M-89: ~/.truememory holds real memories/PII — keep it owner-only (0700), - # never the default-umask 0755. - _TRUEMEMORY_DIR.mkdir(parents=True, exist_ok=True) try: - _TRUEMEMORY_DIR.chmod(0o700) + response = _send_request({"op": "ping"}, timeout=min(0.2, _remaining(deadline))) + except ModelServerBusyError: + return True # A capacity response still proves an existing daemon. + except ProtocolMismatchError: + raise except OSError: - pass + return False + if not response.get("ok"): + raise ProtocolMismatchError("Model endpoint does not accept the readiness ping") + return True - alive = _server_is_alive() - if not alive: - # Clean up all stale artefacts in one pass. - for p in (SOCK_PATH, PID_PATH, PORT_PATH, TOKEN_PATH): - p.unlink(missing_ok=True) - else: - return True - log.info("Starting model server...") +def _write_start_claim(claim: dict) -> None: + temporary = START_STATE_PATH.with_name(START_STATE_PATH.name + "." + secrets.token_hex(8) + ".tmp") + fd = os.open(str(temporary), os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600) + try: + with os.fdopen(fd, "w", encoding="utf-8") as stream: + json.dump(claim, stream) + os.replace(temporary, START_STATE_PATH) + finally: + temporary.unlink(missing_ok=True) - popen_extra = spawn_kwargs() - app_exe = _ensure_app_bundle() +def _launch_is_pending(claim: dict | None) -> bool: + if claim is None: + return False + identity = claim["child"] or claim["launcher"] + return process_birth(identity["pid"]) == identity["born"] + + +def _spawn_server(generation: str, deadline: float) -> subprocess.Popen: + _remaining(deadline) + app_exe = _ensure_app_bundle(deadline) + env = os.environ.copy() + env[_MODEL_SERVER_GENERATION_ENV] = generation if app_exe: - cmd = [app_exe, "-m", "truememory.model_server"] - env = os.environ.copy() env["PYTHONPATH"] = os.pathsep.join(sys.path) - else: - cmd = [sys.executable, "-m", "truememory.model_server"] - env = None + executables = [app_exe, sys.executable] if app_exe else [sys.executable] + for index, executable in enumerate(executables): + _remaining(deadline) + try: + with (_TRUEMEMORY_DIR / "model_server.stderr").open("a") as stderr: + return subprocess.Popen( + [executable, "-m", "truememory.model_server"], + stdout=subprocess.DEVNULL, stderr=stderr, env=env, **spawn_kwargs(), + ) + except OSError: + if index == len(executables) - 1: + raise + raise RuntimeError("No model-server executable available") + +def _start_server(wait_timeout: float | None = None) -> bool: + """Coordinate one launch generation and wait within one monotonic budget. + + A short-lived launch lock protects the claim and Popen; the child owns a + separate lifetime bind lock. A timed-out caller leaves its live child + claimed, so concurrent or later clients wait instead of spawning copies. + """ + if not use_model_server(): + return False + wait = _SERVER_START_TIMEOUT if wait_timeout is None else min(_SERVER_START_TIMEOUT, max(wait_timeout, 0.0)) + if wait <= 0: + return False + deadline = time.monotonic() + wait + launched = False try: - _stderr_path = _TRUEMEMORY_DIR / "model_server.stderr" - _stderr_fh = open(_stderr_path, "a") + _TRUEMEMORY_DIR.mkdir(parents=True, exist_ok=True) try: - subprocess.Popen( - cmd, - stdout=subprocess.DEVNULL, - stderr=_stderr_fh, - env=env, - **popen_extra, - ) - finally: - _stderr_fh.close() - except Exception as e: - log.warning("Failed to start model server: %s", e) - if app_exe: - try: - _stderr_fh2 = open(_stderr_path, "a") + _TRUEMEMORY_DIR.chmod(0o700) + except OSError: + pass + while time.monotonic() < deadline: + if _probe_server(deadline): + return True + _remaining(deadline) + gate = try_file_lock(START_LOCK_PATH) + if gate is not None: try: - subprocess.Popen( - [sys.executable, "-m", "truememory.model_server"], - stdout=subprocess.DEVNULL, - stderr=_stderr_fh2, - **popen_extra, - ) + if _probe_server(deadline): + return True + _remaining(deadline) + bind = try_file_lock(LOCK_PATH) + if bind is not None: + claim = None + try: + # Never trust a PID file or remove endpoint files. + # A held bind lock is the daemon's ownership proof. + previous = read_start_claim(START_STATE_PATH) + if not _launch_is_pending(previous): + if launched: + return False # At most one launch per caller. + born = process_birth(os.getpid()) + if born is None: + raise OSError("Cannot identify startup owner") + claim = {"generation": secrets.token_hex(16), + "launcher": {"pid": os.getpid(), "born": born}, "child": None} + _write_start_claim(claim) + finally: + os.close(bind) + # Release bind ownership BEFORE launching the child. + if claim is not None: + try: + child = _spawn_server(claim["generation"], deadline) + launched = True + child_born = process_birth(child.pid) + if child_born is None: + raise OSError("Model server exited before startup") + claim["child"] = {"pid": child.pid, "born": child_born} + _write_start_claim(claim) + except (OSError, TimeoutError): + # Invalidate this generation; a late child must + # not publish after its launch failed. + START_STATE_PATH.unlink(missing_ok=True) + raise finally: - _stderr_fh2.close() - except Exception as e2: - log.warning("Fallback launch also failed: %s", e2) - return False - else: - return False - - wait = _SERVER_START_TIMEOUT - if wait_timeout is not None: - wait = min(wait, max(wait_timeout, 0.0)) - deadline = time.time() + wait - while time.time() < deadline: - if _server_ready(): - time.sleep(0.2) - return True - time.sleep(0.1) - - log.warning("Model server did not start within %.0fs", wait) + os.close(gate) + time.sleep(min(0.05, _remaining(deadline))) + except ProtocolMismatchError: + raise + except TimeoutError: + pass + except (OSError, ValueError) as error: + log.warning("Shared model-server startup unavailable (%s)", type(error).__name__) + log.warning("Shared model server unavailable within %.2fs; retry or inspect model_server.stderr", wait) return False @@ -373,24 +445,40 @@ def _send_request(request: dict, timeout: float | None = None) -> dict: data = json.dumps(payload).encode("utf-8") header = struct.pack(_HEADER_FMT, len(data)) sock.settimeout(_remaining(deadline) if deadline is not None else _REQUEST_TIMEOUT) - sock.sendall(header + data) - - sock.settimeout(_remaining(deadline) if deadline is not None else _REQUEST_TIMEOUT) - resp_header = _recv_exact(sock, _HEADER_SIZE) - if not resp_header: - raise ConnectionError("Server closed connection") - resp_len = struct.unpack(_HEADER_FMT, resp_header)[0] - if resp_len > _MAX_MESSAGE_SIZE: - raise ConnectionError(f"Response too large: {resp_len} bytes") - sock.settimeout(_remaining(deadline) if deadline is not None else _REQUEST_TIMEOUT) - resp_data = _recv_exact(sock, resp_len) - if not resp_data: - raise ConnectionError("Incomplete response") - return _decode_response(resp_data) + try: + sock.sendall(header + data) + except OSError: + # A bounded server may reject the header before our full send ends. + # Preserve that explicit error without mistaking overload for death. + recovery_deadline = time.monotonic() + 0.25 + if deadline is not None: + recovery_deadline = min(recovery_deadline, deadline) + try: + _receive_response(sock, recovery_deadline) + except ModelServerBusyError: + raise + except (OSError, ValueError): + pass + raise + return _receive_response(sock, deadline) finally: sock.close() +def _receive_response(sock: socket.socket, deadline: float | None) -> dict: + response_deadline = deadline if deadline is not None else time.monotonic() + _REQUEST_TIMEOUT + resp_header = _recv_exact(sock, _HEADER_SIZE, response_deadline) + if not resp_header: + raise ConnectionError("Server closed connection") + resp_len = struct.unpack(_HEADER_FMT, resp_header)[0] + if resp_len > _MAX_MESSAGE_SIZE: + raise ConnectionError(f"Response too large: {resp_len} bytes") + resp_data = _recv_exact(sock, resp_len, response_deadline) + if not resp_data: + raise ConnectionError("Incomplete response") + return _decode_response(resp_data) + + def _decode_response(resp_data: bytes) -> dict: """Decode a server response, detecting a foreign/stale protocol (M-53). @@ -415,12 +503,16 @@ def _decode_response(resp_data: bytes) -> dict: raise ProtocolMismatchError( "protocol mismatch — old model server running; restart it" ) + if resp.get("error_code") == "server_busy": + raise ModelServerBusyError("Model server busy: request capacity exhausted") return resp -def _recv_exact(sock: socket.socket, n: int) -> bytes | None: +def _recv_exact(sock: socket.socket, n: int, deadline: float | None = None) -> bytes | None: buf = bytearray() while len(buf) < n: + if deadline is not None: + sock.settimeout(_remaining(deadline)) chunk = sock.recv(n - len(buf)) if not chunk: return None @@ -471,7 +563,12 @@ def _request_with_autostart(request: dict, timeout: float | None = None) -> dict raise _deadline_error(timeout) if not _start_server(wait_timeout=remaining): - raise ConnectionError("Cannot start model server") + if has_deadline and time.monotonic() - started >= timeout: + raise _deadline_error(timeout) + raise ConnectionError( + "Shared model server unavailable; retry the request or inspect " + "~/.truememory/model_server.stderr" + ) if has_deadline: remaining = timeout - (time.monotonic() - started) @@ -485,6 +582,32 @@ def _request_with_autostart(request: dict, timeout: float | None = None) -> dict return _send_request(request) +def _batch_request(request: dict, kwargs: dict) -> dict: + """Use an additive operation so old daemons cannot ignore an explicit limit.""" + if "batch_size" not in kwargs: + return request + batch_size = kwargs["batch_size"] + if isinstance(batch_size, bool) or not isinstance(batch_size, (int, np.integer)): + raise ValueError("batch_size must be a positive integer") + if batch_size <= 0: + raise ValueError("batch_size must be a positive integer") + return {**request, "op": request["op"] + "_batched", "batch_size": int(batch_size)} + + +def _check_model_response(response: dict, request: dict) -> None: + if response.get("error_code") == "serving_identity_mismatch": + raise ServingIdentityMismatchError("Model server rejected the selected serving identity") + if response.get("ok"): + return + if response.get("error_code") == "server_busy": + raise ModelServerBusyError("Model server busy: request capacity exhausted") + if response.get("error") == f"Unknown op: {request['op']}" and "batch_size" in request: + raise ProtocolMismatchError( + "Model server does not support bounded batches; restart it after upgrading" + ) + raise RuntimeError(f"Model server error: {response.get('error', 'unknown')}") + + class EmbeddingProxy: """Drop-in replacement for the embedding model with .encode() method.""" @@ -497,19 +620,118 @@ def encode(self, texts, timeout: float | None = None, **kwargs) -> np.ndarray: *timeout* is an optional per-call deadline in seconds (issue #577); on expiry a :class:`TimeoutError` is raised (fast-fail, no autostart retry). ``None`` uses the process default / legacy 120s. + + ``batch_size`` is a positive integer ceiling on items per inference + call. Older daemons must be restarted to support explicit ceilings. """ if isinstance(texts, str): texts = [texts] - resp = _request_with_autostart({ + request = _batch_request({ "op": "embed", "texts": list(texts), "tier": self._tier, - }, timeout=timeout) - if not resp.get("ok"): - raise RuntimeError(f"Model server error: {resp.get('error', 'unknown')}") + }, kwargs) + resp = _request_with_autostart(request, timeout=timeout) + _check_model_response(resp, request) return resp["vectors"] +class PreparedEmbeddingProxy: + """Strict target requests; no fallback to the ordinary embedding protocol.""" + + def __init__(self, target: "EmbeddingTarget") -> None: + self.target = target + + def _request(self, operation: str, timeout: float | None, **fields: object) -> dict: + from truememory.embedding_target import EmbeddingTarget, EmbeddingTargetError + + request = {"op": operation, "target": self.target.to_wire(), **fields} + response = _request_with_autostart(request, timeout=timeout) + if response.get("error") == f"Unknown op: {operation}": + raise ProtocolMismatchError( + "Model server does not support certified embedding targets; restart it after upgrading" + ) + _check_model_response(response, request) + try: + effective = EmbeddingTarget.from_wire(response.get("target")) + except EmbeddingTargetError as exc: + raise ProtocolMismatchError("Model server omitted a valid embedding target receipt") from exc + if effective != self.target: + raise ProtocolMismatchError("Model server returned a different embedding target") + return response + + def prepare(self, timeout: float | None = None) -> None: + self._request("prepare_embed_target_v1", timeout) + + def encode( + self, texts: str | list[str], *, timeout: float | None = None, batch_size: int = 32, + ) -> np.ndarray: + from truememory.embedding_target import check_target_vectors + + if isinstance(texts, str): + texts = [texts] + texts = list(texts) + if isinstance(batch_size, bool) or not isinstance(batch_size, int) or batch_size <= 0: + raise ValueError("batch_size must be a positive integer") + response = self._request("embed_target_v1", timeout, texts=texts, batch_size=batch_size) + vectors = response["vectors"] + check_target_vectors(self.target, vectors, len(texts)) + return vectors + + +def _check_serving_protocol(response: dict) -> None: + if type(response.get("protocol")) is not int or response["protocol"] != PROTOCOL_VERSION: + raise ProtocolMismatchError("Model server omitted the selected-serving protocol; restart it after upgrading") + + +def _serving_reranker_name(value: object) -> str: + import re + + if (type(value) is not str or len(value.encode("utf-8")) > 512 + or re.fullmatch(r"[\w][\w.\-]*(/[\w][\w.\-]*)?", value) is None): + raise ValueError("Selected reranking requires an explicit model identity") + return value + + +class CertifiedEmbeddingProxy(EmbeddingProxy): + """Built-in serving with a fresh identity receipt on every fast/main result. + + Construction configures identity only; it does not contact the daemon or + prove model readiness. Custom targets use PreparedEmbeddingProxy instead. + """ + + def __init__(self, target: "EmbeddingTarget") -> None: + from truememory.embedding_target import EmbeddingTarget, EmbeddingTargetError + + self.target = EmbeddingTarget.from_wire(target.to_wire()) + if self.target.tier == "custom": + raise EmbeddingTargetError("Custom serving requires the prepared target protocol") + super().__init__(tier=self.target.model_id) + + def encode(self, texts, timeout: float | None = None, **kwargs) -> np.ndarray: + from truememory.embedding_target import EmbeddingTarget, EmbeddingTargetError, check_target_vectors + + if isinstance(texts, str): + texts = [texts] + texts = list(texts) + request = _batch_request({ + "op": "embed", "texts": texts, "tier": self.target.model_id, + "expected_target": self.target.to_wire(), + }, kwargs) + response = _request_with_autostart(request, timeout=timeout) + _check_model_response(response, request) + _check_serving_protocol(response) + try: + effective = EmbeddingTarget.from_wire(response.get("target")) + except EmbeddingTargetError as exc: + raise ProtocolMismatchError("Model server omitted a valid serving target receipt") from exc + if effective != self.target: + raise ProtocolMismatchError("Model server returned a different serving target") + vectors = response["vectors"] + check_target_vectors(self.target, vectors, len(texts)) + return vectors + + class RerankerProxy: """Drop-in replacement for CrossEncoder with .predict() method.""" @@ -520,31 +742,51 @@ def predict(self, pairs, timeout: float | None = None, **kwargs) -> np.ndarray: """Rerank *pairs* via the model server. *timeout* is an optional per-call deadline in seconds (issue #577); - see :meth:`EmbeddingProxy.encode`. + see :meth:`EmbeddingProxy.encode` for deadlines and ``batch_size``. """ - resp = _request_with_autostart({ + request = _batch_request({ "op": "rerank", "pairs": list(pairs), "model_name": self._model_name, - }, timeout=timeout) - if not resp.get("ok"): - raise RuntimeError(f"Model server error: {resp.get('error', 'unknown')}") + }, kwargs) + resp = _request_with_autostart(request, timeout=timeout) + _check_model_response(resp, request) return resp["scores"] -def use_model_server() -> bool: - """Check if the model server should be used. +class CertifiedRerankerProxy(RerankerProxy): + """Explicit reranker receipt per response, without eager native readiness.""" + + def __init__(self, model_name: str) -> None: + super().__init__(model_name=_serving_reranker_name(model_name)) - Returns True only if: - 1. TRUEMEMORY_NO_MODEL_SERVER is not set - 2. The server endpoint exists (server is running) + def predict(self, pairs, timeout: float | None = None, **kwargs) -> np.ndarray: + request = _batch_request({ + "op": "rerank", "pairs": list(pairs), "model_name": self._model_name, + "expected_model_name": self._model_name, + }, kwargs) + response = _request_with_autostart(request, timeout=timeout) + _check_model_response(response, request) + _check_serving_protocol(response) + receipt = response.get("reranker") + if (type(receipt) is not dict or set(receipt) != {"version", "model_name"} + or type(receipt["version"]) is not int or receipt["version"] != 1 + or type(receipt["model_name"]) is not str or receipt["model_name"] != self._model_name): + raise ProtocolMismatchError("Model server omitted the exact serving reranker receipt") + scores = response.get("scores") + if (not isinstance(scores, np.ndarray) or not scores.shape + or scores.shape[0] != len(request["pairs"])): + raise ProtocolMismatchError("Model server returned a different serving reranker result count") + return scores - Processes that want to ensure the server is running should call - ensure_server_running() first (e.g., during MCP server startup). + +def use_model_server() -> bool: + """Return sharing policy, independently of endpoint readiness. + + Configure local mode before loading models. Existing cached model lifetime + is unchanged; changing environment variables is not a live mode switch. """ - if os.environ.get("TRUEMEMORY_NO_MODEL_SERVER", "") == "1": - return False - return _server_ready() and _server_is_alive() + return os.environ.get("TRUEMEMORY_NO_MODEL_SERVER", "") != "1" def ensure_server_running() -> bool: @@ -553,10 +795,6 @@ def ensure_server_running() -> bool: Call from MCP server startup or CLI to enable the shared model server. Returns True if server is running after this call. """ - if os.environ.get("TRUEMEMORY_NO_MODEL_SERVER", "") == "1": - return False - if _server_is_alive() and _server_ready(): - return True return _start_server() diff --git a/truememory/model_server.py b/truememory/model_server.py index 0cd5751a..37ceb224 100644 --- a/truememory/model_server.py +++ b/truememory/model_server.py @@ -16,28 +16,12 @@ import atexit import os -try: - import psutil -except ImportError: - psutil = None - - def _set_mps_memory_cap(): - """Set MPS memory cap and BLAS thread limits BEFORE torch is imported.""" + """Keep thread defaults early; MPS byte calibration occurs at model load.""" os.environ.setdefault("OMP_NUM_THREADS", "1") os.environ.setdefault("MKL_NUM_THREADS", "1") os.environ.setdefault("OPENBLAS_NUM_THREADS", "1") os.environ.setdefault("NUMEXPR_MAX_THREADS", "1") - if os.environ.get("PYTORCH_MPS_HIGH_WATERMARK_RATIO"): - return - if psutil is not None: - total_gb = psutil.virtual_memory().total / (1024**3) - ratio = min(0.08, 2.5 / total_gb) if total_gb >= 16 else 0.19 - ratio = str(max(ratio, 1.5 / total_gb)) - else: - ratio = "0.19" - os.environ["PYTORCH_MPS_HIGH_WATERMARK_RATIO"] = ratio - os.environ.setdefault("PYTORCH_MPS_LOW_WATERMARK_RATIO", "0.0") _set_mps_memory_cap() @@ -47,6 +31,7 @@ def _set_mps_memory_cap(): import hmac # noqa: E402 import json # noqa: E402 import logging # noqa: E402 +import math # noqa: E402 import secrets # noqa: E402 import signal # noqa: E402 import socket # noqa: E402 @@ -55,12 +40,28 @@ def _set_mps_memory_cap(): import sys # noqa: E402 import threading # noqa: E402 import time # noqa: E402 +from _thread import LockType # noqa: E402 +from collections.abc import Iterator # noqa: E402 +from concurrent.futures import Future, ThreadPoolExecutor # noqa: E402 +from contextlib import contextmanager # noqa: E402 from dataclasses import dataclass # noqa: E402 from pathlib import Path # noqa: E402 +from typing import TYPE_CHECKING # noqa: E402 import numpy as np # noqa: E402 -from truememory._platform import _LOOPBACK_HOST, _USE_UNIX, _env_int, pid_is_alive # noqa: E402 +if TYPE_CHECKING: + from truememory.embedding_target import EmbeddingTarget + +try: + import psutil # noqa: E402 +except ImportError: + psutil = None + +from truememory._platform import ( # noqa: E402 + _LOOPBACK_HOST, _MODEL_SERVER_GENERATION_ENV, _USE_UNIX, _env_int, + pid_is_alive, read_start_claim, try_file_lock, +) log = logging.getLogger(__name__) @@ -72,6 +73,7 @@ def _set_mps_memory_cap(): IDLE_TIMEOUT = _env_int("TRUEMEMORY_MODEL_SERVER_IDLE", 300, lo=0) LOCK_PATH = _TRUEMEMORY_DIR / "model_server.lock" +START_STATE_PATH = _TRUEMEMORY_DIR / "model_server.start.json" # Issue #646 (M-53): protocol/version handshake. Bumped whenever the wire # format changes incompatibly. The client echoes this back on mismatch. @@ -81,6 +83,119 @@ def _set_mps_memory_cap(): _HEADER_SIZE = struct.calcsize(_HEADER_FMT) _MAX_MESSAGE_SIZE = 10 * 1024 * 1024 # 10 MB +# Embedding uses SentenceTransformer's default; reranking keeps the server's 64. +# These cap items per call, not tokens, queued payloads, or total process memory. +_EMBED_BATCH_LIMIT = 32 +_RERANK_BATCH_LIMIT = 64 + + +class _ResultTooLarge(ValueError): + """The complete float32 result cannot fit the existing response frame.""" + + +class _ServingIdentityMismatch(ValueError): + """Explicit selected identity rejection, distinct from native availability.""" + + serving_identity_mismatch = True + + +class _ProcessMemoryRefused(RuntimeError): + """Current process memory cannot admit another model stage.""" + + +def _array_metadata(shape: tuple, encoded: str = "") -> dict: + return {"__ndarray__": encoded, "dtype": "float32", "shape": list(shape)} + + +def _array_base64_size(shape: tuple) -> int: + raw_bytes = math.prod(shape) * 4 + return 4 * ((raw_bytes + 2) // 3) + + +def _result_wire_size(shape: tuple, field: str) -> int: + envelope = {"ok": True, field: _array_metadata(shape), "protocol": PROTOCOL_VERSION} + return len(json.dumps(envelope).encode("utf-8")) + _array_base64_size(shape) + + +def _check_result_size(shape: tuple, field: str) -> None: + size = _result_wire_size(shape, field) + if size > _MAX_MESSAGE_SIZE: + raise _ResultTooLarge( + f"Complete {field} response requires {size} bytes; the limit is " + f"{_MAX_MESSAGE_SIZE} bytes. Split the request into fewer inputs." + ) + + +def _response_wire_size(response: dict) -> int: + """Measure array JSON without making float32, bytes or base64 copies.""" + array_bytes = 0 + + def metadata_only(obj: object) -> dict: + nonlocal array_bytes + if isinstance(obj, np.ndarray): + array_bytes += _array_base64_size(obj.shape) + return _array_metadata(obj.shape) + raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable") + + envelope_bytes = len(json.dumps(response, default=metadata_only).encode("utf-8")) + return envelope_bytes + array_bytes + + +def _admission_setting(name: str, default: int, minimum: int, maximum: int) -> int: + raw = os.environ.get(name) + if raw is None: + return default + if not raw.isascii() or not raw.isdecimal() or not minimum <= int(raw) <= maximum: + raise ValueError(f"{name} must be an integer between {minimum} and {maximum}") + return int(raw) + + +def _automatic_rss_budget_bytes() -> int: + """Finite sampled admission threshold, without a native peak guarantee.""" + if psutil is None: + raise ValueError("Automatic model-server RSS budget requires a physical-memory sensor") + try: + physical = psutil.virtual_memory().total + except (AttributeError, OSError, RuntimeError, TypeError, ValueError, OverflowError) as exc: + raise ValueError("Automatic model-server physical-memory measurement is unavailable") from exc + if isinstance(physical, bool) or not isinstance(physical, int) or not 0 < physical <= 2**63 - 1: + raise ValueError("Physical memory must be a positive signed64 integer byte count") + budget = min(8 * 1024**3, physical // 4) + if budget <= 0: + raise ValueError("Automatic model-server RSS budget must be positive") + return budget + + +def _batch_limit(value: object, maximum: int) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value <= 0: + raise ValueError("batch_size must be a positive integer") + return min(value, maximum) + + +def _checked_batch(values: object, count: int, total: int, field: str) -> np.ndarray: + batch = values if isinstance(values, np.ndarray) else np.asarray(values, dtype=np.float32) + if batch.ndim == 0 or batch.shape[0] != count: + raise ValueError("Model output count does not match the input microbatch") + _check_result_size((total, *batch.shape[1:]), field) + return np.asarray(batch, dtype=np.float32) + + +def _store_batch_result( + result: np.ndarray | None, values: object, offset: int, count: int, total: int, + field: str = "vectors", *, indices: np.ndarray | None = None, +) -> np.ndarray: + """Check the complete output before allocating it; keep every float32 row.""" + batch = _checked_batch(values, count, total, field) + if result is None: + result = np.empty((total, *batch.shape[1:]), dtype=np.float32) + elif result.shape[1:] != batch.shape[1:]: + raise ValueError("Model output dimensions changed between microbatches") + if indices is None: + result[offset:offset + count] = batch + else: + result[indices] = batch + return result + def _safe_to_cleanup_artifacts() -> bool: """Return True when this process may remove the shared artifacts (M-20). @@ -109,11 +224,7 @@ def _json_default(obj): """Encode numpy arrays as base64 for safe JSON serialization.""" if isinstance(obj, np.ndarray): arr = np.ascontiguousarray(obj, dtype=np.float32) - return { - "__ndarray__": base64.b64encode(arr.tobytes()).decode("ascii"), - "dtype": "float32", - "shape": list(arr.shape), - } + return _array_metadata(arr.shape, base64.b64encode(arr.tobytes()).decode("ascii")) raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable") @@ -148,6 +259,85 @@ class _EmbedState: model: object tier: str model_id: str + generation: object = None + prepared_identity: tuple[str, int, str] | None = None + + def __post_init__(self) -> None: + if self.generation is None: + object.__setattr__(self, "generation", object()) + + +class _RequestDeadlineExceeded(TimeoutError): + """Stop this request without triggering model fallback or another retry.""" + + +@dataclass +class _TransportRequest: + length: int + queue_expires_at: float + stopped: threading.Event + frame_expires_at: float = math.inf + started: bool = False + claimed: bool = False + withdrawn: bool = False + + +@dataclass(frozen=True) +class _RequestDeadline: + expires_at: float | None = None + transport: _TransportRequest | None = None + + @classmethod + def from_wall_clock(cls, value: object) -> "_RequestDeadline": + if value is None: + return cls() + try: + wall_deadline = float(value) + except (TypeError, ValueError, OverflowError): + return cls() # Preserve legacy handling of malformed deadlines. + if math.isnan(wall_deadline) or wall_deadline == math.inf: + return cls() + remaining = wall_deadline - time.time() + return cls(time.monotonic() + remaining) + + def remaining(self) -> float | None: + expires_at = self.expires_at + if self.transport is not None: + if self.transport.stopped.is_set(): + raise _RequestDeadlineExceeded("model server is shutting down") + if expires_at is None and not self.transport.started: + expires_at = self.transport.queue_expires_at + if expires_at is None: + return None + remaining = expires_at - time.monotonic() + if remaining <= 0: + raise _RequestDeadlineExceeded("deadline exceeded before encode") + return remaining + + def check(self) -> None: + self.remaining() + + def start_inference(self) -> None: + self.check() + if self.transport is not None: + self.transport.started = True + + @contextmanager + def locked(self, lock: LockType) -> Iterator[None]: + remaining = self.remaining() + if self.transport is not None: + while not lock.acquire(timeout=min(0.1, remaining) if remaining is not None else 0.1): + remaining = self.remaining() + elif remaining is None: + lock.acquire() + elif not lock.acquire(timeout=min(remaining, threading.TIMEOUT_MAX)): + raise _RequestDeadlineExceeded("deadline exceeded before encode") + try: + # A successful wakeup can still occur after the request expired. + self.check() + yield + finally: + lock.release() class ModelServer: @@ -161,7 +351,39 @@ class ModelServer: # batches or OOM recovery. _FAST_LANE_MAX_TEXTS = 1 - def __init__(self): + def __init__(self, *, automatic_rss_budget: bool = False) -> None: + if not isinstance(automatic_rss_budget, bool): + raise ValueError("automatic_rss_budget must be a bool") + # Installed main opts in; explicit settings and direct construction retain their contract. + if automatic_rss_budget and os.environ.get("TRUEMEMORY_MODEL_SERVER_MAX_RSS_MB") is None: + self._max_rss_bytes = _automatic_rss_budget_bytes() + else: + self._max_rss_bytes = _admission_setting( + "TRUEMEMORY_MODEL_SERVER_MAX_RSS_MB", 0, 0, 2**31 - 1, + ) * 1048576 + self._max_handlers = _admission_setting("TRUEMEMORY_MODEL_SERVER_MAX_HANDLERS", 16, 1, 128) + self._max_request_bytes = _admission_setting( + "TRUEMEMORY_MODEL_SERVER_MAX_REQUEST_BYTES", 32 * 1024**2, _MAX_MESSAGE_SIZE, 1024**3, + ) + self._header_timeout = _admission_setting( + "TRUEMEMORY_MODEL_SERVER_HEADER_TIMEOUT_MS", 1000, 100, 30000, + ) / 1000 + self._frame_timeout = _admission_setting( + "TRUEMEMORY_MODEL_SERVER_FRAME_TIMEOUT_MS", 30000, 100, 120000, + ) / 1000 + self._queue_timeout = 120.0 + self._admission_lock = threading.Lock() + self._staging_lock = threading.Lock() + self._staging: socket.socket | None = None + self._clients: dict[socket.socket, _TransportRequest] = {} + self._reserved_request_bytes = 0 + self._admission_high_water = {"handlers": 0, "request_bytes": 0} + self._busy_rejections = 0 + self._stopped = threading.Event() + self._transport_context = threading.local() + # Submission requires a slot+bytes lease; at most max_handlers futures + # can be running or waiting in this executor. + self._workers = ThreadPoolExecutor(max_workers=self._max_handlers, thread_name_prefix="model-client") # Loaded embedding model + identity as ONE immutable snapshot, # assigned only under self._lock; the fast lane reads the single # reference lock-free (issue #577, panel round 2). @@ -169,6 +391,13 @@ def __init__(self): self._reranker = None self._reranker_name: str | None = None self._lock = threading.Lock() + # Only snapshot/cache publication, never model work or destruction. + # Main state -> residency; fast -> residency; never the reverse. + self._residency_lock = threading.Lock() + # Main models stay exclusively owned through loading, recovery and + # CPU retries, even while the state lock is released (issue #738). + # Lock order: inference -> state; fast inference owns only _fast_lock. + self._inference_lock = threading.Lock() # Issue #577: idle-tracking gets its own lock so the single-text # fast lane (and the idle checker) never block on the global # request lock while a batch encode is in flight. @@ -186,10 +415,10 @@ def __init__(self): self._sticky_cpu: set[str] = set() # Issue #577: dedicated CPU encoder for the single-text fast lane, # loaded lazily on the first *contended* single-text request. - # Cached by MODEL ID (not tier string) so it always tracks the main - # path's actual loaded identity. + # A generation distinguishes A -> B -> A from an unchanged residency. self._fast_encoder = None self._fast_model_id: str | None = None + self._fast_generation: object = None self._fast_lock = threading.Lock() # Issue #646 (M-20): exclusive bind-lock fd, held for the process # lifetime; released in _cleanup. None until run() acquires it. @@ -198,6 +427,32 @@ def __init__(self): # kills a request mid-encode. Guarded by _activity_lock. self._inflight = 0 + def _check_process_memory( + self, stage: str, deadline: _RequestDeadline | None = None, + ) -> None: + """Sample current RSS under the caller's model owner, never peak RSS.""" + if deadline is not None: + deadline.check() + budget = self._max_rss_bytes + if budget == 0: + return + try: + usage = psutil.Process().memory_info().rss if psutil is not None else None + except Exception: + usage = None + # Sensor work consumes the same request budget. Expiry takes precedence + # over a memory refusal and cannot admit another native stage. + if deadline is not None: + deadline.check() + if isinstance(usage, bool) or not isinstance(usage, int) or usage <= 0: + raise _ProcessMemoryRefused( + f"Model server busy: current process RSS unavailable before {stage}" + ) + if usage >= budget: + raise _ProcessMemoryRefused( + f"Model server busy: process RSS budget exhausted before {stage}" + ) + def _mark_sticky_cpu(self, kind: str) -> bool: """Permanently degrade *kind* ("embed"/"rerank") to CPU after an MPS OOM (issue #577). Re-promoting to MPS after recovery guaranteed @@ -240,20 +495,58 @@ def _embed_device(self) -> str | None: from truememory.mps_utils import resolve_device return resolve_device(None) - def _recover_embed_oom_locked(self, model) -> None: + def _recover_embed_oom_locked( + self, model: object, deadline: _RequestDeadline | None = None, + ) -> None: """The single recovery path for an embed MPS OOM (issue #577). Caller MUST hold ``self._lock``. Marks the embed path sticky-CPU (loud log once), flushes the MPS cache, and moves the model to CPU — - all atomically in the caller's lock hold. Retry placement belongs to - the caller: single texts re-encode inside the lock (ms-scale); - batches re-encode after releasing it so other clients don't starve. + all atomically in the caller's lock hold. The caller retains main + inference ownership throughout recovery and retry. Batch retries + release the state lock so bookkeeping and the fast lane stay responsive. """ from truememory.mps_utils import flush_mps_cache self._mark_sticky_cpu("embed") flush_mps_cache() + if deadline is not None: + self._check_embed_recovery_deadline_locked(model, deadline) + try: + self._check_process_memory("embed CPU transfer", deadline) + except (_ProcessMemoryRefused, _RequestDeadlineExceeded): + # A refused transfer leaves the failed accelerator instance behind. + # Invalidate it so a cache hit cannot bypass sticky CPU selection. + state = self._embed_state + if state is not None and state.model is model: + self._publish_embed_state(None) + raise + if deadline is not None: + self._check_embed_recovery_deadline_locked(model, deadline) if hasattr(model, "to"): - model.to("cpu") + try: + model.to("cpu") + except BaseException: + # Failed or cancelled transfers cannot certify this instance + # as CPU-ready. Preserve the original error and sticky choice. + state = self._embed_state + if state is not None and state.model is model: + self._publish_embed_state(None) + raise + + def _check_embed_recovery_deadline_locked( + self, model: object, deadline: _RequestDeadline, + ) -> None: + """Record the OOM even if its request cannot recover; caller holds the lock.""" + self._mark_sticky_cpu("embed") + try: + deadline.check() + except _RequestDeadlineExceeded: + # Cached models bypass device resolution. Drop this failed instance + # so the next request loads on CPU without moving it for dead work. + state = self._embed_state + if state is not None and state.model is model: + self._publish_embed_state(None) + raise @staticmethod def _peek_embed_model_id(tier: str) -> str: @@ -281,13 +574,12 @@ def _peek_embed_model_id(tier: str) -> str: @staticmethod def _resolve_embed_model_id(tier: str) -> str: - """Resolve a tier name to the internal embedding model ID, keeping - vector_search's globals in sync (main-path behavior, pre-#577).""" - from truememory.vector_search import EMBEDDING_MODEL, set_embedding_model - - if tier and tier != EMBEDDING_MODEL: - set_embedding_model(tier) - + """Resolve this request without changing the daemon's default model.""" + if tier and tier.strip().lower() == "qwen3": + # Retain the removed-model error formerly raised by the setter, + # without adopting its broader normalization or changing globals. + from truememory.vector_search import _resolve_model_name + _resolve_model_name(tier) return ModelServer._peek_embed_model_id(tier) @staticmethod @@ -300,6 +592,8 @@ def _build_embed_model(model_id: str, device: str | None): "minishlab/potion-base-8M", force_download=False ) if model_id == "qwen3_256": + from truememory.mps_utils import ensure_mps_memory_budget + ensure_mps_memory_budget(device) from sentence_transformers import SentenceTransformer mkwargs = {} if sys.platform == "darwin": @@ -323,10 +617,12 @@ def _build_embed_model(model_id: str, device: str | None): return StaticModel.from_pretrained( "minishlab/potion-base-8M", force_download=False ) - from sentence_transformers import SentenceTransformer from truememory.tier_config import resolve_custom_tier cfg = resolve_custom_tier() custom_dim = cfg["embed_dim"] + from truememory.mps_utils import ensure_mps_memory_budget + ensure_mps_memory_budget(device) + from sentence_transformers import SentenceTransformer return SentenceTransformer( model_id, truncate_dim=custom_dim, trust_remote_code=False, @@ -345,25 +641,217 @@ def _build_embed_model(model_id: str, device: str | None): # declines all of those and falls through to the main path. _FAST_LANE_SAFE_MODEL_IDS = frozenset({"model2vec", "qwen3_256"}) - def _get_embed_model(self, tier: str): + def _resolve_embed_cache(self, tier: str) -> tuple[str, _EmbedState | None]: + """Return the requested identity and its compatible cached snapshot.""" state = self._embed_state if state is not None and state.tier == tier: - return state.model + # A queued empty-tier request must follow the load it waited for, + # even if the default changed mid-load. Custom keys likewise keep + # the identity actually built rather than re-reading their config. + return state.model_id, state model_id = self._resolve_embed_model_id(tier) - model = self._build_embed_model(model_id, self._embed_device()) + if state is not None and state.model_id == model_id and ( + model_id in self._FAST_LANE_SAFE_MODEL_IDS or state.tier == tier + ): + return model_id, state + return model_id, None + + def _preflight_embed_result(self, tier: str, count: int) -> None: + """Caller holds the state lock; mirror the actual server constructors.""" + if not count: + return + model_id, state = self._resolve_embed_cache(tier) + cached = state is not None + known = model_id in ("model2vec", "qwen3_256", "minilm", "bge-small") + fallback = not cached and os.environ.get("TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD", "").strip() != "1" + if known or fallback: + # Legacy minilm/bge-small fall through to model2vec here, not + # the 384-dimensional models used by the local loader. + _check_result_size((count, 256), "vectors") + # A custom truncate_dim is only an upper bound. Even ST's dimension + # getter can return that bound when native width is unknown; check + # the actual first slice before allocating the complete result. + + def _embed_global_order( + self, model: object, texts: list, limit: int, deadline: _RequestDeadline, + ) -> np.ndarray | None: + """Match modern ST's native32 order only for the known Qwen model.""" + deadline.check() + state = self._embed_state + if (limit != 32 or len(texts) <= limit or state is None or state.model is not model + or state.model_id != "qwen3_256" or not all(isinstance(text, str) for text in texts)): + return None + length = getattr(model, "_input_length", None) + flatten = getattr(model, "_can_flatten_inputs", None) + if not callable(length) or not callable(flatten) or flatten(): + return None + deadline.check() + lengths = [-length(text) for text in texts] + deadline.check() + order = np.argsort(lengths) + deadline.check() + return order + + @staticmethod + def _embed_slice_indices( + model: object, texts: list, order: np.ndarray, offset: int, limit: int, + deadline: _RequestDeadline, + ) -> np.ndarray: + deadline.check() + indices = order[offset:offset + limit] + # Each encode sorts again. Invert its permutation within equal-length + # groups so its native rows keep the exact global order, including ties. + lengths = [-model._input_length(texts[index]) for index in indices] + deadline.check() + inner = np.argsort(lengths) + deadline.check() + inverse = np.argsort(inner) + deadline.check() + return indices[inverse] + + def _rerank_global_order( + self, model: object, pairs: list, limit: int, deadline: _RequestDeadline, + ) -> np.ndarray | None: + """Match modern CrossEncoder's native 64-row policy for ModernBERT.""" + deadline.check() + if (limit != 64 or len(pairs) <= limit or self._reranker is not model + or self._reranker_name != "Alibaba-NLP/gte-reranker-modernbert-base" + or not all(isinstance(pair, (list, tuple)) and len(pair) == 2 + and all(isinstance(text, str) for text in pair) for pair in pairs)): + return None + length = getattr(model, "_input_length", None) + flatten = getattr(model, "_can_flatten_inputs", None) + if not callable(length) or not callable(flatten) or flatten(): + return None + deadline.check() + lengths = [-length(pair) for pair in pairs] + deadline.check() + order = np.argsort(lengths) + deadline.check() + return order + + @staticmethod + def _rerank_slice_indices( + model: object, pairs: list, order: np.ndarray, offset: int, limit: int, + deadline: _RequestDeadline, + ) -> np.ndarray: + deadline.check() + indices = order[offset:offset + limit] + # Predict sorts each supplied batch again, including equal-length pairs. + lengths = [-model._input_length(pairs[index]) for index in indices] + deadline.check() + inner = np.argsort(lengths) + deadline.check() + inverse = np.argsort(inner) + deadline.check() + return indices[inverse] + + @staticmethod + def _preflight_rerank_result(model_name: str | None, count: int) -> None: + from truememory.reranker import get_current_reranker_name + name = model_name or get_current_reranker_name() + if count and name in ( + "cross-encoder/ms-marco-MiniLM-L-6-v2", + "Alibaba-NLP/gte-reranker-modernbert-base", + ): + _check_result_size((count,), "scores") + # Custom CrossEncoder constructors preserve num_labels=None; their + # output may contain several labels per pair. Inspect its real shape. + + def _retire_stale_fast_encoder(self) -> None: + """Drop an idle stale clone; an active owner retries this on release.""" + if not self._fast_lock.acquire(blocking=False): + return + release_fast = True + try: + with self._residency_lock: + state = self._embed_state + if self._fast_encoder is None or ( + state is not None and self._fast_generation is state.generation + ): + # Unlock before publication can change the generation, so + # a following invalidation cannot miss an idle stale cache. + self._fast_lock.release() + release_fast = False + return + retired = self._fast_encoder + self._fast_encoder = None + self._fast_model_id = None + self._fast_generation = None + # Destruction stays outside residency, but before another fast load. + state = None + del retired + finally: + if release_fast: + self._fast_lock.release() + + def _publish_embed_state(self, state: _EmbedState | None) -> None: + """Caller owns the main state lock; retire without waiting for fast work.""" + with self._residency_lock: + retired = self._embed_state + self._embed_state = state + del retired + self._retire_stale_fast_encoder() + + def _get_embed_model(self, tier: str, deadline: _RequestDeadline | None = None): + model_id, state = self._resolve_embed_cache(tier) + if state is not None: + if state.tier != tier: + self._publish_embed_state(_EmbedState( + model=state.model, tier=tier, model_id=model_id, generation=state.generation, + prepared_identity=state.prepared_identity, + )) + return state.model + + # Memory-first replacement: failed construction leaves the cache empty. + self._publish_embed_state(None) + self._check_process_memory("embed construction", deadline) + device = self._embed_device() + if deadline is not None: + deadline.check() + model = self._build_embed_model(model_id, device) + self._check_process_memory("embed construction completion", deadline) # ONE atomic reference assignment of an immutable snapshot — the # fast lane can never observe a torn (model, tier, model_id) triple. - self._embed_state = _EmbedState(model=model, tier=tier, model_id=model_id) + self._publish_embed_state(_EmbedState(model=model, tier=tier, model_id=model_id)) log.info("Loaded embedding model for tier=%s (model=%s)", tier, model_id) return model - def _get_fast_encoder(self, tier: str): + def _get_prepared_embed_model(self, target: "EmbeddingTarget", deadline: _RequestDeadline) -> object: + from truememory.embedding_target import build_target_model + + target.check_configuration() + deadline.check() + state = self._embed_state + if state is not None and ( + state.prepared_identity == target.identity + or (state.model_id == target.model_id and target.model_id in self._FAST_LANE_SAFE_MODEL_IDS) + ): + return state.model + # A legacy custom label is not evidence that its constructor did not + # fall back. Only strict construction can establish that identity. + state = None + self._publish_embed_state(None) + self._check_process_memory("prepared embed construction", deadline) + device = self._embed_device() + deadline.check() + model = build_target_model(target, device, check=deadline.check) + self._check_process_memory("prepared embed construction completion", deadline) + self._publish_embed_state(_EmbedState( + model=model, tier=target.tier, model_id=target.model_id, + prepared_identity=target.identity, + )) + return model + + def _get_fast_encoder( + self, tier: str, deadline: _RequestDeadline | None = None, *, expected_target: "EmbeddingTarget | None" = None, + ): """CPU-resident encoder for the single-text fast lane (issue #577). Loaded lazily on the first single-text request that finds the global - lock busy, then kept for the server's lifetime. Caller must hold - ``self._fast_lock``. + lock busy, then cached for that main residency. Caller must hold + ``self._fast_lock`` through inference and retire stale cache on release. Vector-space parity (panel rounds 1-2): the fast encoder is ONLY ever built from the main path's actual loaded identity, taken from @@ -376,47 +864,116 @@ def _get_fast_encoder(self, tier: str): state = self._embed_state # single atomic reference read if state is None or state.tier != tier: # No main model loaded for this tier yet — declining means the - # one-time load happens exactly once, on the main path, with - # its usual global sync. + # one-time load happens exactly once, on the main path, + # without changing the daemon's default selection. return None if state.model_id not in self._FAST_LANE_SAFE_MODEL_IDS: return None - if self._fast_encoder is not None and self._fast_model_id == state.model_id: + model_id, generation = state.model_id, state.generation + if expected_target is not None and model_id != expected_target.model_id: + from truememory.embedding_target import EmbeddingTargetError + raise EmbeddingTargetError("Fast encoder residency does not match the serving target") + # A slow CPU constructor must not retain the old main model snapshot. + state = None + if self._fast_encoder is not None and self._fast_generation is generation: + if expected_target is not None and self._fast_model_id != model_id: + from truememory.embedding_target import EmbeddingTargetError + raise EmbeddingTargetError("Fast encoder cache does not match the serving target") return self._fast_encoder - self._fast_encoder = self._build_embed_model(state.model_id, "cpu") - self._fast_model_id = state.model_id + self._fast_encoder = None + self._fast_model_id = None + self._fast_generation = None + self._check_process_memory("fast embed construction", deadline) + model = self._build_embed_model(model_id, "cpu") + self._check_process_memory("fast embed construction completion", deadline) + # A changed generation, including A -> B -> A, must not publish this + # construction. Its already-captured request may still finish on it. + if self._residency_lock.acquire(blocking=False): + try: + state = self._embed_state + if state is not None and state.generation is generation: + self._fast_encoder = model + self._fast_model_id = model_id + self._fast_generation = generation + finally: + self._residency_lock.release() + state = None log.info( "Fast-lane CPU encoder loaded (tier=%s, model=%s) — single-text " - "requests no longer queue behind batch work", tier, state.model_id, + "requests no longer queue behind batch work", tier, model_id, ) - return self._fast_encoder + return model - def _get_reranker(self, model_name: str | None = None): + def _get_reranker( + self, model_name: str | None = None, deadline: _RequestDeadline | None = None, + ): from truememory.reranker import get_current_reranker_name name = model_name or get_current_reranker_name() if self._reranker is not None and self._reranker_name == name: return self._reranker - from sentence_transformers import CrossEncoder - from truememory.mps_utils import auto_detect_device, resolve_device + self._reranker = None + self._reranker_name = None + self._check_process_memory("reranker construction", deadline) + from truememory.mps_utils import auto_detect_device, ensure_mps_memory_budget, resolve_device if "rerank" in self._sticky_cpu: device = "cpu" else: device = resolve_device(auto_detect_device()) - self._reranker = CrossEncoder(name, device=device) + ensure_mps_memory_budget(device) + from sentence_transformers import CrossEncoder + if deadline is not None: + deadline.check() + model = CrossEncoder(name, device=device) + self._check_process_memory("reranker construction completion", deadline) + self._reranker = model self._reranker_name = name log.info("Loaded reranker model=%s device=%s", name, device) return self._reranker - def _handle_fast_embed(self, texts: list, tier: str) -> dict | None: + def _check_serving_embed_model(self, model: object, target: "EmbeddingTarget | None") -> None: + if target is None: + return + from truememory.embedding_target import EmbeddingTargetError + + state = self._embed_state + if state is None or state.model is not model or state.model_id != target.model_id: + raise EmbeddingTargetError("Effective encoder does not match the serving target") + + @staticmethod + def _serving_embed_response(vectors: object, count: int, target: "EmbeddingTarget | None") -> dict: + if target is None: + return {"ok": True, "vectors": vectors} + from truememory.embedding_target import check_target_vectors + + check_target_vectors(target, vectors, count) + return {"ok": True, "vectors": vectors, "target": target.to_wire(), "protocol": PROTOCOL_VERSION} + + @staticmethod + def _serving_reranker_name(value: object) -> str: + import re + + if (type(value) is not str or len(value.encode("utf-8")) > 512 + or re.fullmatch(r"[\w][\w.\-]*(/[\w][\w.\-]*)?", value) is None): + raise _ServingIdentityMismatch("Selected reranking requires an explicit model identity") + return value + + def _check_serving_reranker(self, model: object, expected: str | None) -> None: + if expected is not None and (self._reranker is not model or self._reranker_name != expected): + raise _ServingIdentityMismatch("Effective reranker does not match the serving target") + + def _handle_fast_embed( + self, texts: list, tier: str, deadline: _RequestDeadline | None = None, + *, expected_target: "EmbeddingTarget | None" = None, + ) -> dict | None: """Single-text fast lane (issue #577). - Tries the main path without blocking; when the global lock is busy + Tries the main path without blocking; when its inference owner is busy (a batch encode or OOM recovery is in progress) the text is encoded on a dedicated CPU encoder OUTSIDE the global lock, so hook recall queries never wait on ingestion work. Fast-lane requests skip the @@ -426,34 +983,88 @@ def _handle_fast_embed(self, texts: list, tier: str) -> dict | None: Returns a response dict, or None to fall through to the normal (locked) path. """ - if self._lock.acquire(blocking=False): + deadline = deadline or _RequestDeadline() + deadline.check() + if self._inference_lock.acquire(blocking=False): try: - model = self._get_embed_model(tier) - try: - vectors = model.encode(texts, show_progress_bar=False) - except RuntimeError as exc: - from truememory.mps_utils import is_mps_oom - if not is_mps_oom(exc): - raise - # Same recovery path as the batch handler, atomic in - # this lock hold; a single text on CPU is ms-scale, so - # the retry stays under the lock too. - self._recover_embed_oom_locked(model) - vectors = model.encode(texts, show_progress_bar=False) - return {"ok": True, "vectors": np.asarray(vectors, dtype=np.float32)} + if self._lock.acquire(blocking=False): + model = None + try: + deadline.check() + deadline.start_inference() + self._preflight_embed_result(tier, len(texts)) + deadline.check() + model = self._get_embed_model(tier, deadline=deadline) + self._check_serving_embed_model(model, expected_target) + self._check_process_memory("embed slice", deadline) + deadline.check() + recover = False + try: + vectors = model.encode(texts, batch_size=1, show_progress_bar=False) + except RuntimeError as exc: + from truememory.mps_utils import is_mps_oom + if not is_mps_oom(exc): + raise + recover = True + if recover: + # Leave the exception scope before recovery so the + # failed forward's traceback no longer owns tensors. + self._check_embed_recovery_deadline_locked(model, deadline) + self._recover_embed_oom_locked(model, deadline) + self._check_process_memory("embed CPU retry", deadline) + deadline.check() + vectors = model.encode(texts, batch_size=1, show_progress_bar=False) + return self._serving_embed_response( + _checked_batch(vectors, len(texts), len(texts), "vectors"), len(texts), expected_target, + ) + finally: + model = None + self._lock.release() finally: - self._lock.release() + self._inference_lock.release() + was_started = deadline.transport.started if deadline.transport is not None else False + fast_owned = False try: - with self._fast_lock: - model = self._get_fast_encoder(tier) - if model is None: - # Vector-space parity with the main path cannot be - # guaranteed (custom model) — decline the fast lane. - return None - vectors = model.encode(texts, show_progress_bar=False) - return {"ok": True, "vectors": np.asarray(vectors, dtype=np.float32)} - except Exception: + try: + with deadline.locked(self._fast_lock): + fast_owned = True + model = None + try: + deadline.start_inference() + model = (self._get_fast_encoder(tier, deadline=deadline) if expected_target is None else + self._get_fast_encoder(tier, deadline=deadline, expected_target=expected_target)) + deadline.check() + if model is None: + if deadline.transport is not None: + deadline.transport.started = was_started + # Custom identities decline rather than drift. + return None + self._check_process_memory("fast embed slice", deadline) + deadline.check() + vectors = model.encode(texts, batch_size=1, show_progress_bar=False) + if expected_target is not None: + response = self._serving_embed_response( + _checked_batch(vectors, len(texts), len(texts), "vectors"), len(texts), expected_target, + ) + finally: + model = None + finally: + if fast_owned: + self._retire_stale_fast_encoder() + return (response if expected_target is not None else + {"ok": True, "vectors": _checked_batch(vectors, len(texts), len(texts), "vectors")}) + except (_RequestDeadlineExceeded, _ResultTooLarge, _ProcessMemoryRefused): + raise + except Exception as exc: + if expected_target is not None: + from truememory.embedding_target import EmbeddingTargetError + if isinstance(exc, EmbeddingTargetError): + # An identity/shape rejection cannot be relabelled by a + # later fallback. Other fast-lane failures still retry. + raise + if deadline.transport is not None: + deadline.transport.started = was_started log.warning( "Fast-lane CPU encode failed — falling back to the main " "embed path", exc_info=True, @@ -471,6 +1082,12 @@ def handle_request(self, request: dict) -> dict: self._inflight += 1 try: return self._handle_request_inner(request) + except _RequestDeadlineExceeded as exc: + return {"ok": False, "error": str(exc)} + except _ResultTooLarge as exc: + return {"ok": False, "error": str(exc), "error_code": "result_too_large"} + except _ProcessMemoryRefused as exc: + return {"ok": False, "error": str(exc), "error_code": "server_busy", "retry_after_ms": 250} finally: with self._activity_lock: self._inflight -= 1 @@ -479,35 +1096,58 @@ def handle_request(self, request: dict) -> dict: def _handle_request_inner(self, request: dict) -> dict: op = request.get("op") + # Convert the wire epoch once. Queueing, loading and recovery consume + # one monotonic budget; wall-clock adjustments cannot renew it. + deadline = _RequestDeadline.from_wall_clock(request.get("deadline")) + deadline = _RequestDeadline(deadline.expires_at, getattr(self._transport_context, "request", None)) + deadline.check() + if op == "ping": return {"ok": True} - # Issue #646 (M-44): server-side deadline. The client ships an - # absolute monotonic-equivalent deadline as wall-clock epoch seconds - # ("deadline"). If it has already passed, fail cheap BEFORE acquiring - # the global lock or running a full encode — the client has already - # abandoned the request and fallen back to FTS-only. - deadline = request.get("deadline") - if deadline is not None: + if op in ("embed", "embed_batched", "prepare_embed_target_v1", "embed_target_v1"): + target = None + expected_target = None + preparing = op == "prepare_embed_target_v1" + if op in ("prepare_embed_target_v1", "embed_target_v1"): + from truememory.embedding_target import ( + EmbeddingTarget, TARGET_PROBE_TEXT, check_target_vectors, + ) + target = EmbeddingTarget.from_wire(request.get("target")) + target.check_configuration() + texts = [TARGET_PROBE_TEXT] if preparing else request["texts"] + if not isinstance(texts, list) or not all(isinstance(text, str) for text in texts): + raise ValueError("Prepared embedding inputs must be a list of strings") + empty_target_input = not texts + if empty_target_input: + texts = [TARGET_PROBE_TEXT] + tier = target.tier + else: + texts = request["texts"] + tier = request.get("tier", "") + if "expected_target" in request: + from truememory.embedding_target import EmbeddingTarget, EmbeddingTargetError, check_target_vectors + expected_target = EmbeddingTarget.from_wire(request["expected_target"]) + if expected_target.tier == "custom" or tier != expected_target.model_id: + raise EmbeddingTargetError("Selected serving requires its canonical built-in model identity") + if not isinstance(texts, list) or not all(isinstance(text, str) for text in texts): + raise EmbeddingTargetError("Selected embedding inputs must be a list of strings") try: - if time.time() >= float(deadline): - return {"ok": False, "error": "deadline exceeded before encode"} - except (TypeError, ValueError): - pass - - if op == "embed": - texts = request["texts"] - tier = request.get("tier", "") + batch_limit = _batch_limit(request.get("batch_size", _EMBED_BATCH_LIMIT), + _EMBED_BATCH_LIMIT) + except ValueError as exc: + return {"ok": False, "error": str(exc)} # Single-text fast lane (issue #577): hook recall queries must # never queue behind batch ingestion work or OOM recovery. - if len(texts) <= self._FAST_LANE_MAX_TEXTS: - fast = self._handle_fast_embed(texts, tier) + if target is None and len(texts) <= self._FAST_LANE_MAX_TEXTS: + fast = (self._handle_fast_embed(texts, tier, deadline) if expected_target is None else + self._handle_fast_embed(texts, tier, deadline, expected_target=expected_target)) if fast is not None: return fast now = time.time() - with self._lock: + with deadline.locked(self._lock): self._embed_timestamps.append(now) self._embed_timestamps = [ t for t in self._embed_timestamps @@ -519,101 +1159,212 @@ def _handle_request_inner(self, request: dict) -> dict: ) if should_activate: - with self._lock: + with deadline.locked(self._lock): self._activate_throttler() - # Issue #646 (M-43): capture the throttler to a local under the - # lock. Reading self._throttler_active then self._throttler - # separately raced _deactivate_throttler nulling the attribute - # between the two reads (AttributeError on before_batch()). - with self._lock: - throttler = self._throttler if self._throttler_active else None - if throttler is not None: - throttler.before_batch() - - encode_start = time.time() - # `vectors` is assigned on exactly one of two paths: inside the - # locked try (retry_on_cpu stays False), or by the post-lock - # retry (retry_on_cpu True). Any other outcome raises. - vectors = None - retry_on_cpu = False - with self._lock: - model = self._get_embed_model(tier) + limit, throttler = self._request_batch_limit(batch_limit, deadline) + with deadline.locked(self._inference_lock): + model = None try: - vectors = model.encode(texts, show_progress_bar=False) - except RuntimeError as exc: - from truememory.mps_utils import is_mps_oom - if not is_mps_oom(exc): - raise - # Issue #577: sticky-CPU degradation via the single - # shared recovery path, atomic in this lock hold. The - # model is never re-promoted to MPS (the old re-promotion - # left the MPS pool above its watermark cap and - # guaranteed the next OOM — retry storms). Only the - # expensive full re-encode runs OUTSIDE the lock, so - # other clients are not starved for 23-112s. - self._recover_embed_oom_locked(model) - retry_on_cpu = True - if retry_on_cpu: - log.warning( - "MPS OOM during encoding — retrying on CPU outside the " - "request lock" - ) - vectors = model.encode(texts, show_progress_bar=False) - encode_time = time.time() - encode_start - - # M-43: re-capture under the lock for the same reason as above. - with self._lock: - throttler = self._throttler if self._throttler_active else None - if throttler is not None: - throttler.after_batch(len(texts), encode_time) - if throttler.should_flush_cache(): - self._flush_mps_cache() - - with self._lock: - should_deactivate = self._throttler_active and len(self._embed_timestamps) < 3 - if should_deactivate: - self._deactivate_throttler() - - return {"ok": True, "vectors": np.asarray(vectors, dtype=np.float32)} - - if op == "rerank": + encode_start = time.monotonic() + vectors = None + offset = 0 + order = None + while offset < len(texts) or vectors is None: + retry = None + with deadline.locked(self._lock): + if model is None: + deadline.start_inference() + if target is None: + self._preflight_embed_result(tier, len(texts)) + else: + _check_result_size((len(texts), target.dimension), "vectors") + deadline.check() + model = (self._get_embed_model(tier, deadline=deadline) if target is None + else self._get_prepared_embed_model(target, deadline)) + self._check_serving_embed_model(model, expected_target) + order = self._embed_global_order(model, texts, limit, deadline) + while offset < len(texts) or vectors is None: + deadline.check() + indices = None if order is None else self._embed_slice_indices( + model, texts, order, offset, limit, deadline, + ) + batch = (texts[offset:offset + limit] if indices is None else + [texts[index] for index in indices]) + self._check_process_memory("embed slice", deadline) + deadline.check() + recover = False + try: + values = model.encode(batch, batch_size=limit, show_progress_bar=False) + except RuntimeError as exc: + from truememory.mps_utils import is_mps_oom + if not is_mps_oom(exc): + raise + recover = True + if recover: + self._check_embed_recovery_deadline_locked(model, deadline) + self._recover_embed_oom_locked(model, deadline) + retry = batch + break + if target is not None or expected_target is not None: + check_target_vectors(target or expected_target, values, len(batch)) + vectors = _store_batch_result( + vectors, values, offset, len(batch), len(texts), indices=indices, + ) + offset += len(batch) + if retry is not None: + # Release only the state lock; inference ownership + # still covers the retry and remaining slices. + self._check_process_memory("embed CPU retry", deadline) + deadline.check() + log.warning("MPS OOM during encoding; retrying microbatch on CPU") + values = model.encode(retry, batch_size=limit, show_progress_bar=False) + if target is not None or expected_target is not None: + check_target_vectors(target or expected_target, values, len(retry)) + vectors = _store_batch_result( + vectors, values, offset, len(retry), len(texts), indices=indices, + ) + offset += len(retry) + self._after_request_batches(throttler, len(texts), encode_start, deadline) + + with deadline.locked(self._lock): + should_deactivate = self._throttler_active and len(self._embed_timestamps) < 3 + if should_deactivate: + self._deactivate_throttler() + + if target is not None: + return ({"ok": True, "target": target.to_wire()} if preparing else + {"ok": True, "target": target.to_wire(), + "vectors": vectors[:0] if empty_target_input else vectors}) + return self._serving_embed_response(vectors, len(texts), expected_target) + finally: + model = None + + if op in ("rerank", "rerank_batched"): pairs = request["pairs"] model_name = request.get("model_name") - oom = False - with self._lock: - reranker = self._get_reranker(model_name) + expected_name = None + if "expected_model_name" in request: + expected_name = self._serving_reranker_name(request["expected_model_name"]) + if model_name != expected_name: + raise _ServingIdentityMismatch("Selected reranking requires its explicit model identity") + try: + batch_limit = _batch_limit(request.get("batch_size", _RERANK_BATCH_LIMIT), + _RERANK_BATCH_LIMIT) + except ValueError as exc: + return {"ok": False, "error": str(exc)} + limit, throttler = self._request_batch_limit(batch_limit, deadline) + with deadline.locked(self._inference_lock): + reranker = None try: - scores = reranker.predict( - pairs, batch_size=64, show_progress_bar=False - ) - except RuntimeError as exc: - from truememory.mps_utils import is_mps_oom, flush_mps_cache - if not is_mps_oom(exc): - raise - # Issue #577: the rerank path previously had NO OOM - # handler — a raw MPS OOM reached the client (3/3 rerank - # deaths in the baseline probe). Same sticky-CPU policy - # as embed: drop the MPS-loaded model and reload on CPU. - # Mark + reload happen ATOMICALLY under this same lock - # hold so the retry below always runs on the locally - # reloaded CPU instance (panel round 1, item 1). - self._mark_sticky_cpu("rerank") - flush_mps_cache() - self._reranker = None - self._reranker_name = None - reranker = self._get_reranker(model_name) # sticky → CPU - oom = True - if oom: - # Retry outside the lock — rerank batches are the expensive - # part; other clients must not starve behind the recovery. - scores = reranker.predict( - pairs, batch_size=64, show_progress_bar=False - ) - return {"ok": True, "scores": np.asarray(scores, dtype=np.float32)} + predict_start = time.monotonic() + scores = None + offset = 0 + order = None + recovery_name = model_name + while offset < len(pairs) or scores is None: + retry = None + with deadline.locked(self._lock): + if reranker is None: + deadline.start_inference() + self._preflight_rerank_result(model_name, len(pairs)) + deadline.check() + reranker = self._get_reranker(model_name, deadline=deadline) + self._check_serving_reranker(reranker, expected_name) + order = self._rerank_global_order(reranker, pairs, limit, deadline) + if order is not None: + recovery_name = self._reranker_name + while offset < len(pairs) or scores is None: + deadline.check() + indices = None if order is None else self._rerank_slice_indices( + reranker, pairs, order, offset, limit, deadline, + ) + batch = (pairs[offset:offset + limit] if indices is None else + [pairs[index] for index in indices]) + self._check_process_memory("reranker slice", deadline) + deadline.check() + recover = False + try: + values = reranker.predict(batch, batch_size=limit, show_progress_bar=False) + except RuntimeError as exc: + from truememory.mps_utils import is_mps_oom, flush_mps_cache + if not is_mps_oom(exc): + raise + recover = True + if recover: + self._mark_sticky_cpu("rerank") + self._reranker = None + self._reranker_name = None + # Neither the failed traceback nor this + # local may retain the obsolete accelerator + # model while its CPU replacement is built. + reranker = None + deadline.check() + flush_mps_cache() + deadline.check() + reranker = self._get_reranker(recovery_name, deadline=deadline) + self._check_serving_reranker(reranker, expected_name) + retry = batch + break + if indices is None: + scores = _store_batch_result(scores, values, offset, len(batch), len(pairs), "scores") + else: + scores = _store_batch_result( + scores, values, offset, len(batch), len(pairs), "scores", indices=indices, + ) + offset += len(batch) + if retry is not None: + self._check_process_memory("reranker CPU retry", deadline) + deadline.check() + values = reranker.predict(retry, batch_size=limit, show_progress_bar=False) + if indices is None: + scores = _store_batch_result(scores, values, offset, len(retry), len(pairs), "scores") + else: + scores = _store_batch_result( + scores, values, offset, len(retry), len(pairs), "scores", indices=indices, + ) + offset += len(retry) + self._after_request_batches(throttler, len(pairs), predict_start, deadline) + return ({"ok": True, "scores": scores} if expected_name is None else + {"ok": True, "scores": scores, "protocol": PROTOCOL_VERSION, + "reranker": {"version": 1, "model_name": expected_name}}) + finally: + reranker = None return {"ok": False, "error": f"Unknown op: {op}"} + def _request_batch_limit( + self, limit: int, deadline: _RequestDeadline, + ) -> tuple[int, object | None]: + # Preserve once-per-request sensing outside the model state lock. + # Per-slice sensing needs a scheduler. + # Capture the active instance under the lock, preserving #646's race fix. + with deadline.locked(self._lock): + throttler = self._throttler if self._throttler_active else None + if throttler is not None and not getattr(throttler, "adaptive_applicable", True): + # No applicable sensor policy is not healthy ramp evidence. Keep + # the existing caller/server hard bound without MPS slow-start or + # pacing. Recheck on each request for newly published MPS state. + deadline.check() + return limit, None + if throttler is not None: + deadline.check() + safe_limit, _ = throttler.before_batch() + deadline.check() + limit = _batch_limit(safe_limit, limit) + return limit, throttler + + def _after_request_batches( + self, throttler: object | None, count: int, started: float, + deadline: _RequestDeadline, + ) -> None: + deadline.check() + if throttler is not None: + throttler.after_batch(count, time.monotonic() - started) + if throttler.should_flush_cache(): + deadline.check() + self._flush_mps_cache() + def _activate_throttler(self): """Start adaptive throttling for sustained workload. @@ -661,69 +1412,217 @@ def _deactivate_throttler(self): def _flush_mps_cache(self): """Flush MPS cache — only called when throttler says to.""" try: + from truememory.mps_utils import get_mps_memory_budget import torch - if hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): + if get_mps_memory_budget() is not None and hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): torch.mps.empty_cache() torch.mps.synchronize() except Exception: pass gc.collect() - _CLIENT_TIMEOUT = 30.0 # seconds; caps how long any single client can block + _CLIENT_TIMEOUT = 30.0 + _REJECT_TIMEOUT = 0.25 - def handle_client(self, conn: socket.socket): + def _admit_client(self, conn: socket.socket) -> _TransportRequest | None: + # run() has one accept/staging slot. Direct synchronous users cannot + # create a second staging queue behind it. + if not self._staging_lock.acquire(blocking=False): + conn.close() + return None + admitted = None + accepted_at = time.monotonic() try: - conn.settimeout(self._CLIENT_TIMEOUT) - - # --- HMAC authentication for TCP transport --- + with self._admission_lock: + if self._stopped.is_set(): + return None + self._staging = conn + frame_deadline = accepted_at + self._frame_timeout + header_deadline = min(accepted_at + self._header_timeout, frame_deadline) if not _USE_UNIX: if self._token is None: - # Fail closed: TCP transport must always have a token. - log.warning("TCP client rejected: no HMAC token configured") - conn.close() - return - token_bytes = self._recv_exact(conn, _HMAC_TOKEN_BYTES) - if token_bytes is None: - # Connection closed before sending token (e.g. idle-timeout - # dummy connection). Drop silently -- not a real auth failure. - conn.close() - return - if not hmac.compare_digest(token_bytes, self._token): - log.warning("TCP client failed HMAC authentication") - conn.close() - return - - header = self._recv_exact(conn, _HEADER_SIZE) - if not header: - return + return None + token = self._recv_exact(conn, _HMAC_TOKEN_BYTES, header_deadline) + if token is None or not hmac.compare_digest(token, self._token): + return None + header = self._recv_exact(conn, _HEADER_SIZE, header_deadline) + if header is None: + return None length = struct.unpack(_HEADER_FMT, header)[0] - if length > _MAX_MESSAGE_SIZE: - log.warning( - "Rejecting oversized request (%d bytes, max %d)", - length, - _MAX_MESSAGE_SIZE, - ) + if not 0 < length <= _MAX_MESSAGE_SIZE: + log.warning("Rejecting invalid request frame length (%d bytes)", length) + return None + with self._admission_lock: + if self._stopped.is_set(): + return None + if (len(self._clients) >= self._max_handlers + or self._reserved_request_bytes + length > self._max_request_bytes): + self._busy_rejections += 1 + else: + admitted = _TransportRequest( + length, accepted_at + self._queue_timeout, self._stopped, + frame_deadline, + ) + self._clients[conn] = admitted + self._reserved_request_bytes += length + self._admission_high_water["handlers"] = max( + self._admission_high_water["handlers"], len(self._clients), + ) + self._admission_high_water["request_bytes"] = max( + self._admission_high_water["request_bytes"], self._reserved_request_bytes, + ) + if admitted is None: + self._reject_client(conn, length, "server_busy", "Model server busy: request capacity exhausted") + return admitted + except (OSError, TimeoutError): + return None + finally: + with self._admission_lock: + self._staging = None + self._staging_lock.release() + if admitted is None: conn.close() + + def _reject_client(self, conn: socket.socket, length: int, code: str, message: str) -> None: + expires_at = time.monotonic() + self._REJECT_TIMEOUT + try: + conn.settimeout(self._REJECT_TIMEOUT) + response = {"ok": False, "error_code": code, "error": message} + if code == "server_busy": + response["retry_after_ms"] = 250 + self._send_response(conn, response) + conn.shutdown(socket.SHUT_WR) + # Legacy clients send before reading. Drain without retaining their + # body so closing unread input does not discard the busy response. + while length > 0: + remaining = expires_at - time.monotonic() + if remaining <= 0: + break + conn.settimeout(remaining) + chunk = conn.recv(min(length, 4096)) + if not chunk: + break + length -= len(chunk) + except OSError: + pass + + def _dispatch_client(self, conn: socket.socket) -> Future | None: + admitted = self._admit_client(conn) + if admitted is None: + return None + try: + return self._workers.submit(self._run_admitted_client, conn, admitted) + except (RuntimeError, OSError, MemoryError): + # submit() may enqueue before thread.start fails. Withdraw unclaimed + # leases and disable the executor rather than leaving orphan work. + self._stop_clients() + raise + + def handle_client(self, conn: socket.socket) -> None: + admitted = self._admit_client(conn) + if admitted is not None: + self._run_admitted_client(conn, admitted) + + def _run_admitted_client(self, conn: socket.socket, admitted: _TransportRequest) -> None: + with self._admission_lock: + if admitted.withdrawn: return - data = self._recv_exact(conn, length) - if not data: - return + admitted.claimed = True + try: + self._serve_client(conn, admitted) + finally: + # The serving frame and exception traceback have returned. No + # decoded request, raw body, or response survives credit release. + self._release_client(conn) + + def _release_client(self, conn: socket.socket) -> None: + try: + conn.close() + finally: + with self._admission_lock: + admitted = self._clients.pop(conn, None) + if admitted is not None: + self._reserved_request_bytes -= admitted.length + with self._activity_lock: + self._last_activity = time.time() + def _serve_client(self, conn: socket.socket, admitted: _TransportRequest) -> None: + data = request = response = None + try: + data = self._recv_exact(conn, admitted.length, admitted.frame_expires_at) + if data is None: + return request = json.loads(data, object_hook=_json_object_hook) + data = None + if not isinstance(request, dict): + raise ValueError("Request must be a JSON object") + self._transport_context.request = admitted response = self.handle_request(request) + conn.settimeout(self._CLIENT_TIMEOUT) self._send_response(conn, response) - except Exception as e: + except Exception as error: try: - self._send_response(conn, {"ok": False, "error": str(e)}) + conn.settimeout(self._REJECT_TIMEOUT) + failure = {"ok": False, "error": str(error)} + identity_error = getattr(type(error), "serving_identity_mismatch", False) is True + if isinstance(request, dict) and ( + "expected_target" in request or request.get("op") in ("prepare_embed_target_v1", "embed_target_v1") + ): + from truememory.embedding_target import EmbeddingTargetError + identity_error = identity_error or isinstance(error, EmbeddingTargetError) + if identity_error: + failure["error_code"] = "serving_identity_mismatch" + self._send_response(conn, failure) except Exception: + # The connection is already failing. Keep serialization errors + # inside this boundary so their tracebacks do not retain input. pass finally: - conn.close() + self._transport_context.request = None + data = request = response = None + + def _stop_clients(self) -> None: + self._running = False + self._stopped.set() + with self._admission_lock: + connections = list(self._clients) + if self._staging is not None: + connections.append(self._staging) + for conn, admitted in list(self._clients.items()): + if not admitted.claimed: + admitted.withdrawn = True + del self._clients[conn] + self._reserved_request_bytes -= admitted.length + for conn in connections: + try: + conn.shutdown(socket.SHUT_RDWR) + except OSError: + pass + self._workers.shutdown(wait=False, cancel_futures=True) + for conn in connections: + with self._admission_lock: + retained = conn in self._clients + if not retained: + conn.close() - def _recv_exact(self, conn: socket.socket, n: int) -> bytes | None: + def _recv_exact(self, conn: socket.socket, n: int, expires_at: float | None = None) -> bytes | None: buf = bytearray() while len(buf) < n: - chunk = conn.recv(n - len(buf)) + if self._stopped.is_set(): + return None + if expires_at is not None: + remaining = expires_at - time.monotonic() + if remaining <= 0: + raise TimeoutError("request frame receive timed out: deadline exceeded") + # Cross-thread shutdown does not reliably interrupt recv on + # every platform. Poll shutdown without extending the frame budget. + conn.settimeout(min(remaining, 0.1)) + try: + chunk = conn.recv(min(n - len(buf), 65536)) + except TimeoutError: + if expires_at is None: + raise + continue if not chunk: return None buf.extend(chunk) @@ -735,9 +1634,9 @@ def _send_response(self, conn: socket.socket, response: dict): # an unexpected payload shape. if "protocol" not in response: response = {**response, "protocol": PROTOCOL_VERSION} + if _response_wire_size(response) > _MAX_MESSAGE_SIZE: + response = {"ok": False, "error": "Response too large", "protocol": PROTOCOL_VERSION} data = json.dumps(response, default=_json_default).encode("utf-8") - if len(data) > _MAX_MESSAGE_SIZE: - data = json.dumps({"ok": False, "error": "Response too large"}).encode("utf-8") header = struct.pack(_HEADER_FMT, len(data)) conn.sendall(header + data) @@ -746,18 +1645,23 @@ def _idle_checker(self): time.sleep(60) if not self._running: break - with self._activity_lock: - last = self._last_activity - inflight = self._inflight - elapsed = time.time() - last + with self._admission_lock: + admitted = bool(self._clients) or self._staging is not None + with self._activity_lock: + last = self._last_activity + inflight = self._inflight + elapsed = time.time() - last + should_stop = elapsed >= IDLE_TIMEOUT and inflight == 0 and not admitted + if should_stop: + self._running = False + self._stopped.set() # Issue #646 (M-74): never idle-shut-down while a request is # mid-flight, even if its start timestamp is older than the idle # horizon (a long batch encode can outlast IDLE_TIMEOUT). - if elapsed >= IDLE_TIMEOUT and inflight == 0: + if should_stop: log.info( "Idle timeout (%.0fs), shutting down model server", elapsed ) - self._running = False # Send a dummy connection to unblock accept(). try: if _USE_UNIX: @@ -848,20 +1752,30 @@ def _acquire_bind_lock(self): Returns the lock fd, or raises ``RuntimeError`` if another live server holds the lock. """ - fd = os.open(str(LOCK_PATH), os.O_CREAT | os.O_RDWR, 0o644) - try: - if sys.platform == "win32": - import msvcrt - msvcrt.locking(fd, msvcrt.LK_NBLCK, 1) - else: - import fcntl - fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB) - except OSError: - os.close(fd) + fd = try_file_lock(LOCK_PATH) + if fd is None: raise RuntimeError("another model server holds the bind lock") return fd + def _validate_launch_generation_locked(self) -> None: + """A superseded managed child must not publish endpoint artifacts.""" + generation = os.environ.get(_MODEL_SERVER_GENERATION_ENV) + if generation is None: + return # Explicit CLI launch still uses the lifetime bind lock. + try: + claim = read_start_claim(START_STATE_PATH) + except OSError as error: + os.close(self._lock_fd) + self._lock_fd = None + raise RuntimeError("Cannot verify model-server launch generation") from error + if claim is None or claim["generation"] != generation: + os.close(self._lock_fd) + self._lock_fd = None + raise RuntimeError("model-server launch was superseded; retry the request") + def run(self): + if getattr(self, "_cleaned_up", False): + raise RuntimeError("a stopped model server cannot be restarted in the same instance") # M-89: keep ~/.truememory owner-only (0700) — it holds memories/PII. _TRUEMEMORY_DIR.mkdir(parents=True, exist_ok=True) try: @@ -871,6 +1785,7 @@ def run(self): # Exclusive bind lock BEFORE touching socket/pid artifacts (M-20). self._lock_fd = self._acquire_bind_lock() + self._validate_launch_generation_locked() if _USE_UNIX: # We hold the exclusive lock, so any socket file here is stale @@ -922,10 +1837,7 @@ def run(self): if not self._running: conn.close() break - t = threading.Thread( - target=self.handle_client, args=(conn,), daemon=True - ) - t.start() + self._dispatch_client(conn) finally: srv.close() self._cleanup() @@ -934,33 +1846,47 @@ def _cleanup(self): if getattr(self, "_cleaned_up", False): return self._cleaned_up = True + self._stop_clients() + with self._admission_lock: + with self._activity_lock: + retained_work = bool(self._clients) or self._inflight > 0 + if retained_work: + # Native inference cannot be preempted. Keep model references and + # bind ownership until OS exit so another daemon cannot overlap + # that work. Cleanup remains nonblocking and the instance cannot + # be restarted; stale endpoint files are reclaimed by the next + # bind owner after this process exits. + log.info("Shutdown retains model-server ownership until process exit") + return + # Keep bind ownership until every cached model has been released, + # including cycles collected below. If teardown fails, retain the + # lock until process exit instead of overlapping a successor's load. + self._embed_state = None + self._reranker = None + self._reranker_name = None + self._fast_encoder = None + self._fast_model_id = None + self._fast_generation = None + self._token = None + gc.collect() # Only remove artifacts THIS process owns (issue #646, M-20). After a # crash a fresh server may already hold the lock and have rewritten # PID_PATH; unlinking its live socket/token here would let concurrent # hooks cycle servers indefinitely. - if _safe_to_cleanup_artifacts(): + lock_fd = getattr(self, "_lock_fd", None) + if lock_fd is not None and _safe_to_cleanup_artifacts(): for p in (SOCK_PATH, PID_PATH, PORT_PATH, TOKEN_PATH): try: p.unlink(missing_ok=True) except OSError: pass # Release the bind lock (lock fd is process-owned, always safe). - lock_fd = getattr(self, "_lock_fd", None) if lock_fd is not None: try: os.close(lock_fd) except OSError: pass self._lock_fd = None - # Reset the embed snapshot and fast-lane identity so no stale model - # identity survives a stop (issue #577, panel round 2). - self._embed_state = None - self._reranker = None - self._reranker_name = None - self._fast_encoder = None - self._fast_model_id = None - self._token = None - gc.collect() log.info("Model server stopped") @@ -986,21 +1912,9 @@ def main(): if hasattr(signal, "SIGHUP"): signal.signal(signal.SIGHUP, _handle_signal) - # Fast pre-check: a live PID means a server is (probably) already up. - # This is advisory only — the authoritative guard is the exclusive bind - # lock taken inside run() (issue #646, M-20), which closes the TOCTOU - # window this check alone left open. Stale artifacts from a crashed - # predecessor are reclaimed under the lock in run(), not here. - if PID_PATH.exists(): - try: - old_pid = int(PID_PATH.read_text().strip()) - if pid_is_alive(old_pid): - log.error("Model server already running (pid=%d)", old_pid) - sys.exit(1) - except (ValueError, OSError): - pass - - server = ModelServer() + # A PID file can outlive its process and point at an unrelated live PID. + # Only run()'s exclusive bind lock decides endpoint ownership. + server = ModelServer(automatic_rss_budget=True) # Ensure cleanup runs even on unhandled exit. atexit.register(server._cleanup) diff --git a/truememory/mps_utils.py b/truememory/mps_utils.py index 0ecc1d19..e8a9e30e 100644 --- a/truememory/mps_utils.py +++ b/truememory/mps_utils.py @@ -8,15 +8,209 @@ import gc import logging +import math import os import threading +from _thread import LockType +from collections.abc import Callable, Iterator +from contextlib import contextmanager +from dataclasses import dataclass, field logger = logging.getLogger(__name__) _device_lock = threading.Lock() + +@dataclass +class _ModelOwnership: + lock: LockType = field(default_factory=threading.Lock) + users: int = 0 + + +# Entries live only while a caller owns or waits for that exact instance. The +# caller keeps the model alive, so its id cannot be reused during this interval. +_model_owners: dict[int, _ModelOwnership] = {} + + +@contextmanager +def _own_model( + model: object, *, remaining: Callable[[], float | None] | None = None, +) -> Iterator[None]: + @contextmanager + def locked(lock: LockType) -> Iterator[None]: + if remaining is None: + with lock: + yield + return + budget = remaining() + if budget is None: + lock.acquire() + elif budget <= 0 or not lock.acquire(timeout=min(budget, threading.TIMEOUT_MAX)): + raise TimeoutError("Model ownership deadline exceeded") + try: + remaining() + yield + finally: + lock.release() + + key = id(model) + with locked(_device_lock): + owner = _model_owners.get(key) + if owner is None: + owner = _ModelOwnership() + _model_owners[key] = owner + owner.users += 1 + try: + with locked(owner.lock): + yield + finally: + # Cleanup cannot abandon a registered user when its request expires. + # The registry lock covers bookkeeping only, never native work. + with _device_lock: + owner.users -= 1 + if owner.users == 0: + del _model_owners[key] + + _VALID_DEVICE_VALUES = ("cpu", "mps", "cuda", "auto") +_GIB = 1024**3 +_MPS_HIGH_ENV = "PYTORCH_MPS_HIGH_WATERMARK_RATIO" +_MPS_LOW_ENV = "PYTORCH_MPS_LOW_WATERMARK_RATIO" +_mps_budget_lock = threading.Lock() + + +class MPSBudgetError(RuntimeError): + """MPS model loading cannot proceed with an unverified allocation policy.""" + + +@dataclass(frozen=True) +class MPSMemoryBudget: + """HIGH setter result; LOW records bootstrap intent, not verified native state.""" + + intended_bytes: int | None + recommended_bytes: int + effective_bytes: int | None + fraction: float + requested_low_watermark_ratio: float + source: str + enforcement: str + + +_mps_budget: MPSMemoryBudget | None = None +_mps_budget_failure: str | None = None + + +def get_mps_memory_budget() -> MPSMemoryBudget | None: + """Read this process's successfully configured policy without importing torch.""" + return _mps_budget + + +def _positive_bytes(value: object, name: str) -> int: + if isinstance(value, bool) or not isinstance(value, int) or value <= 0: + raise MPSBudgetError(f"{name} must report a positive integer byte count") + return value + + +def _default_mps_budget_bytes(physical_bytes: int) -> int: + physical_bytes = _positive_bytes(physical_bytes, "Physical memory") + target = min(0.08 * physical_bytes, 2.5 * _GIB) if physical_bytes >= 16 * _GIB else 0.19 * physical_bytes + return int(max(target, 1.5 * _GIB)) + + +def _watermark_ratio(name: str, value: str) -> float: + try: + ratio = float(value) + except ValueError as exc: + raise MPSBudgetError(f"{name} must be a finite ratio between 0 and 2") from exc + if not math.isfinite(ratio) or not 0 <= ratio <= 2: + raise MPSBudgetError(f"{name} must be a finite ratio between 0 and 2") + return ratio + + +def ensure_mps_memory_budget(device: str | None) -> MPSMemoryBudget | None: + """Configure the allocator once, before constructing this process's MPS models. + + ``None`` preserves framework auto-selection at the constructor; detection + here only determines whether MPS calibration is needed. CPU/CUDA callers + do not touch the MPS allocator or its environment settings. + """ + selected = auto_detect_device() if device is None else str(device) + if selected.split(":", 1)[0] != "mps": + return None + + global _mps_budget, _mps_budget_failure + with _mps_budget_lock: + if _mps_budget is not None: + return _mps_budget + if _mps_budget_failure is not None: + raise MPSBudgetError(_mps_budget_failure) + try: + high_text = os.environ.get(_MPS_HIGH_ENV) + high = _watermark_ratio(_MPS_HIGH_ENV, high_text) if high_text is not None else None + low = _watermark_ratio(_MPS_LOW_ENV, os.environ.get(_MPS_LOW_ENV, "0.0")) + bootstrap_high = high if high is not None else 1.7 + if low > (2.0 if bootstrap_high == 0 else bootstrap_high): + raise MPSBudgetError("MPS low watermark exceeds the initial high watermark") + + # recommended_max_memory() itself initializes the allocator. Keep + # the existing low=0 policy before that query; no public low setter + # exists. Explicit operator values always take precedence. An + # embedding host may already have initialized LOW; this setting + # records bootstrap intent and cannot verify that native state. + os.environ.setdefault(_MPS_LOW_ENV, "0.0") + import torch + + if not (hasattr(torch.backends, "mps") and torch.backends.mps.is_available()): + raise MPSBudgetError("MPS is unavailable for allocation-budget calibration") + recommended = getattr(torch.mps, "recommended_max_memory", None) + setter = getattr(torch.mps, "set_per_process_memory_fraction", None) + if not callable(recommended) or not callable(setter): + raise MPSBudgetError( + "PyTorch lacks the public MPS allocation-budget APIs; MPS requires " + "PyTorch>=2.5. Upgrade to a compatible build if available" + ) + + intended = None + if high is None: + import psutil + intended = _default_mps_budget_bytes(psutil.virtual_memory().total) + recommended_bytes = _positive_bytes(recommended(), "MPS recommended memory") + if high is not None: + fraction = high + else: + assert intended is not None + fraction = intended / recommended_bytes + if not math.isfinite(fraction) or not 0 <= fraction <= 2 or (fraction == 0 and high is None): + raise MPSBudgetError("Intended MPS byte budget is outside PyTorch's supported ratio range") + if fraction != 0 and low > fraction: + raise MPSBudgetError("MPS low watermark exceeds the calibrated high watermark") + effective = int(fraction * recommended_bytes) if fraction else None + if fraction and not effective: + raise MPSBudgetError("MPS allocation budget rounds down to zero bytes") + if high is not None: + intended = effective + budget = MPSMemoryBudget( + intended, recommended_bytes, effective, fraction, low, + "operator" if high is not None else "automatic", + "enforced" if fraction else "disabled", + ) + setter(float(fraction)) + except (ImportError, AttributeError, OSError, RuntimeError, TypeError, ValueError, OverflowError) as exc: + detail = str(exc) if isinstance(exc, MPSBudgetError) else type(exc).__name__ + _mps_budget_failure = ( + "Cannot configure MPS allocation budget: " + detail + + ". Correct the policy or select TRUEMEMORY_DEVICE=cpu, then restart this process." + ) + raise MPSBudgetError(_mps_budget_failure) from exc + + # A partially initialized allocator is never advertised as configured. + _mps_budget = budget + logger.info("MPS allocation budget: source=%s intended_bytes=%s recommended_bytes=%d effective_bytes=%s fraction=%.8g enforcement=%s", + budget.source, budget.intended_bytes, budget.recommended_bytes, + budget.effective_bytes, budget.fraction, budget.enforcement) + return budget + def auto_detect_device() -> str: """Auto-detect the best torch device: cuda -> mps -> cpu. @@ -101,7 +295,7 @@ def flush_mps_cache() -> None: """Flush MPS/CUDA cache and run garbage collection.""" try: import torch - if hasattr(torch, "mps") and hasattr(torch.mps, "empty_cache"): + if get_mps_memory_budget() is not None and hasattr(torch, "mps") and hasattr(torch.mps, "empty_cache"): torch.mps.empty_cache() if hasattr(torch, "cuda"): torch.cuda.empty_cache() @@ -110,31 +304,35 @@ def flush_mps_cache() -> None: gc.collect() -def encode_with_mps_fallback(model, texts, **kwargs): - """Encode texts, falling back to CPU if MPS runs out of memory. +def encode_with_model_ownership(model: object, texts: object, **kwargs: object) -> object: + """Serialize local inference while preserving the caller's recovery policy.""" + from truememory.model_client import EmbeddingProxy - Thread-safe: acquires _device_lock around model.to() transitions. - Restores model to MPS after successful CPU fallback. - """ - try: + if isinstance(model, EmbeddingProxy): return model.encode(texts, **kwargs) - except RuntimeError as exc: - if not is_mps_oom(exc): - raise - logger.warning("MPS OOM during encoding — flushing cache and retrying on CPU") - flush_mps_cache() + with _own_model(model): + return model.encode(texts, **kwargs) + + +def encode_with_mps_fallback(model: object, texts: object, **kwargs: object) -> object: + """Own a local model through inference and recovery; keep recovered models on CPU.""" + from truememory.model_client import EmbeddingProxy + + if isinstance(model, EmbeddingProxy): + # The daemon owns native inference. Serializing its proxy here would + # prevent recall from using the daemon's independent fast lane. + return model.encode(texts, **kwargs) + + with _own_model(model): + try: + return model.encode(texts, **kwargs) + except RuntimeError as exc: + if not is_mps_oom(exc): + raise + logger.warning("MPS OOM during local encoding; retrying on CPU without MPS re-promotion") + # Leave the exception block first so the failed forward traceback can + # release its tensors. Move live weights before flushing freed storage. if hasattr(model, "to"): - with _device_lock: - model.to("cpu") - try: - result = model.encode(texts, **kwargs) - finally: - try: - import torch - if hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): - with _device_lock: - model.to("mps") - except Exception: - pass - return result + model.to("cpu") + flush_mps_cache() return model.encode(texts, **kwargs) diff --git a/truememory/personality.py b/truememory/personality.py index 6317528e..25beb870 100644 --- a/truememory/personality.py +++ b/truememory/personality.py @@ -8,8 +8,8 @@ returns nothing useful. This module pre-computes entity profiles (communication style, topics, -relationships, traits, preferences) so that personality questions can be -answered from structured data rather than raw message search alone. +relationships and traits). Preference questions use query-time retrieval; +scheduled preference-map persistence and refresh are not implemented. Personality categories handled: - General character (``"What kind of person is X?"``) @@ -24,14 +24,22 @@ """ import contextlib +import hashlib import json import re import sqlite3 +import struct +import uuid import warnings from collections import defaultdict +from collections.abc import Iterable, Iterator from datetime import datetime, timezone from truememory.fts_search import _build_safe_fts_query, _fts_search +from truememory.personality_style_vec import ( + _l0_directive_filter, _l0_source_schema, _l0_transaction, _l0_validate_source, +) +from truememory.storage import _dunbar_ownership_ready, _initialize_dunbar_ownership # --------------------------------------------------------------------------- @@ -142,20 +150,16 @@ # Internal helpers # --------------------------------------------------------------------------- -def _get_all_messages(conn: sqlite3.Connection) -> list[dict]: - """Fetch all messages ordered by timestamp.""" - rows = conn.execute( - "SELECT id, content, sender, recipient, timestamp, category, modality " - "FROM messages ORDER BY timestamp" - ).fetchall() - return [ - { - "id": r[0], "content": r[1], "sender": r[2], - "recipient": r[3], "timestamp": r[4], - "category": r[5], "modality": r[6], - } - for r in rows - ] +_PROFILE_SOURCE_COLUMNS = ( + "id", "content", "sender", "recipient", "timestamp", "category", "modality", +) + + +def _profile_source_query(schema: tuple[tuple, ...]) -> str: + return ( + "SELECT id, content, sender, recipient, timestamp, category, modality, " + f"{_l0_directive_filter(schema)} AS ordinary FROM messages ORDER BY timestamp" + ) def _get_messages_by_sender(conn: sqlite3.Connection, sender: str, @@ -438,32 +442,7 @@ def _warnings_ctx(): yield -def build_entity_profiles(conn: sqlite3.Connection) -> dict: - """ - Analyze all messages and build personality profiles for key entities. - - For each entity (sender), extracts: - - - **message_count**: total messages sent. - - **topics**: frequent themes (startup, health, food, etc.). - - **communication_style**: average message length, emoji usage, - formality level, and typical greeting. - - **relationships**: who they message most and approximate topic focus - per recipient. - - **traits**: personality descriptors inferred from content analysis. - - Results are stored in the ``entity_profiles`` table and also returned - as a dict keyed by entity name. - - Args: - conn: Open database connection (from :func:`truememory.storage.create_db`). - - Returns: - ``{entity: profile_dict}`` for every sender who has at least one - message in the database. - """ - all_msgs = _get_all_messages(conn) - +def _compute_entity_profiles(all_msgs: list[dict]) -> dict[str, dict]: # Group messages by sender (normalized to lowercase for case-insensitive matching) by_sender: dict[str, list[dict]] = defaultdict(list) for msg in all_msgs: @@ -534,25 +513,72 @@ def build_entity_profiles(conn: sqlite3.Connection) -> dict: } profiles[sender] = profile - # Store in database - now = datetime.now(timezone.utc).isoformat() - conn.execute( - """INSERT OR REPLACE INTO entity_profiles + return profiles + + +def build_entity_profiles(conn: sqlite3.Connection) -> dict: + """ + Analyze all messages and build personality profiles for key entities. + + For each entity (sender), extracts: + + - **message_count**: total messages sent. + - **topics**: frequent themes (startup, health, food, etc.). + - **communication_style**: average message length, emoji usage, + formality level, and typical greeting. + - **relationships**: who they message most and approximate topic focus + per recipient. + - **traits**: personality descriptors inferred from content analysis. + + Results are stored in the ``entity_profiles`` table and also returned + as a dict keyed by entity name. + + Args: + conn: Open database connection (from :func:`truememory.storage.create_db`). + + Returns: + ``{entity: profile_dict}`` for every sender who has at least one + ordinary message in the database. + + CPU computation precedes the builder's write transaction. Publication + checks the exact source and atomically replaces the table, including + obsolete entities. Changed source raises ``sqlite3.OperationalError``. + Existing caller transactions are preserved. + """ + with _l0_transaction(conn): + source_token = _l0_tracked_source(conn) + schema = _l0_source_schema(conn) + query = _profile_source_query(schema) + source_rows = [tuple(row) for row in conn.execute(query)] + # Filtering via the directive index can reorder timestamp ties. Keep the + # original scan order and remove ineligible rows before computation. + all_msgs = [dict(zip(_PROFILE_SOURCE_COLUMNS, row[:-1])) for row in source_rows if row[-1]] + profiles = _compute_entity_profiles(all_msgs) + now = datetime.now(timezone.utc).isoformat() + stored_rows = [ + ( + sender, profile["message_count"], json.dumps(profile["traits"]), + json.dumps(profile["communication_style"]), json.dumps(profile["topics"]), + json.dumps(profile["relationships"]), now, + ) + for sender, profile in profiles.items() + ] + with _l0_transaction(conn, write=True): + _l0_validate_source(conn, schema, query, source_rows) + if _l0_tracked_source(conn) != source_token: + raise ProfileRebuildRequired("Profile source revision changed during computation") + if source_token is not None: + _prepare_profile_certificate(conn) + conn.execute("DELETE FROM entity_profiles") + conn.executemany( + """INSERT INTO entity_profiles (entity, message_count, traits, communication_style, topics, relationships, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)""", - ( - sender, - len(messages), - json.dumps(traits), - json.dumps(comm_style), - json.dumps(topics), - json.dumps(relationships), - now, - ), + stored_rows, ) - - conn.commit() + if source_token is not None: + _write_profile_certificate(conn, source_token) return profiles @@ -578,7 +604,9 @@ def extract_preferences(conn: sqlite3.Connection, Returns: Dict with keys ``food``, ``activities``, ``people``, ``routines``, ``fears``, ``values``. Each value is a list of extracted snippets - or structured data. + or structured data. With no entity, returns an empty dict without + reading messages. Results are returned only, never persisted; this + helper is not a scheduled preference refresh. """ if entity is None: return {} @@ -907,6 +935,172 @@ def search_personality(conn: sqlite3.Connection, query: str, return results[:limit] +_PROFILE_TOKEN_KEY = "entity_profile_source_v1" +_PROFILE_OUTPUT_TRIGGERS = { + "truememory_profile_output_" + operation.lower(): + "CREATE TRIGGER truememory_profile_output_" + operation.lower() + + " AFTER " + operation + " ON entity_profiles BEGIN " + + "UPDATE metadata SET value='unverified' WHERE key='" + _PROFILE_TOKEN_KEY + "'; END" + for operation in ("INSERT", "UPDATE", "DELETE") +} + + +class ProfileRebuildRequired(sqlite3.OperationalError): + """A retained generation cannot be read or extended until full repair.""" + + +_PROFILE_SOURCE_TRIGGER_SHA256 = { + "messages_maintenance_ai": "b75f5431c0ce50627341f9afa36b4ad8920a56a561e19f42b3c6fd5d6a08c778", + "messages_maintenance_au": "e9626141915f0588897aaf24c97e184e0aeb47f8c3f1eb712c346638a9287749", + "messages_maintenance_unique_au": "3d8c28d2a9351bfaeed175dc6d058abaa77b7f0b64ab6b0c9e0e6999660fea1b", + "messages_maintenance_ad": "5d0f978eddd7fbad96c49b0c95942ea1d69526ed102bd0afb8229eb195263c0e", +} + + +def _profile_utf8_size(value: str) -> int: + try: + return len(value.encode("utf-8")) + except UnicodeEncodeError as error: + raise ProfileRebuildRequired("Profile proof text is not UTF-8") from error + + +def _l0_tracked_source(conn: sqlite3.Connection) -> tuple | None: + """Return a canonical source proof or preserve a genuinely untracked schema.""" + if conn.execute("SELECT 1 FROM main.sqlite_master WHERE type='table' " + "AND name='maintenance_source_state'").fetchone() is None: + return None + if conn.execute("SELECT 1 FROM temp.sqlite_master WHERE type IN ('table','view') " + "AND lower(name) IN ('messages','maintenance_source_state','metadata') LIMIT 1").fetchone(): + raise ProfileRebuildRequired("L0 source tracking is shadowed") + row = conn.execute("SELECT epoch,revision,insert_count,correction_count,max_seen_message_id," + "nonappend_revision,tracking_ready FROM main.maintenance_source_state WHERE singleton=1").fetchone() + if (row is None or row[6] != 1 or not isinstance(row[0], str) or not row[0] + or _profile_utf8_size(row[0]) > 256 + or any(type(value) is not int or value < 0 for value in row[1:6]) + or row[1] != row[2] + row[3] or row[5] > row[1]): + raise ProfileRebuildRequired("L0 source revision is unavailable") + schema = list(conn.execute("PRAGMA main.table_xinfo(messages)")) + identifiers = [column for column in schema if column[5]] + definition = conn.execute("SELECT sql FROM main.sqlite_master WHERE type='table' AND name='messages'").fetchone() + if (len(identifiers) != 1 or identifiers[0][1:3] != ('id', 'INTEGER') + or definition is None or 'WITHOUT ROWID' in ' '.join(definition[0].upper().split())): + raise ProfileRebuildRequired("L0 source identity is unavailable") + expected = _PROFILE_SOURCE_TRIGGER_SHA256 + names = tuple(expected) + installed = {name: hashlib.sha256(' '.join(sql.strip().rstrip(';').split()).encode()).hexdigest() + for name, sql in conn.execute("SELECT name,sql FROM main.sqlite_master WHERE type='trigger' " + "AND name IN (?,?,?,?)", names)} + if installed != expected or conn.execute("SELECT 1 FROM temp.sqlite_master WHERE type='trigger' " + "AND (lower(tbl_name)='messages' OR name IN (?,?,?,?)) LIMIT 1", names).fetchone(): + raise ProfileRebuildRequired("L0 source triggers are unavailable") + return tuple(row[:6]) + + +def _profile_trigger_proof(conn: sqlite3.Connection, *, prepare: bool = False) -> bool: + names = tuple(_PROFILE_OUTPUT_TRIGGERS) + if conn.execute("SELECT 1 FROM temp.sqlite_master WHERE type IN ('table','view') " + "AND lower(name) IN ('entity_profiles','metadata') LIMIT 1").fetchone(): + raise ProfileRebuildRequired("Profile tables are shadowed") + installed = {name: " ".join(sql.strip().rstrip(';').split()) for name, sql in conn.execute( + "SELECT name,sql FROM main.sqlite_master WHERE type='trigger' AND name IN (?,?,?)", names)} + expected = {name: " ".join(sql.split()) for name, sql in _PROFILE_OUTPUT_TRIGGERS.items()} + if conn.execute("SELECT 1 FROM main.sqlite_master WHERE type='trigger' " + "AND lower(tbl_name) IN ('entity_profiles','metadata') AND name NOT IN (?,?,?) LIMIT 1", names).fetchone(): + raise ProfileRebuildRequired("Profile output triggers are unsupported") + if conn.execute("SELECT 1 FROM temp.sqlite_master WHERE type='trigger' " + "AND (lower(tbl_name) IN ('entity_profiles','metadata') OR name IN (?,?,?)) LIMIT 1", names).fetchone(): + raise ProfileRebuildRequired("Profile output triggers are shadowed") + if any(sql != expected[name] for name, sql in installed.items()): + raise ProfileRebuildRequired("Profile ownership definition changed") + if prepare: + for name, sql in _PROFILE_OUTPUT_TRIGGERS.items(): + if name not in installed: + conn.execute(sql) + return True + return installed == expected + + +def _prepare_profile_certificate(conn: sqlite3.Connection) -> None: + _profile_trigger_proof(conn, prepare=True) + + +def _write_profile_certificate(conn: sqlite3.Connection, source: tuple) -> None: + value = json.dumps({"schema_version": 1, "coverage": "complete", "source": list(source)}, + sort_keys=True, separators=(",", ":"), allow_nan=False) + conn.execute("INSERT INTO main.metadata(key,value) VALUES(?,?) " + "ON CONFLICT(key) DO UPDATE SET value=excluded.value", (_PROFILE_TOKEN_KEY, value)) + + +def _profile_json_object(pairs: list[tuple[str, object]]) -> dict[str, object]: + result: dict[str, object] = {} + for key, value in pairs: + if key in result: + raise ProfileRebuildRequired("Profile source token has duplicate keys") + result[key] = value + return result + + +def _profile_read_state(conn: sqlite3.Connection) -> tuple[tuple | None, tuple | None]: + source = _l0_tracked_source(conn) + names = tuple(_PROFILE_OUTPUT_TRIGGERS) + enrolled = conn.execute("SELECT 1 FROM main.sqlite_master WHERE type='trigger' " + "AND name IN (?,?,?) LIMIT 1", names).fetchone() is not None + metadata = conn.execute("SELECT 1 FROM main.sqlite_master WHERE type='table' AND name='metadata'").fetchone() + row = conn.execute("SELECT value FROM main.metadata WHERE key=?", (_PROFILE_TOKEN_KEY,)).fetchone() if metadata else None + if not enrolled and row is None: + if source is not None and (source[3] or source[5]): + raise ProfileRebuildRequired("Uncertified profiles have correction history") + return source, None + if source is None or row is None or not _profile_trigger_proof(conn): + raise ProfileRebuildRequired("Profile source proof is unavailable") + value = row[0] + if not isinstance(value, str) or _profile_utf8_size(value) > 2048: + raise ProfileRebuildRequired("Profile source token is unsupported") + try: + token = json.loads(value, object_pairs_hook=_profile_json_object) + except json.JSONDecodeError as error: + raise ProfileRebuildRequired("Profile source token is invalid") from error + if (not isinstance(token, dict) or set(token) != {"schema_version", "coverage", "source"} + or type(token["schema_version"]) is not int or token["schema_version"] != 1 + or token["coverage"] != "complete" or not isinstance(token["source"], list) + or len(token["source"]) != 6): + raise ProfileRebuildRequired("Profile source token is unsupported") + previous = tuple(token["source"]) + if (not isinstance(previous[0], str) or previous[0] != source[0] + or any(type(value) is not int or value < 0 for value in previous[1:]) + or previous[1] != previous[2] + previous[3] or previous[5] > previous[1] + or previous[1] > source[1] or previous[2] > source[2] or previous[4] > source[4] + or previous[3] != source[3] or source[5] > previous[1]): + raise ProfileRebuildRequired("Profile generation requires correction repair") + return source, previous + + +@contextlib.contextmanager +def _profile_append_guard(conn: sqlite3.Connection, sender: str, content: str, + recipient: str) -> Iterator[None]: + source, previous = _profile_read_state(conn) + if source is None: + yield + return + with _l0_transaction(conn, write=True): + source, previous = _profile_read_state(conn) + if previous is not None: + if (source[1] != previous[1] + 1 or source[2] != previous[2] + 1 + or source[4] <= previous[4]): + raise ProfileRebuildRequired("Profile append lacks one exact source occurrence") + row = conn.execute("SELECT content,sender,recipient,directive FROM main.messages WHERE id=?", + (source[4],)).fetchone() + if (row is None or row[0] != content or not isinstance(row[1], str) + or row[1].lower() != sender.lower() or not isinstance(row[2], str) + or row[2].lower() != recipient.lower() or row[3] not in (0, None)): + raise ProfileRebuildRequired("Profile append source fields do not match") + yield + if _l0_tracked_source(conn) != source: + raise ProfileRebuildRequired("Profile append source changed") + if previous is not None: + _write_profile_certificate(conn, source) + + def update_entity_profile_incremental( conn: sqlite3.Connection, sender: str, message: str, recipient: str = "", @@ -927,130 +1121,131 @@ def update_entity_profile_incremental( if not sender or not message: return - # Normalize entity name to lowercase for case-insensitive matching (#467) - sender = sender.lower() - - # ── Read existing profile (if any) ──────────────────────────────── - existing = get_entity_profile(conn, sender) + with _profile_append_guard(conn, sender, message, recipient): + # Normalize entity name to lowercase for case-insensitive matching (#467) + sender = sender.lower() - if existing: - msg_count = existing["message_count"] + 1 - old_topics = existing.get("topics") or [] - old_traits = existing.get("traits") or [] - old_style = existing.get("communication_style") or {} - old_rels = existing.get("relationships") or {} - else: - msg_count = 1 - old_topics = [] - old_traits = [] - old_style = {} - old_rels = {} - - # ── Communication style (rolling average) ───────────────────────── - old_avg = old_style.get("avg_length", 0.0) - old_count = msg_count - 1 - new_avg = ((old_avg * old_count) + len(message)) / msg_count - - uses_emoji = old_style.get("uses_emoji", False) or _detect_emoji(message) - - msg_dict = {"content": message} - if msg_count == 1: - formality = _assess_formality([msg_dict]) - else: - formality = old_style.get("formality", "mixed") + # ── Read existing profile (if any) ──────────────────────────────── + existing = get_entity_profile(conn, sender) - comm_style = { - "avg_length": round(new_avg, 1), - "uses_emoji": uses_emoji, - "formality": formality, - "typical_greeting": old_style.get("typical_greeting", ""), - } - - # ── Topics (single-message extraction, threshold=1) ───────────── - # _extract_topics requires >= 2 hits per cluster which is too strict - # for a single message. Inline a threshold-1 scan instead. - lower = message.lower() - words = set(lower.split()) - _topic_clusters = { - "startup/business": {"startup", "company", "founded", "revenue", - "funding", "investor", "pitch", "valuation", - "mrr", "arr", "series", "seed", "raise"}, - "technology": {"code", "deploy", "database", "api", "backend", - "frontend", "server", "aws", "cloud", "repo", - "kubernetes", "docker", "github", "python", "go", - "rewrite", "migrate", "typescript", "javascript"}, - "health/fitness": {"gym", "run", "running", "workout", "health", - "marathon", "sleep", "meditation", "therapy", - "doctor", "weight"}, - "relationships": {"dating", "anniversary", "valentine", "proposal", - "ring", "wedding", "girlfriend", "boyfriend", - "partner"}, - "food/dining": {"restaurant", "coffee", "tacos", "sushi", "dinner", - "lunch", "breakfast", "eat", "cook", "bar"}, - "finance": {"salary", "savings", "money", "bank", "mortgage", - "house", "rent", "401k", "investment", "budget"}, - "career": {"job", "quit", "hired", "hiring", "employee", - "cofounder", "cto", "ceo", "role", "interview"}, - "pets": {"dog", "puppy", "vet", "pet", "shelter"}, - "entertainment": {"show", "movie", "netflix", "watch", "concert", - "festival", "book", "game"}, - "family": {"mom", "dad", "sister", "brother", "parents", "family", - "birthday"}, - } - new_topics = [t for t, kws in _topic_clusters.items() if words & kws] - topics = list(dict.fromkeys(old_topics + new_topics)) + if existing: + msg_count = existing["message_count"] + 1 + old_topics = existing.get("topics") or [] + old_traits = existing.get("traits") or [] + old_style = existing.get("communication_style") or {} + old_rels = existing.get("relationships") or {} + else: + msg_count = 1 + old_topics = [] + old_traits = [] + old_style = {} + old_rels = {} + + # ── Communication style (rolling average) ───────────────────────── + old_avg = old_style.get("avg_length", 0.0) + old_count = msg_count - 1 + new_avg = ((old_avg * old_count) + len(message)) / msg_count + + uses_emoji = old_style.get("uses_emoji", False) or _detect_emoji(message) + + msg_dict = {"content": message} + if msg_count == 1: + formality = _assess_formality([msg_dict]) + else: + formality = old_style.get("formality", "mixed") - # ── Traits (single-message extraction, threshold=1) ─────────────── - _trait_indicators = { - "ambitious": {"goal", "growth", "scale", "expand", "raise", - "million", "revenue", "build", "launch", "grind"}, - "anxious": {"worried", "anxious", "stress", "panic", "scared", - "nervous", "overwhelmed"}, - "caring": {"love", "miss", "proud", "care", "support"}, - "analytical": {"data", "metrics", "numbers", "analysis", - "measure", "accuracy", "benchmark"}, - "health-conscious": {"gym", "workout", "run", "meditation", - "sleep", "diet", "healthy", "fitness"}, - "technical": {"code", "deploy", "api", "database", "server", - "algorithm", "architecture", "framework", "debug"}, - "entrepreneurial": {"startup", "founder", "company", "pitch", - "investor", "equity", "valuation"}, - } - new_traits = [ - tr for tr, inds in _trait_indicators.items() - if any(ind in lower for ind in inds) - ] - traits = list(dict.fromkeys(old_traits + new_traits)) + comm_style = { + "avg_length": round(new_avg, 1), + "uses_emoji": uses_emoji, + "formality": formality, + "typical_greeting": old_style.get("typical_greeting", ""), + } - # ── Relationships ───────────────────────────────────────────────── - if recipient: - recipient_key = recipient.lower() - rel = old_rels.get(recipient_key, {"message_count": 0, "primary_topic": "general"}) - rel["message_count"] = rel.get("message_count", 0) + 1 + # ── Topics (single-message extraction, threshold=1) ───────────── + # _extract_topics requires >= 2 hits per cluster which is too strict + # for a single message. Inline a threshold-1 scan instead. lower = message.lower() - if any(w in lower for w in ("code", "deploy", "api", "database", "bug", "server")): - rel["primary_topic"] = "technical" - elif any(w in lower for w in ("worried", "anxious", "scared", "stress")): - rel["primary_topic"] = "emotional" - elif any(w in lower for w in ("dinner", "drinks", "hang out", "watch")): - rel["primary_topic"] = "social" - elif any(w in lower for w in ("revenue", "investor", "pricing", "customer")): - rel["primary_topic"] = "business" - old_rels[recipient_key] = rel - - # ── Store ───────────────────────────────────────────────────────── - now = datetime.now(timezone.utc).isoformat() - conn.execute( - """INSERT OR REPLACE INTO entity_profiles + words = set(lower.split()) + _topic_clusters = { + "startup/business": {"startup", "company", "founded", "revenue", + "funding", "investor", "pitch", "valuation", + "mrr", "arr", "series", "seed", "raise"}, + "technology": {"code", "deploy", "database", "api", "backend", + "frontend", "server", "aws", "cloud", "repo", + "kubernetes", "docker", "github", "python", "go", + "rewrite", "migrate", "typescript", "javascript"}, + "health/fitness": {"gym", "run", "running", "workout", "health", + "marathon", "sleep", "meditation", "therapy", + "doctor", "weight"}, + "relationships": {"dating", "anniversary", "valentine", "proposal", + "ring", "wedding", "girlfriend", "boyfriend", + "partner"}, + "food/dining": {"restaurant", "coffee", "tacos", "sushi", "dinner", + "lunch", "breakfast", "eat", "cook", "bar"}, + "finance": {"salary", "savings", "money", "bank", "mortgage", + "house", "rent", "401k", "investment", "budget"}, + "career": {"job", "quit", "hired", "hiring", "employee", + "cofounder", "cto", "ceo", "role", "interview"}, + "pets": {"dog", "puppy", "vet", "pet", "shelter"}, + "entertainment": {"show", "movie", "netflix", "watch", "concert", + "festival", "book", "game"}, + "family": {"mom", "dad", "sister", "brother", "parents", "family", + "birthday"}, + } + new_topics = [t for t, kws in _topic_clusters.items() if words & kws] + topics = list(dict.fromkeys(old_topics + new_topics)) + + # ── Traits (single-message extraction, threshold=1) ─────────────── + _trait_indicators = { + "ambitious": {"goal", "growth", "scale", "expand", "raise", + "million", "revenue", "build", "launch", "grind"}, + "anxious": {"worried", "anxious", "stress", "panic", "scared", + "nervous", "overwhelmed"}, + "caring": {"love", "miss", "proud", "care", "support"}, + "analytical": {"data", "metrics", "numbers", "analysis", + "measure", "accuracy", "benchmark"}, + "health-conscious": {"gym", "workout", "run", "meditation", + "sleep", "diet", "healthy", "fitness"}, + "technical": {"code", "deploy", "api", "database", "server", + "algorithm", "architecture", "framework", "debug"}, + "entrepreneurial": {"startup", "founder", "company", "pitch", + "investor", "equity", "valuation"}, + } + new_traits = [ + tr for tr, inds in _trait_indicators.items() + if any(ind in lower for ind in inds) + ] + traits = list(dict.fromkeys(old_traits + new_traits)) + + # ── Relationships ───────────────────────────────────────────────── + if recipient: + recipient_key = recipient.lower() + rel = old_rels.get(recipient_key, {"message_count": 0, "primary_topic": "general"}) + rel["message_count"] = rel.get("message_count", 0) + 1 + lower = message.lower() + if any(w in lower for w in ("code", "deploy", "api", "database", "bug", "server")): + rel["primary_topic"] = "technical" + elif any(w in lower for w in ("worried", "anxious", "scared", "stress")): + rel["primary_topic"] = "emotional" + elif any(w in lower for w in ("dinner", "drinks", "hang out", "watch")): + rel["primary_topic"] = "social" + elif any(w in lower for w in ("revenue", "investor", "pricing", "customer")): + rel["primary_topic"] = "business" + old_rels[recipient_key] = rel + + # ── Store ───────────────────────────────────────────────────────── + now = datetime.now(timezone.utc).isoformat() + conn.execute( + """INSERT OR REPLACE INTO entity_profiles (entity, message_count, traits, communication_style, topics, relationships, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)""", - ( - sender, msg_count, - json.dumps(traits), json.dumps(comm_style), - json.dumps(topics), json.dumps(old_rels), now, - ), - ) + ( + sender, msg_count, + json.dumps(traits), json.dumps(comm_style), + json.dumps(topics), json.dumps(old_rels), now, + ), + ) def get_entity_profile(conn: sqlite3.Connection, @@ -1068,12 +1263,24 @@ def get_entity_profile(conn: sqlite3.Connection, ``updated_at``. Returns ``None`` if no profile exists for the entity. """ - row = conn.execute( - "SELECT entity, message_count, traits, communication_style, " - " topics, relationships, updated_at " - "FROM entity_profiles WHERE LOWER(entity) = LOWER(?)", - (entity,), - ).fetchone() + owned = not conn.in_transaction + try: + if owned: + conn.execute("BEGIN") + try: + _profile_read_state(conn) + row = conn.execute( + "SELECT entity, message_count, traits, communication_style, " + " topics, relationships, updated_at " + "FROM entity_profiles WHERE LOWER(entity) = LOWER(?)", + (entity,), + ).fetchone() + + finally: + if owned and conn.in_transaction: + conn.rollback() + except sqlite3.OperationalError: + return None if not row: return None @@ -1239,42 +1446,107 @@ def resolve_entity(conn, name_query, context=""): return name_query # Return original if no match found -def build_dunbar_hierarchy(conn, primary_entity=None): - """ - Classify contacts by interaction frequency/recency into Dunbar layers: - - intimate (5): closest, most frequent - - close (15): regular interaction - - friend (50): moderate interaction - - acquaintance (150+): occasional - - Stores in entity_relationships table. - """ - if primary_entity is None: - return {} - - primary_entity = primary_entity.lower() - - # Count messages per contact - rows = conn.execute( - """ - SELECT LOWER(name) as name, COUNT(*) as cnt, MAX(ts) as last_ts FROM ( - SELECT recipient as name, timestamp as ts FROM messages WHERE LOWER(sender) = ? AND recipient != '' - UNION ALL - SELECT sender as name, timestamp as ts FROM messages WHERE LOWER(recipient) = ? AND sender != '' - ) GROUP BY LOWER(name) ORDER BY cnt DESC - """, - (primary_entity, primary_entity) - ).fetchall() +_DUNBAR_RELATIONSHIP_COLUMNS = ( + "id,entity_a,entity_b,relationship_type,strength,dunbar_layer,last_interaction" +) - if not rows: - return {} - # Clear existing relationships for this entity - conn.execute( - "DELETE FROM entity_relationships WHERE entity_a = ?", - (primary_entity,) +@contextlib.contextmanager +def _dunbar_transaction(conn: sqlite3.Connection, *, write: bool = False) -> Iterator[None]: + owned = not conn.in_transaction + conn.execute(("BEGIN IMMEDIATE" if write else "BEGIN") if owned else "SAVEPOINT truememory_dunbar") + completed = False + try: + if write and not owned: + conn.execute("UPDATE entity_relationships SET id=id WHERE 0") + yield + conn.execute("COMMIT" if owned else "RELEASE truememory_dunbar") + completed = True + finally: + if not completed and conn.in_transaction: + if owned: + conn.rollback() + else: + conn.execute("ROLLBACK TO truememory_dunbar") + conn.execute("RELEASE truememory_dunbar") + + +def _dunbar_fingerprint(rows: Iterable[tuple]) -> bytes: + digest = hashlib.sha256() + for row in rows: + digest.update(struct.pack(">Q", len(row))) + for value in row: + if value is None: + tag, data = b"n", b"" + elif isinstance(value, bytes): + tag, data = b"b", value + elif isinstance(value, str): + tag, data = b"s", value.encode("utf-8") + elif isinstance(value, int): + tag, data = b"i", str(value).encode("ascii") + elif isinstance(value, float): + tag, data = b"f", struct.pack(">d", value) + else: + raise TypeError("Unsupported SQLite value in Dunbar source") + digest.update(tag + struct.pack(">Q", len(data))) + digest.update(data) + return digest.digest() + + +def _dunbar_has_ledger(conn: sqlite3.Connection) -> bool: + return conn.execute( + "SELECT 1 FROM sqlite_master WHERE type='table' AND name='dunbar_generated_relationships'" + ).fetchone() is not None + + +def _dunbar_snapshot(conn: sqlite3.Connection) -> tuple[bytes, ...]: + schema = conn.execute( + "SELECT type,name,rootpage,sql FROM sqlite_master WHERE tbl_name IN " + "('messages','entity_relationships','dunbar_generated_relationships') ORDER BY type,name" ) + schema_hash = _dunbar_fingerprint(schema) + source_hash = _dunbar_fingerprint(conn.execute( + "SELECT id,sender,recipient,timestamp FROM messages ORDER BY id" + )) + relationship_hash = _dunbar_fingerprint(conn.execute( + f"SELECT {_DUNBAR_RELATIONSHIP_COLUMNS} FROM entity_relationships ORDER BY id" + )) + ledger_hash = _dunbar_fingerprint(conn.execute( + "SELECT relationship_id,generation,row_fingerprint FROM dunbar_generated_relationships " + "ORDER BY relationship_id" + )) if _dunbar_has_ledger(conn) else b"" + return schema_hash, source_hash, relationship_hash, ledger_hash + + +def _dunbar_owned_rows(conn: sqlite3.Connection) -> Iterator[tuple[int, bool]]: + tracked = _dunbar_ownership_ready(conn) + columns = ",".join("r." + name for name in _DUNBAR_RELATIONSHIP_COLUMNS.split(",")) + for row in conn.execute( + f"SELECT g.relationship_id,g.row_fingerprint,{columns} " + "FROM dunbar_generated_relationships g LEFT JOIN entity_relationships r " + "ON r.id=g.relationship_id ORDER BY g.relationship_id" + ): + yield row[0], tracked and row[2] is not None and _dunbar_fingerprint((row[2:],)) == row[1] + + +def read_dunbar_coverage(conn: sqlite3.Connection) -> dict[str, int | str]: + """Describe row ownership in one snapshot, without claiming source freshness.""" + with _dunbar_transaction(conn): + tracked = _dunbar_ownership_ready(conn) + owned = dict(_dunbar_owned_rows(conn)) if _dunbar_has_ledger(conn) else {} + managed = sum(owned.values()) + unowned_contacts = sum( + not owned.get(row[0], False) for row in conn.execute( + "SELECT id FROM entity_relationships WHERE relationship_type='contact'" + ) + ) + invalid = len(owned) - managed + status = "partial" if unowned_contacts or invalid else ("managed_only" if tracked else "untracked") + return {"status": status, "managed_rows": managed, + "unowned_contacts": unowned_contacts, "invalid_ownership": invalid} + +def _compute_dunbar_hierarchy(rows: list[tuple]) -> dict[str, dict]: max_count = rows[0][1] if rows else 1 hierarchy = {} @@ -1299,12 +1571,47 @@ def build_dunbar_hierarchy(conn, primary_entity=None): "strength": round(freq_score, 3), } - conn.execute( - "INSERT INTO entity_relationships " - "(entity_a, entity_b, relationship_type, strength, dunbar_layer, last_interaction) " - "VALUES (?, ?, ?, ?, ?, ?)", - (primary_entity, name, "contact", round(freq_score, 3), layer, last_ts or "") - ) + return hierarchy + - conn.commit() +def build_dunbar_hierarchy(conn: sqlite3.Connection, primary_entity: str | None = None) -> dict[str, dict]: + """Publish one Dunbar generation, preserving unowned and changed relationships. + + Empty input or no primary retires only verified generated rows. A caller's + transaction retains its final commit, including an additive ledger migration. + """ + primary = primary_entity.lower() if primary_entity is not None else None + with _dunbar_transaction(conn): + snapshot = _dunbar_snapshot(conn) + rows = conn.execute(""" + SELECT LOWER(name) as name, COUNT(*) as cnt, MAX(ts) as last_ts FROM ( + SELECT recipient as name, timestamp as ts FROM messages WHERE LOWER(sender) = ? AND recipient != '' + UNION ALL + SELECT sender as name, timestamp as ts FROM messages WHERE LOWER(recipient) = ? AND sender != '' + ) GROUP BY LOWER(name) ORDER BY cnt DESC + """, (primary, primary)).fetchall() if primary is not None else [] + hierarchy = _compute_dunbar_hierarchy(rows) + + with _dunbar_transaction(conn, write=True): + if _dunbar_snapshot(conn) != snapshot: + raise sqlite3.OperationalError("Dunbar source or ownership changed during computation") + _initialize_dunbar_ownership(conn) + # Materialize IDs before deleting from either side of the ledger join. + verified_ids = [row_id for row_id, verified in _dunbar_owned_rows(conn) if verified] + conn.executemany("DELETE FROM entity_relationships WHERE id=?", ((row_id,) for row_id in verified_ids)) + conn.execute("DELETE FROM dunbar_generated_relationships") + generation = uuid.uuid4().hex + for name, data in hierarchy.items(): + cursor = conn.execute( + "INSERT INTO entity_relationships " + "(entity_a,entity_b,relationship_type,strength,dunbar_layer,last_interaction) VALUES (?,?,?,?,?,?)", + (primary, name, "contact", data["strength"], data["dunbar_layer"], data["last_interaction"]), + ) + persisted = conn.execute( + f"SELECT {_DUNBAR_RELATIONSHIP_COLUMNS} FROM entity_relationships WHERE id=?", (cursor.lastrowid,), + ).fetchone() + conn.execute( + "INSERT INTO dunbar_generated_relationships(relationship_id,generation,row_fingerprint) VALUES (?,?,?)", + (cursor.lastrowid, generation, _dunbar_fingerprint((persisted,))), + ) return hierarchy diff --git a/truememory/personality_style_vec.py b/truememory/personality_style_vec.py index 6caeb779..f840ec09 100644 --- a/truememory/personality_style_vec.py +++ b/truememory/personality_style_vec.py @@ -26,6 +26,9 @@ import re import sqlite3 import struct +import sys +from collections.abc import Callable, Iterator +from contextlib import contextmanager from datetime import datetime, timezone @@ -35,6 +38,20 @@ def _stable_hash(s: str) -> int: DIM = 256 NGRAM_SIZES = (3, 4, 5) +_STYLE_ACCUMULATOR_VERSION = 1 +_STYLE_SUM = struct.Struct("<256d") + + +class StyleVectorRebuildRequired(RuntimeError): + """The stored profile remains readable but cannot accept exact appends.""" + + def __init__(self, reason: str) -> None: + self.reason = reason + super().__init__(f"Style accumulator rebuild required: {reason}") + + +class _StyleSourceChanged(sqlite3.OperationalError): + """A verified source mismatch, distinct from unexpected SQLite failures.""" def compute_style_vector(text: str) -> list[float]: @@ -111,54 +128,251 @@ def cosine_similarity(a: list[float], b: list[float]) -> float: return num / (na * nb) -def build_entity_style_vectors(conn: sqlite3.Connection) -> dict[str, list[float]]: - """Batch-build style vectors for every entity (sender) in the database. +@contextmanager +def _l0_transaction(conn: sqlite3.Connection, *, write: bool = False) -> Iterator[None]: + """Own only this phase, retaining any transaction opened by the caller.""" + owned = not conn.in_transaction + if owned: + conn.execute("BEGIN IMMEDIATE" if write else "BEGIN") + else: + conn.execute("SAVEPOINT truememory_l0") + completed = False + try: + if write and not owned: + # Upgrade before validation: a stale WAL reader must not publish. + conn.execute("UPDATE messages SET sender = sender WHERE 0") + yield + conn.execute("COMMIT" if owned else "RELEASE SAVEPOINT truememory_l0") + completed = True + finally: + if not completed and conn.in_transaction: + if owned: + conn.rollback() + else: + # If rollback fails, never RELEASE potentially partial writes. + conn.execute("ROLLBACK TO SAVEPOINT truememory_l0") + conn.execute("RELEASE SAVEPOINT truememory_l0") + + +def _l0_source_schema(conn: sqlite3.Connection) -> tuple[tuple, ...]: + return tuple(tuple(row) for row in conn.execute("PRAGMA table_info(messages)")) + + +def _l0_directive_filter(schema: tuple[tuple, ...]) -> str: + # Public builders also accept older standalone schemas without directives. + if any(column[1] == "directive" for column in schema): + return "(directive = 0 OR directive IS NULL)" + return "1" + + +def _l0_validate_source( + conn: sqlite3.Connection, + schema: tuple[tuple, ...], + query: str, + source_rows: list[tuple], + check_cancel: Callable[[], None] | None = None, +) -> None: + """Compare exact relevant source values while holding writer ownership.""" + error = "L0 source changed during computation; retry the rebuild" + if _l0_source_schema(conn) != schema: + raise _StyleSourceChanged(error) + current = conn.execute(query) + try: + for expected in source_rows: + if check_cancel is not None: + check_cancel() + row = current.fetchone() + if row is None or tuple(row) != expected: + raise _StyleSourceChanged(error) + if current.fetchone() is not None: + raise _StyleSourceChanged(error) + finally: + current.close() + + +def _compute_entity_style_vectors( + rows: list[tuple], + *, check_cancel: Callable[[], None] | None = None, +) -> tuple[dict[str, list[float]], list[tuple]]: + from collections import defaultdict - For each sender: - 1. Compute ``compute_style_vector(msg.content)`` for each message. - 2. Mean-pool all per-message vectors via ``mean_pool_vectors``. - 3. Store the result in the ``entity_style_vectors`` table. + by_sender: dict[str, list[str]] = defaultdict(list) + for sender, content, _timestamp, ordinary in rows: + if check_cancel is not None: + check_cancel() + if ordinary: + by_sender[sender.lower()].append(content) - Args: - conn: Open database connection (from :func:`truememory.storage.create_db`). + result: dict[str, list[float]] = {} + stored_rows: list[tuple] = [] + now = datetime.now(timezone.utc).isoformat() + for sender, contents in by_sender.items(): + total = [0.0] * DIM + for content in contents: + if check_cancel is not None: + check_cancel() + vec = _validated_style_vector(compute_style_vector(content)) + for index in range(DIM): + total[index] += vec[index] + mean_vec = _style_profile_from_sum(total, len(contents)) + result[sender] = mean_vec + stored_rows.append((sender, json.dumps(mean_vec), len(contents), now, + _STYLE_SUM.pack(*total), _STYLE_ACCUMULATOR_VERSION)) + return result, stored_rows - Returns: - ``{entity: vector}`` for every sender. - """ + +def _validated_style_vector(vector: list[float]) -> list[float]: + if not isinstance(vector, (list, tuple)) or len(vector) != DIM: + raise ValueError("Style vector must have 256 finite components") + if any(not isinstance(value, (int, float)) or isinstance(value, bool) for value in vector): + raise ValueError("Style vector must have 256 finite components") + try: + values = [float(value) for value in vector] + except (OverflowError, ValueError) as error: + raise ValueError("Style vector must have 256 finite components") from error + if not all(math.isfinite(value) for value in values): + raise ValueError("Style vector must have 256 finite components") + return values + + +def _style_profile_from_sum(total: list[float], count: int) -> list[float]: + # Retain mean_pool_vectors' divide-then-normalize arithmetic exactly. + mean = [value / count for value in total] + norm = math.sqrt(sum(value * value for value in mean)) + if not math.isfinite(norm): + raise ValueError("Style profile magnitude is not finite") + return [value / norm for value in mean] if norm > 0 else mean + + +def _ensure_style_accumulator_schema(conn: sqlite3.Connection) -> None: + """Add format columns inside the caller's publication transaction.""" conn.execute( """CREATE TABLE IF NOT EXISTS entity_style_vectors ( entity TEXT PRIMARY KEY, vector TEXT, message_count INTEGER DEFAULT 0, - updated_at TEXT + updated_at TEXT, + vector_sum BLOB DEFAULT NULL, + accumulator_version INTEGER NOT NULL DEFAULT 0 )""" ) + columns = {row[1] for row in conn.execute("PRAGMA table_info(entity_style_vectors)")} + if "vector_sum" not in columns: + conn.execute("ALTER TABLE entity_style_vectors ADD COLUMN vector_sum BLOB DEFAULT NULL") + if "accumulator_version" not in columns: + conn.execute("ALTER TABLE entity_style_vectors ADD COLUMN accumulator_version INTEGER NOT NULL DEFAULT 0") + + +def _decode_style_accumulator(row: tuple) -> tuple[list[float], int]: + raw, count, version = row + if type(version) is not int or version < 0: + raise StyleVectorRebuildRequired("invalid") + if version == 0: + raise StyleVectorRebuildRequired("legacy") + if version != _STYLE_ACCUMULATOR_VERSION: + raise StyleVectorRebuildRequired("unsupported_version") + if not isinstance(raw, bytes) or len(raw) != _STYLE_SUM.size or type(count) is not int or count < 1: + raise StyleVectorRebuildRequired("invalid") + total = list(_STYLE_SUM.unpack(raw)) + if not all(math.isfinite(value) for value in total): + raise StyleVectorRebuildRequired("invalid") + try: + _style_profile_from_sum(total, count) + except ValueError as error: + raise StyleVectorRebuildRequired("invalid") from error + return total, count + + +@contextmanager +def _style_append_transaction(conn: sqlite3.Connection) -> Iterator[None]: + """Serialize read-modify-write without committing an append for its caller.""" + owned = not conn.in_transaction + if owned: + conn.execute("BEGIN IMMEDIATE") + savepoint = False + completed = False + try: + conn.execute("SAVEPOINT truememory_style_append") + savepoint = True + yield + conn.execute("RELEASE SAVEPOINT truememory_style_append") + completed = True + finally: + if not completed and conn.in_transaction: + if owned: + conn.rollback() + elif savepoint: + conn.execute("ROLLBACK TO SAVEPOINT truememory_style_append") + conn.execute("RELEASE SAVEPOINT truememory_style_append") + + +def _capture_style_source( + conn: sqlite3.Connection, check_cancel: Callable[[], None] | None = None, +) -> tuple[tuple[tuple, ...], str, list[tuple]]: + schema = _l0_source_schema(conn) + query = ( + f"SELECT sender, content, timestamp, {_l0_directive_filter(schema)} AS ordinary " + "FROM messages WHERE sender != '' ORDER BY sender, timestamp" + ) + rows = [] + cursor = conn.execute(query) + try: + for row in cursor: + if check_cancel is not None: + check_cancel() + rows.append(tuple(row)) + finally: + cursor.close() + return schema, query, rows + + +def _publish_style_vectors( + conn: sqlite3.Connection, schema: tuple[tuple, ...], query: str, + rows: list[tuple], stored_rows: list[tuple], *, + check_cancel: Callable[[], None] | None = None, + before_replace: Callable[[sqlite3.Connection], None] | None = None, +) -> None: + """Publish only inside the transaction owned by the selected entry point.""" + _l0_validate_source(conn, schema, query, rows, check_cancel) + _ensure_style_accumulator_schema(conn) + if before_replace is not None: + before_replace(conn) + if check_cancel is not None: + check_cancel() + conn.execute("DELETE FROM entity_style_vectors") + conn.executemany( + """INSERT INTO entity_style_vectors + (entity, vector, message_count, updated_at, vector_sum, accumulator_version) + VALUES (?, ?, ?, ?, ?, ?)""", + stored_rows, + ) - rows = conn.execute( - "SELECT sender, content FROM messages WHERE sender != '' ORDER BY sender, timestamp" - ).fetchall() - from collections import defaultdict - by_sender: dict[str, list[str]] = defaultdict(list) - for sender, content in rows: - by_sender[sender.lower()].append(content) +def build_entity_style_vectors(conn: sqlite3.Connection) -> dict[str, list[float]]: + """Batch-build style vectors for every entity (sender) in the database. - result: dict[str, list[float]] = {} - now = datetime.now(timezone.utc).isoformat() + For each sender: + 1. Compute ``compute_style_vector(msg.content)`` for each message. + 2. Mean-pool with the same arithmetic as ``mean_pool_vectors``. + 3. Store the profile and its raw sum in ``entity_style_vectors``. - for sender, contents in by_sender.items(): - vecs = [compute_style_vector(c) for c in contents] - mean_vec = mean_pool_vectors(vecs) - result[sender] = mean_vec + Args: + conn: Open database connection (from :func:`truememory.storage.create_db`). - conn.execute( - """INSERT OR REPLACE INTO entity_style_vectors - (entity, vector, message_count, updated_at) - VALUES (?, ?, ?, ?)""", - (sender, json.dumps(mean_vec), len(contents), now), - ) + Returns: + ``{entity: vector}`` for every sender. + + Only ordinary messages contribute. Computation starts no write transaction; + any transaction already held by the caller remains theirs. Publication + atomically replaces the complete table after checking the source snapshot. + A changed source raises ``sqlite3.OperationalError`` without retrying. + """ + with _l0_transaction(conn): + schema, query, rows = _capture_style_source(conn) + result, stored_rows = _compute_entity_style_vectors(rows) - conn.commit() + with _l0_transaction(conn, write=True): + _publish_style_vectors(conn, schema, query, rows, stored_rows) return result @@ -168,12 +382,11 @@ def update_entity_style_vector_incremental( ) -> None: """Incrementally update an entity's style vector with a new message. - Uses a running weighted average: given the existing mean vector and - its message count, the new mean is:: - - new_mean = (existing * count + new_vec) / (count + 1) - - Then re-L2-normalized. + Persist the sum of per-message vectors and normalize only the exposed + profile. A legacy, unsupported or invalid accumulator raises + ``StyleVectorRebuildRequired`` without changing its previous profile. + Rebuild explicitly with ``build_entity_style_vectors`` before appending. + This function never commits; successful writes remain the caller's. Args: conn: Open database connection. @@ -187,50 +400,109 @@ def update_entity_style_vector_incremental( # Normalize entity name to lowercase for case-insensitive matching (#467) entity = entity.lower() - conn.execute( - """CREATE TABLE IF NOT EXISTS entity_style_vectors ( - entity TEXT PRIMARY KEY, - vector TEXT, - message_count INTEGER DEFAULT 0, - updated_at TEXT - )""" + new_vec = _validated_style_vector( + _pre_computed_vec if _pre_computed_vec is not None else compute_style_vector(new_message) ) - - new_vec = _pre_computed_vec if _pre_computed_vec is not None else compute_style_vector(new_message) now = datetime.now(timezone.utc).isoformat() - - row = conn.execute( - "SELECT vector, message_count FROM entity_style_vectors WHERE entity = ?", - (entity,), - ).fetchone() - - if row is None or row[0] is None: + with _style_append_transaction(conn): + _ensure_style_accumulator_schema(conn) + # Existing callers may hold only a read snapshot. Upgrade before + # reading the accumulator so another connection cannot lose an append. + conn.execute("UPDATE entity_style_vectors SET entity=entity WHERE 0") + row = conn.execute( + "SELECT vector_sum, message_count, accumulator_version FROM entity_style_vectors WHERE entity = ?", + (entity,), + ).fetchone() + if row is None: + total, count = new_vec, 1 + else: + previous, count = _decode_style_accumulator(row) + total = [previous[index] + new_vec[index] for index in range(DIM)] + count += 1 + profile = _style_profile_from_sum(total, count) conn.execute( """INSERT OR REPLACE INTO entity_style_vectors - (entity, vector, message_count, updated_at) - VALUES (?, ?, ?, ?)""", - (entity, json.dumps(new_vec), 1, now), + (entity, vector, message_count, updated_at, vector_sum, accumulator_version) + VALUES (?, ?, ?, ?, ?, ?)""", + (entity, json.dumps(profile), count, now, _STYLE_SUM.pack(*total), _STYLE_ACCUMULATOR_VERSION), ) - else: - existing_vec = json.loads(row[0]) - count = row[1] or 0 - new_count = count + 1 - merged = [ - (existing_vec[i] * count + new_vec[i]) / new_count - for i in range(len(existing_vec)) - ] - norm = math.sqrt(sum(x * x for x in merged)) - if norm > 0: - merged = [x / norm for x in merged] - - conn.execute( - """INSERT OR REPLACE INTO entity_style_vectors - (entity, vector, message_count, updated_at) - VALUES (?, ?, ?, ?)""", - (entity, json.dumps(merged), new_count, now), - ) +_STYLE_READ_TRIGGER_SHA256 = { + "messages_maintenance_ai": "b75f5431c0ce50627341f9afa36b4ad8920a56a561e19f42b3c6fd5d6a08c778", + "messages_maintenance_au": "e9626141915f0588897aaf24c97e184e0aeb47f8c3f1eb712c346638a9287749", + "messages_maintenance_unique_au": "3d8c28d2a9351bfaeed175dc6d058abaa77b7f0b64ab6b0c9e0e6999660fea1b", + "messages_maintenance_ad": "5d0f978eddd7fbad96c49b0c95942ea1d69526ed102bd0afb8229eb195263c0e", + "truememory_style_output_insert": "db594eb1ec763aba7557d220389f73cd19de26ac3b7456947cbd252fab9e4254", + "truememory_style_output_update": "8063fd739011d53ba809f39f6abfb01b11ac6069ccb1de58d533395fee7bb6a0", + "truememory_style_output_delete": "3174a45efdf4c34800e5434db139030399bd261d73b8c1279b53cf13e7a84922", +} + + +def _style_cached_readable(conn: sqlite3.Connection) -> bool: + """Validate an enrolled generation without importing its maintenance worker.""" + enrolled = conn.execute( + "SELECT 1 FROM main.sqlite_master WHERE type='trigger' AND name IN (?,?,?) LIMIT 1", + tuple(_STYLE_READ_TRIGGER_SHA256)[4:], + ).fetchone() is not None + table = conn.execute( + "SELECT 1 FROM main.sqlite_master WHERE type='table' AND name='maintenance_layers'" + ).fetchone() + if table is None: + return not enrolled + checkpoint = conn.execute( + "SELECT builder_version,successful_epoch,successful_revision,successful_dependency," + "full_rebuild_required,successful_coverage,output_count FROM main.maintenance_layers " + "WHERE layer='style_vectors'" + ).fetchone() + if checkpoint is None: + return not enrolled + shadows = ("messages", "maintenance_source_state", "maintenance_layers", "metadata", "entity_style_vectors") + placeholders = ",".join("?" for _ in shadows) + if conn.execute("SELECT 1 FROM temp.sqlite_master WHERE type IN ('table','view') " + f"AND lower(name) IN ({placeholders}) LIMIT 1", shadows).fetchone(): + return False + source = conn.execute( + "SELECT epoch,revision,insert_count,correction_count,max_seen_message_id," + "nonappend_revision,tracking_ready FROM main.maintenance_source_state WHERE singleton=1" + ).fetchone() + if (source is None or source[6] != 1 or not isinstance(source[0], str) or not source[0] + or any(type(value) is not int or value < 0 for value in source[1:6]) + or source[1] != source[2] + source[3] or source[5] > source[1]): + return False + schema = list(conn.execute("PRAGMA main.table_xinfo(messages)")) + identifiers = [row for row in schema if row[5]] + definition = conn.execute("SELECT sql FROM main.sqlite_master WHERE type='table' AND name='messages'").fetchone() + if (len(identifiers) != 1 or identifiers[0][1:3] != ("id", "INTEGER") + or definition is None or "WITHOUT ROWID" in " ".join(definition[0].upper().split())): + return False + names = tuple(_STYLE_READ_TRIGGER_SHA256) + placeholders = ",".join("?" for _ in names) + installed = {name: hashlib.sha256(" ".join(sql.strip().rstrip(";").split()).encode()).hexdigest() + for name, sql in conn.execute("SELECT name,sql FROM main.sqlite_master WHERE type='trigger' " + f"AND name IN ({placeholders})", names)} + if installed != _STYLE_READ_TRIGGER_SHA256: + return False + if conn.execute("SELECT 1 FROM temp.sqlite_master WHERE type='trigger' AND " + "(lower(tbl_name) IN ('messages','entity_style_vectors','maintenance_layers','metadata') " + f"OR name IN ({placeholders})) LIMIT 1", names).fetchone(): + return False + output_names = names[4:] + if conn.execute("SELECT 1 FROM main.sqlite_master WHERE type='trigger' " + "AND lower(tbl_name) IN ('entity_style_vectors','maintenance_layers','metadata') " + "AND NOT (lower(tbl_name)='entity_style_vectors' AND name IN (?,?,?)) LIMIT 1", + output_names).fetchone(): + return False + dependency = json.dumps({"builder_version": 1, "parameters": { + "accumulator_version": 1, "hash_version": 2, "dimension": DIM, + "ngrams": list(NGRAM_SIZES), "sum_format": "<256d"}, + "available": True, "error_category": None, "python": list(sys.version_info[:3])}, + sort_keys=True, separators=(",", ":"), allow_nan=False) + return (checkpoint[0] == 1 and checkpoint[1] == source[0] + and type(checkpoint[2]) is int and source[5] <= checkpoint[2] <= source[1] + and checkpoint[3] == dependency and checkpoint[4] == 0 + and checkpoint[5] == "complete" and type(checkpoint[6]) is int and checkpoint[6] >= 0 + and conn.execute("SELECT value FROM main.metadata WHERE key='style_vec_hash_version'").fetchone() == ("2",)) def get_entity_style_vector( @@ -247,11 +519,20 @@ def get_entity_style_vector( """ # Normalize entity name to lowercase for case-insensitive matching (#467) entity = entity.lower() + owned = not conn.in_transaction try: - row = conn.execute( - "SELECT vector FROM entity_style_vectors WHERE entity = ?", - (entity,), - ).fetchone() + if owned: + conn.execute("BEGIN") + try: + if not _style_cached_readable(conn): + return None + row = conn.execute( + "SELECT vector FROM entity_style_vectors WHERE entity = ?", + (entity,), + ).fetchone() + finally: + if owned and conn.in_transaction: + conn.rollback() except sqlite3.OperationalError: return None diff --git a/truememory/predictive.py b/truememory/predictive.py index 7f6f3ba4..dc51ac86 100644 --- a/truememory/predictive.py +++ b/truememory/predictive.py @@ -131,7 +131,6 @@ def _ensure_surprise_table(conn: sqlite3.Connection) -> None: FOREIGN KEY (message_id) REFERENCES messages(id) ) """) - conn.commit() # --------------------------------------------------------------------------- @@ -307,7 +306,67 @@ def compute_surprise_score(content: str, existing_facts: set) -> float: return max(0.05, min(1.0, score)) -def build_surprise_index(conn: sqlite3.Connection) -> dict: +def _compute_surprise_rows( + rows: list[tuple], +) -> tuple[dict[int, float], list[tuple[int, float, int, int]]]: + existing_facts: set[str] = set() + scores: dict[int, float] = {} + stored_rows: list[tuple[int, float, int, int]] = [] + for msg_id, content, _timestamp in rows: + message_facts = extract_facts(content) + new_facts = message_facts - existing_facts + surprise = compute_surprise_score(content, existing_facts) + stored_rows.append( + (msg_id, round(surprise, 4), len(message_facts), len(new_facts)) + ) + scores[msg_id] = surprise + existing_facts.update(message_facts) + return scores, stored_rows + + +def _publish_surprise_rows( + conn: sqlite3.Connection, + source_rows: list[tuple], + stored_rows: list[tuple[int, float, int, int]], +) -> None: + conn.execute("SAVEPOINT truememory_surprise_publish") + try: + _ensure_surprise_table(conn) + # Claim SQLite writer ownership without changing rows. A stale caller + # WAL snapshot fails here; a fresh connection validates its source + # below while no other writer can change it before publication. + conn.execute("DELETE FROM surprise_scores WHERE 0") + current = conn.execute( + "SELECT id, content, timestamp FROM messages ORDER BY timestamp, id" + ) + try: + for expected in source_rows: + if current.fetchone() != expected: + raise sqlite3.OperationalError( + "Surprise index source changed during computation; retry the rebuild" + ) + if current.fetchone() is not None: + raise sqlite3.OperationalError( + "Surprise index source changed during computation; retry the rebuild" + ) + finally: + current.close() + conn.execute("DELETE FROM surprise_scores") + conn.executemany( + "INSERT INTO surprise_scores " + "(message_id, surprise, fact_count, new_fact_count) VALUES (?, ?, ?, ?)", + stored_rows, + ) + conn.execute("RELEASE SAVEPOINT truememory_surprise_publish") + except BaseException: + # Include cancellation. Never release the savepoint if rollback fails, + # because releasing an outermost savepoint could commit partial rows. + conn.execute("ROLLBACK TO SAVEPOINT truememory_surprise_publish") + conn.execute("RELEASE SAVEPOINT truememory_surprise_publish") + raise + + +def build_surprise_index(conn: sqlite3.Connection) -> dict[int, float]: """ Score all messages by surprise value and store the results. @@ -327,42 +386,18 @@ def build_surprise_index(conn: sqlite3.Connection) -> dict: Returns: ``{message_id: surprise_score}`` for every message in the database. - """ - _ensure_surprise_table(conn) - # Clear existing scores for a clean rebuild - conn.execute("DELETE FROM surprise_scores") - - # Fetch all messages in chronological order + Computation precedes this function's write transaction. Publication checks + the exact source snapshot under writer ownership and atomically replaces + the index. A changed source raises ``sqlite3.OperationalError`` for the + caller to retry. Existing caller transactions are never committed or + restarted; any write lock they already hold remains their responsibility. + """ rows = conn.execute( "SELECT id, content, timestamp FROM messages ORDER BY timestamp, id" ).fetchall() - - existing_facts: set[str] = set() - scores: dict[int, float] = {} - - for msg_id, content, timestamp in rows: - # Extract facts from this message - message_facts = extract_facts(content) - new_facts = message_facts - existing_facts - - # Compute surprise - surprise = compute_surprise_score(content, existing_facts) - - # Store the score - conn.execute( - "INSERT INTO surprise_scores " - "(message_id, surprise, fact_count, new_fact_count) " - "VALUES (?, ?, ?, ?)", - (msg_id, round(surprise, 4), len(message_facts), len(new_facts)), - ) - - scores[msg_id] = surprise - - # Add this message's facts to the accumulated set - existing_facts.update(message_facts) - - conn.commit() + scores, stored_rows = _compute_surprise_rows(rows) + _publish_surprise_rows(conn, rows, stored_rows) return scores diff --git a/truememory/rebuild_source.py b/truememory/rebuild_source.py new file mode 100644 index 00000000..9bb8becc --- /dev/null +++ b/truememory/rebuild_source.py @@ -0,0 +1,390 @@ +"""Bounded rebuild inputs and durable, source-attested progress. + +Manifests contain identifiers and counters only. Page reads finish before the +caller computes embeddings; publication must revalidate under writer ownership. +""" + +import datetime +import hashlib +import json +import sqlite3 +import uuid +from collections.abc import Iterator +from contextlib import AbstractContextManager, contextmanager, nullcontext +from dataclasses import asdict, dataclass, replace + +from truememory.maintenance import MaintenanceUnavailableError, SourceRevision, read_source_revision +from truememory.storage import _MAINTENANCE_SOURCE_FIELDS, _MESSAGES_TABLE_SQL, _maintenance_trigger_definitions + +_BRIDGE_TABLE = "truememory_rebuild_source_v1" +_BRIDGE_OWNER = "truememory.rebuild-source.v1" +_BRIDGE_TRIGGERS = tuple(f"truememory_rebuild_messages_v1_{suffix}" for suffix in ("ai", "au", "ad")) +_BRIDGE_SQL = f"""CREATE TABLE {_BRIDGE_TABLE} ( + singleton INTEGER PRIMARY KEY CHECK (singleton = 1), + owner TEXT NOT NULL CHECK (owner = '{_BRIDGE_OWNER}'), + epoch TEXT NOT NULL, + revision INTEGER NOT NULL CHECK (typeof(revision) = 'integer' AND revision >= 0), + insert_count INTEGER NOT NULL CHECK (typeof(insert_count) = 'integer' AND insert_count >= 0), + correction_count INTEGER NOT NULL CHECK (typeof(correction_count) = 'integer' AND correction_count >= 0), + max_seen_message_id INTEGER NOT NULL, + nonappend_revision INTEGER NOT NULL, + source_signature TEXT NOT NULL, + covered_columns TEXT NOT NULL, + tracking_ready INTEGER NOT NULL CHECK (tracking_ready IN (0, 1)) +)""" + + +class RebuildSourceChanged(RuntimeError): + """The captured source or generation can no longer be published.""" + + +def _normalized_sql(sql: str) -> str: + return " ".join(sql.strip().rstrip(";").split()) + + +def _source_schema(conn: sqlite3.Connection) -> tuple[tuple[str, ...], str, bool]: + table = conn.execute("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'messages'").fetchone() + columns = [tuple(row) for row in conn.execute("PRAGMA table_xinfo(messages)")] + ordinary = {row[1]: row for row in columns if row[6] == 0} + present = {row[1] for row in columns} + identifier = ordinary.get("id") + if (table is None or not table[0] or table[0].upper().startswith("CREATE VIRTUAL TABLE") + or "content" not in present or identifier is None + or identifier[2].upper() != "INTEGER" or identifier[5] != 1 + or sum(row[5] > 0 for row in columns) != 1): + raise MaintenanceUnavailableError("Rebuild tracking requires messages with an INTEGER PRIMARY KEY id and content") + indexes = [] + conservative = False + for index in conn.execute("PRAGMA index_list(messages)"): + index = tuple(index) + name = index[1] + definition = conn.execute("SELECT sql FROM sqlite_master WHERE type='index' AND name=?", (name,)).fetchone() + quoted = '"' + name.replace('"', '""') + '"' + indexes.append([index, tuple(definition) if definition else None, + [tuple(row) for row in conn.execute(f"PRAGMA index_xinfo({quoted})")]]) + # An indexed INTEGER PRIMARY KEY (for example inline DESC) is not + # a rowid alias. REPLACE on its separate rowid can delete a prefix row. + conservative |= bool(index[2]) + covered = tuple(name for name in _MAINTENANCE_SOURCE_FIELDS if name in present) + signature = hashlib.sha256(json.dumps([table[0], columns, indexes], separators=(",", ":")).encode()).hexdigest() + return covered, signature, conservative + + +def _installed_triggers(conn: sqlite3.Connection, names: tuple[str, ...]) -> dict[str, str]: + placeholders = ",".join("?" for _ in names) + return {name: _normalized_sql(sql) for name, sql in conn.execute( + f"SELECT name, sql FROM sqlite_master WHERE type = 'trigger' AND name IN ({placeholders})", names, + )} + + +def _canonical_revision(conn: sqlite3.Connection, covered: tuple[str, ...], conservative: bool) -> SourceRevision | None: + if conservative or covered != _MAINTENANCE_SOURCE_FIELDS: + return None + table = conn.execute("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'messages'").fetchone() + # Canonical triggers use declared column comparisons. Only our trusted + # definition attests their byte-sensitive semantics; custom DDL stays on + # the bridge without trying to parse arbitrary collations or affinities. + trusted = _MESSAGES_TABLE_SQL.replace("CREATE TABLE IF NOT EXISTS ", "CREATE TABLE ", 1) + if table is None or _normalized_sql(table[0]) != _normalized_sql(trusted): + return None + required = {"epoch", "revision", "insert_count", "correction_count", "max_seen_message_id", + "nonappend_revision", "tracking_ready", "singleton"} + if not required.issubset({row[1] for row in conn.execute("PRAGMA table_info(maintenance_source_state)")}): + return None + definitions = _maintenance_trigger_definitions() + if _installed_triggers(conn, tuple(definitions)) != {name: _normalized_sql(sql) for name, sql in definitions.items()}: + return None + try: + return read_source_revision(conn) + except MaintenanceUnavailableError: + return None + + +def _bridge_definitions(conn: sqlite3.Connection, covered: tuple[str, ...], conservative: bool) -> dict[str, str]: + # SQLite's BLOB cast formats REALs as text and can lose precision. Numeric + # comparison restores that distinction, but signed REAL zero needs a + # conservative invalidation because SQL equality does not expose its sign. + changed = " OR ".join( + f"typeof(old.{name}) IS NOT typeof(new.{name}) " + f"OR CAST(old.{name} AS BLOB) IS NOT CAST(new.{name} AS BLOB) " + f"OR old.{name} COLLATE BINARY IS NOT new.{name} COLLATE BINARY " + f"OR (typeof(old.{name}) = 'real' AND old.{name} = 0)" + for name in covered + ) + update_columns = "" + if not conservative: + columns = {row[1]: row[6] for row in conn.execute("PRAGMA table_xinfo(messages)")} + if not any(columns[name] for name in covered): + # Generated source fields can depend on any column. Ordinary + # sources can limit zero invalidation to source writes, including + # unshadowed rowid aliases that also update INTEGER PRIMARY KEY id. + aliases = tuple(name for name in ("rowid", "_rowid_", "oid") + if name not in {column.lower() for column in columns}) + update_columns = " OF " + ", ".join((*covered, *aliases)) + update_guard = "" if conservative else f" WHEN {changed}" + insert_fence = ("revision + 1" if conservative else + "CASE WHEN new.id <= max_seen_message_id THEN revision + 1 ELSE nonappend_revision END") + insert, update, delete = _BRIDGE_TRIGGERS + return { + insert: f"""CREATE TRIGGER {insert} AFTER INSERT ON messages BEGIN + UPDATE {_BRIDGE_TABLE} SET + nonappend_revision = {insert_fence}, + max_seen_message_id = MAX(max_seen_message_id, new.id), + revision = revision + 1, insert_count = insert_count + 1 + WHERE singleton = 1 AND tracking_ready = 1; + END""", + update: f"""CREATE TRIGGER {update} AFTER UPDATE{update_columns} ON messages{update_guard} BEGIN + UPDATE {_BRIDGE_TABLE} SET nonappend_revision = revision + 1, + max_seen_message_id = MAX(max_seen_message_id, new.id), + revision = revision + 1, correction_count = correction_count + 1 + WHERE singleton = 1 AND tracking_ready = 1; + END""", + delete: f"""CREATE TRIGGER {delete} AFTER DELETE ON messages BEGIN + UPDATE {_BRIDGE_TABLE} SET nonappend_revision = revision + 1, + revision = revision + 1, correction_count = correction_count + 1 + WHERE singleton = 1 AND tracking_ready = 1; + END""", + } + + +def _bridge_row(conn: sqlite3.Connection) -> tuple | None: + table = conn.execute("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?", (_BRIDGE_TABLE,)).fetchone() + if table is None: + return None + if _normalized_sql(table[0]) != _normalized_sql(_BRIDGE_SQL): + raise MaintenanceUnavailableError("Reserved rebuild tracking table has an unsupported definition") + return conn.execute( + f"SELECT epoch, revision, insert_count, correction_count, max_seen_message_id, nonappend_revision, " + f"source_signature, covered_columns, tracking_ready FROM {_BRIDGE_TABLE} WHERE singleton = 1 AND owner = ?", + (_BRIDGE_OWNER,), + ).fetchone() + + +def _valid_bridge(conn: sqlite3.Connection, covered: tuple[str, ...], signature: str, + conservative: bool) -> SourceRevision | None: + row = _bridge_row(conn) + definitions = _bridge_definitions(conn, covered, conservative) + if (row is None or not row[8] or row[6] != signature or row[7] != json.dumps(covered) + or _installed_triggers(conn, _BRIDGE_TRIGGERS) != {name: _normalized_sql(sql) for name, sql in definitions.items()}): + return None + return SourceRevision(*row[:6]) + + +def read_rebuild_revision(conn: sqlite3.Connection) -> tuple[str, SourceRevision, str]: + """Read only; a scoped bridge never certifies unrelated maintenance layers.""" + covered, signature, conservative = _source_schema(conn) + canonical = _canonical_revision(conn, covered, conservative) + if canonical is not None: + return "canonical-v1", canonical, signature + bridge = _valid_bridge(conn, covered, signature, conservative) + if bridge is None: + raise MaintenanceUnavailableError("Rebuild source tracking is unavailable or its schema identity changed") + return "rebuild-bridge-conservative-v1" if conservative else "rebuild-bridge-v1", bridge, signature + + +def ensure_rebuild_tracking(conn: sqlite3.Connection) -> None: + """Install only this rebuild's tracker, under writer ownership before models.""" + with rebuild_transaction(conn): + covered, signature, conservative = _source_schema(conn) + canonical = _canonical_revision(conn, covered, conservative) + bridge_row = _bridge_row(conn) + installed = _installed_triggers(conn, _BRIDGE_TRIGGERS) + if canonical is not None and not installed and (bridge_row is None or not bridge_row[8]): + return + if canonical is None and _valid_bridge(conn, covered, signature, conservative) is not None: + return + with rebuild_transaction(conn, write=True): + covered, signature, conservative = _source_schema(conn) + canonical = _canonical_revision(conn, covered, conservative) + bridge_row = _bridge_row(conn) + installed = _installed_triggers(conn, _BRIDGE_TRIGGERS) + owned_table = conn.execute("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?", (_BRIDGE_TABLE,)).fetchone() + if not owned_table and installed: + raise MaintenanceUnavailableError("Reserved rebuild trigger names are already in use without owned tracking state") + if canonical is not None: + if owned_table: + for name in installed: + conn.execute(f"DROP TRIGGER {name}") + conn.execute(f"UPDATE {_BRIDGE_TABLE} SET tracking_ready = 0 WHERE singleton = 1") + return + if _valid_bridge(conn, covered, signature, conservative) is not None: + return + if conn.execute("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = ?", (_BRIDGE_TABLE,)).fetchone() is None: + conn.execute(_BRIDGE_SQL) + for name in installed: + conn.execute(f"DROP TRIGGER {name}") + for definition in _bridge_definitions(conn, covered, conservative).values(): + conn.execute(definition) + # A repair cannot certify the interval before installation. A fresh + # epoch also prevents a later fallback from reviving a retired bridge. + conn.execute( + f"INSERT OR REPLACE INTO {_BRIDGE_TABLE} VALUES (1, ?, ?, 0, 0, 0, " + "(SELECT MAX(0, COALESCE(MAX(id), 0)) FROM messages), 0, ?, ?, 1)", + (_BRIDGE_OWNER, uuid.uuid4().hex, signature, json.dumps(covered)), + ) + + +@contextmanager +def rebuild_transaction(conn: sqlite3.Connection, *, write: bool = False, + publication_fence: AbstractContextManager | None = None) -> Iterator[None]: + owned = not conn.in_transaction + conn.execute(("BEGIN IMMEDIATE" if write else "BEGIN") if owned else "SAVEPOINT rebuild_source") + try: + if write and not owned: + conn.execute("UPDATE messages SET id = id WHERE 0") + with publication_fence if publication_fence is not None else nullcontext(): + yield + if owned: + if write: + conn.commit() + else: + conn.execute("COMMIT") + else: + conn.execute("RELEASE SAVEPOINT rebuild_source") + except BaseException: + if owned: + conn.rollback() + else: + conn.execute("ROLLBACK TO SAVEPOINT rebuild_source") + conn.execute("RELEASE SAVEPOINT rebuild_source") + raise + + +@dataclass(frozen=True) +class RebuildSource: + revision: SourceRevision + after_id: int | None + high_id: int | None + total: int + tracker: str = "canonical-v1" + schema_signature: str | None = None + + def check(self, conn: sqlite3.Connection) -> None: + try: + tracker, revision, signature = read_rebuild_revision(conn) + except MaintenanceUnavailableError as error: + raise RebuildSourceChanged("Rebuild source tracking changed; start a fresh generation") from error + if (tracker != self.tracker or signature != self.schema_signature + or not revision.is_append_only_since(self.revision)): + raise RebuildSourceChanged("Rebuild source changed; start a fresh generation") + + def page(self, conn: sqlite3.Connection, after_id: int | None, size: int, *, + include_metadata: bool = True) -> list[dict]: + if size < 1: + raise ValueError("Rebuild page size must be positive") + with rebuild_transaction(conn): + self.check(conn) + if self.high_id is None: + return [] + lower = " AND id > ?" if after_id is not None else "" + parameters = (self.high_id, after_id, size) if lower else (self.high_id, size) + columns = (("id", "content", "sender", "recipient", "timestamp") + if include_metadata else ("id", "content")) + cursor = conn.execute( + f"SELECT {', '.join(columns)} FROM messages WHERE id <= ?{lower} ORDER BY id LIMIT ?", parameters, + ) + try: + return [dict(zip(columns, row)) for row in cursor] + finally: + cursor.close() + + +def capture_source(conn: sqlite3.Connection, *, after_id: int | None = None) -> RebuildSource: + with rebuild_transaction(conn): + tracker, revision, signature = read_rebuild_revision(conn) + predicate = " WHERE id > ?" if after_id is not None else "" + count, high = conn.execute( + f"SELECT COUNT(*), MAX(id) FROM messages{predicate}", + (after_id,) if predicate else (), + ).fetchone() + return RebuildSource(revision, after_id, high, count, tracker, signature) + + +def input_fingerprint(messages: list[dict], *, separation: bool) -> str: + """Bind explicit caller input without copying its corpus or changing order.""" + digest = hashlib.sha256() + fields = ("id", "content", "sender", "recipient", "timestamp") if separation else ("id", "content") + for message in messages: + payload = json.dumps([message.get(field) for field in fields], ensure_ascii=False, + separators=(",", ":")).encode("utf-8") + digest.update(len(payload).to_bytes(8, "big")) + digest.update(payload) + return digest.hexdigest() + + +@dataclass(frozen=True) +class RebuildManifest: + generation: str + model: str + dimension: int + targets: tuple[str, ...] + source: RebuildSource | None + input_digest: str | None + total: int + cursor: int | None = None + consumed: int = 0 + outputs: int = 0 + complete: bool = False + schema_version: int | None = None + + @classmethod + def new(cls, model: str, dimension: int, targets: tuple[str, ...], *, + source: RebuildSource | None = None, input_digest: str | None = None, + total: int = 0) -> "RebuildManifest": + return cls(uuid.uuid4().hex, model, dimension, targets, source, input_digest, + source.total if source is not None else total, + source.after_id if source is not None else None) + + def advance(self, last_id: int, consumed: int, outputs: int) -> "RebuildManifest": + return replace(self, cursor=last_id, consumed=self.consumed + consumed, + outputs=self.outputs + outputs) + + def check(self, conn: sqlite3.Connection, key: str) -> None: + if self.schema_version != conn.execute("PRAGMA schema_version").fetchone()[0]: + raise RebuildSourceChanged("Rebuild database schema changed; start a fresh generation") + if self.source is not None: + self.source.check(conn) + saved = load_manifest(conn, key) + if saved != self: + raise RebuildSourceChanged("Rebuild generation changed; retry with its current owner") + + +def manifest_key(targets: tuple[str, ...]) -> str: + return "vec_source_v1:" + ":".join(targets) + + +def load_manifest(conn: sqlite3.Connection, key: str) -> RebuildManifest | None: + try: + row = conn.execute("SELECT value FROM metadata WHERE key = ?", (key,)).fetchone() + except sqlite3.OperationalError as error: + if str(error) == "no such table: metadata": + return None + raise + if row is None: + return None + try: + payload = json.loads(row[0]) + if not isinstance(payload, dict) or payload.pop("version") != 1: + return None + source = payload["source"] + if source is not None: + source["revision"] = SourceRevision(*source["revision"]) + payload["source"] = RebuildSource(**source) + payload["targets"] = tuple(payload["targets"]) + manifest = RebuildManifest(**payload) + if not 0 <= manifest.consumed <= manifest.total or manifest.outputs < 0: + return None + return manifest + except (TypeError, ValueError, KeyError): + return None + + +def save_manifest(conn: sqlite3.Connection, key: str, manifest: RebuildManifest) -> None: + conn.execute("CREATE TABLE IF NOT EXISTS metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT)") + payload = asdict(manifest) + payload["version"] = 1 + conn.execute( + "INSERT OR REPLACE INTO metadata(key,value,updated_at) VALUES (?,?,?)", + (key, json.dumps(payload, separators=(",", ":")), + datetime.datetime.now(datetime.timezone.utc).isoformat()), + ) diff --git a/truememory/reranker.py b/truememory/reranker.py index 3308e23b..35a6d04b 100644 --- a/truememory/reranker.py +++ b/truememory/reranker.py @@ -27,6 +27,8 @@ import logging import threading +from collections.abc import Iterator +from contextlib import contextmanager from typing import TYPE_CHECKING log = logging.getLogger(__name__) @@ -40,6 +42,7 @@ _model = None _model_name: str = "cross-encoder/ms-marco-MiniLM-L-6-v2" +_model_certified = False _lock = threading.Lock() _inference_lock = threading.Lock() # Protects concurrent model.predict() calls @@ -63,6 +66,7 @@ _TIER_RERANKERS = {t: cfg["reranker"] for t, cfg in _TIERS.items()} _active_tier: str | None = None # None = not yet resolved; resolved lazily +_frozen_reranker_id: str | None = None def get_reranker_name_for_tier(tier: str) -> str: @@ -134,12 +138,15 @@ def set_active_tier(tier: str) -> None: resolve to the new tier's reranker. Empty / unknown tier falls back to "edge". """ - global _active_tier - if not tier: - _active_tier = "edge" - return - t = tier.strip().lower() - _active_tier = t if t in ("edge", "base", "pro", "custom") else "edge" + global _active_tier, _frozen_reranker_id + from truememory.tier_switch.serving import exclusive_activation + with exclusive_activation(): + _frozen_reranker_id = None + if not tier: + _active_tier = "edge" + return + t = tier.strip().lower() + _active_tier = t if t in ("edge", "base", "pro", "custom") else "edge" def get_current_reranker_name() -> str: @@ -150,11 +157,52 @@ def get_current_reranker_name() -> str: is called. """ global _active_tier + import sys + runtime = sys.modules.get("truememory.tier_switch.runtime") + operation = runtime.current_runtime_operation() if runtime is not None else None + if operation is not None: + return operation.reranker_id + if _frozen_reranker_id is not None: + return _frozen_reranker_id if _active_tier is None: _active_tier = _resolve_tier_from_env_and_config() return get_reranker_name_for_tier(_active_tier) +def apply_reranker_policy(tier: str, model_name: str, *, deadline: float | None = None, + cancelled: threading.Event | None = None) -> None: + """Publish policy identity without loading an absent native model.""" + global _model, _model_name, _model_certified, _active_tier, _frozen_reranker_id + from truememory.tier_switch.runtime import _reranker_id + from truememory.tier_switch.serving import exclusive_activation, _control + from truememory.model_client import CertifiedRerankerProxy, RerankerProxy + _reranker_id(model_name) + control = _control(None, deadline, cancelled) + with exclusive_activation(deadline=deadline, cancelled=cancelled), _reranker_load_lock(control): + if _model_name != model_name: + _model, _model_certified = None, False + elif isinstance(_model, RerankerProxy): + _model, _model_certified = CertifiedRerankerProxy(model_name), True + _model_name, _active_tier, _frozen_reranker_id = model_name, tier, model_name + + +def apply_frozen_reranker( + tier: str, model_name: str, *, deadline: float | None = None, + cancelled: threading.Event | None = None, +) -> None: + """Publish an explicit default only after its existing singleton is ready.""" + global _active_tier, _frozen_reranker_id + from truememory.tier_switch.serving import exclusive_activation + with exclusive_activation(deadline=deadline, cancelled=cancelled): + # Release before loading a distinct reranker. An acknowledgement belongs + # to the runtime bridge and is not published on this partial boundary. + if _model_name != model_name: + unload_reranker() + get_reranker(model_name=model_name, deadline=deadline, cancelled=cancelled, certified=True) + _active_tier = tier + _frozen_reranker_id = model_name + + def unload_reranker( should_unload: "Callable[[], bool] | None" = None, ) -> bool: @@ -170,7 +218,8 @@ def unload_reranker( ``True`` if the model was actually unloaded, ``False`` if skipped. """ global _model - with _lock: + from truememory.tier_switch.serving import exclusive_activation + with exclusive_activation(), _lock: if should_unload is not None and not should_unload(): return False if _model is None: @@ -179,12 +228,32 @@ def unload_reranker( return True -def get_reranker(model_name: str | None = None, device: str | None = None): +@contextmanager +def _reranker_load_lock(control: object) -> Iterator[None]: + from truememory.tier_switch.serving import _check, _wait_seconds + acquired = False + try: + while not acquired: + _check(control) + seconds = _wait_seconds(control) + acquired = _lock.acquire() if seconds is None else _lock.acquire(timeout=seconds) + _check(control) + yield + finally: + if acquired: + _lock.release() + + +def get_reranker( + model_name: str | None = None, device: str | None = None, *, + deadline: float | None = None, cancelled: threading.Event | None = None, certified: bool = False, +): """ Lazy-load the cross-encoder reranker (singleton). When the shared model server is enabled (default), returns a proxy - that routes inference to the server. Falls back to local loading. + that routes inference to the server even when its endpoint is absent. + Local loading requires explicit TRUEMEMORY_NO_MODEL_SERVER=1. Args: model_name: HuggingFace model ID. If None (the default), resolves via @@ -198,43 +267,62 @@ def get_reranker(model_name: str | None = None, device: str | None = None): Returns: A ``sentence_transformers.CrossEncoder`` instance or RerankerProxy. """ - global _model, _model_name + global _model, _model_name, _model_certified + from truememory.tier_switch.serving import _control, _check + control = _control(None, deadline, cancelled) + _check(control) + if type(certified) is not bool: + raise TypeError("Reranker certification must be boolean") name = model_name or get_current_reranker_name() + from truememory.tier_switch.runtime import current_runtime_operation, TierRuntimeError + operation = current_runtime_operation() + if operation is not None and name != operation.reranker_id: + raise TierRuntimeError("Reranker override must be captured before serving admission") + if operation is not None and operation.selection is not None: + certified = True cached_model, cached_name = _model, _model_name - if cached_model is not None and name == cached_name: + if cached_model is not None and name == cached_name and (not certified or _model_certified): return cached_model - with _lock: - cached_model, cached_name = _model, _model_name - if cached_model is not None and name == cached_name: - return cached_model - - from truememory.model_client import use_model_server, get_reranker_proxy - if use_model_server(): - try: - proxy = get_reranker_proxy(model_name=name) + from truememory.tier_switch.runtime import require_model_load_allowed + require_model_load_allowed() + from contextlib import nullcontext + from truememory.tier_switch.serving import exclusive_activation + with exclusive_activation(deadline=deadline, cancelled=cancelled) if operation is None else nullcontext(): + with _reranker_load_lock(control): + cached_model, cached_name = _model, _model_name + if cached_model is not None and name == cached_name and (not certified or _model_certified): + return cached_model + + _model = cached_model = None # Release a distinct cached slot before construction. + from truememory.model_client import use_model_server, get_reranker_proxy + if use_model_server(): + _check(control) + if certified: + from truememory.model_client import CertifiedRerankerProxy + proxy = CertifiedRerankerProxy(name) + else: + proxy = get_reranker_proxy(model_name=name) _model = proxy _model_name = name + _model_certified = certified return proxy - except Exception: - log.warning( - "Model server available but reranker proxy failed — " - "falling back to local model loading (high memory cost). " - "Check ~/.truememory/model_server.stderr for details." - ) - - from sentence_transformers import CrossEncoder - - if device is None: - # Issue #577: honor TRUEMEMORY_DEVICE (cpu|mps|cuda|auto) before - # auto-detection. An explicit device= parameter wins over the env. - from truememory.mps_utils import auto_detect_device, resolve_device - device = resolve_device(auto_detect_device()) - - _model = CrossEncoder(name, device=device) - _model_name = name - result = _model - return result + + if device is None: + # Issue #577: honor TRUEMEMORY_DEVICE (cpu|mps|cuda|auto) before + # auto-detection. An explicit device= parameter wins over the env. + from truememory.mps_utils import auto_detect_device, resolve_device + device = resolve_device(auto_detect_device()) + + from truememory.mps_utils import ensure_mps_memory_budget + ensure_mps_memory_budget(device) + from sentence_transformers import CrossEncoder + _check(control) + _model = CrossEncoder(name, device=device) + _model_name = name + _model_certified = True + result = _model + return result # --------------------------------------------------------------------------- diff --git a/truememory/storage.py b/truememory/storage.py index 02d908c0..967f171f 100644 --- a/truememory/storage.py +++ b/truememory/storage.py @@ -18,6 +18,7 @@ import os import sqlite3 import time +from collections.abc import Callable from pathlib import Path log = logging.getLogger(__name__) @@ -27,9 +28,103 @@ # Schema DDL # --------------------------------------------------------------------------- -_SCHEMA_SQL = """ --- Core messages table -CREATE TABLE IF NOT EXISTS messages ( +_MESSAGES_FTS_FIELDS = ("id", "content", "sender", "recipient", "category", "modality") +_MESSAGES_UNIQUE_GUARD = 'EXISTS (SELECT 1 FROM pragma_index_list(\'messages\') WHERE "unique" = 1)' + + +def _message_value_changed(fields: tuple[str, ...]) -> str: + # BLOB casts format REALs with insufficient precision. BINARY comparison + # preserves that precision, but SQL cannot distinguish signed REAL zero. + return " OR ".join( + f"typeof(old.{field}) IS NOT typeof(new.{field}) " + f"OR old.{field} COLLATE BINARY IS NOT new.{field} COLLATE BINARY " + f"OR (typeof(old.{field}) = 'real' AND old.{field} = 0)" + for field in fields + ) + + +def _message_update_columns(conn: sqlite3.Connection | None, fields: tuple[str, ...]) -> str: + columns = {} if conn is None else { + row[1].lower(): row[6] for row in conn.execute("PRAGMA table_xinfo(messages)") + } + if any(columns.get(field, 0) for field in fields): + # A generated source can depend on any otherwise-derived column. + return "" + aliases = tuple(alias for alias in ("rowid", "_rowid_", "oid") if alias not in columns) + return " OF " + ", ".join((*fields, *aliases)) + + +def _messages_have_rowid_id(conn: sqlite3.Connection) -> bool: + columns = [tuple(row) for row in conn.execute("PRAGMA table_xinfo(messages)")] + identifier = next((row for row in columns if row[1].lower() == "id"), None) + table = conn.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='messages'").fetchone() + return bool( + table and table[0] and not table[0].upper().startswith("CREATE VIRTUAL TABLE") + and identifier and identifier[2].upper() == "INTEGER" and identifier[5] == 1 + and identifier[6] == 0 and sum(row[5] > 0 for row in columns) == 1 + # WITHOUT ROWID and inline INTEGER PRIMARY KEY DESC have a separate + # primary-key index. Their id does not provide the required rowid alias. + and not any(row[3] == "pk" for row in conn.execute("PRAGMA index_list(messages)")) + ) + + +def _messages_fts_trigger_definitions(conn: sqlite3.Connection | None = None) -> dict[str, str]: + definitions = { + "messages_ai": """CREATE TRIGGER messages_ai AFTER INSERT ON messages BEGIN + INSERT INTO messages_fts(rowid, content, sender, recipient, category, modality) + VALUES (new.id, new.content, new.sender, new.recipient, new.category, new.modality); + END""", + "messages_ad": """CREATE TRIGGER messages_ad AFTER DELETE ON messages BEGIN + DELETE FROM messages_fts WHERE rowid = old.id; + END""", + "messages_au": f"""CREATE TRIGGER messages_au AFTER UPDATE{_message_update_columns(conn, _MESSAGES_FTS_FIELDS)} ON messages + WHEN {_message_value_changed(_MESSAGES_FTS_FIELDS)} BEGIN + DELETE FROM messages_fts WHERE rowid = old.id; + INSERT INTO messages_fts(rowid, content, sender, recipient, category, modality) + VALUES (new.id, new.content, new.sender, new.recipient, new.category, new.modality); + END""", + } + for suffix, event in (("ai", "INSERT"), ("au", "UPDATE")): + name = f"messages_fts_cleanup_{suffix}" + # The trigger-level guard avoids an FTS scan on ordinary schemas. + # Source fencing is independent of this cleanup and its trigger order. + definitions[name] = f"""CREATE TRIGGER {name} AFTER {event} ON messages + WHEN {_MESSAGES_UNIQUE_GUARD} BEGIN + DELETE FROM messages_fts WHERE NOT EXISTS ( + SELECT 1 FROM messages WHERE messages.id = messages_fts.rowid + ); + END""" + return definitions + + +_MESSAGES_FTS_UPDATE_TRIGGER_SQL = _messages_fts_trigger_definitions()["messages_au"].replace( + "CREATE TRIGGER ", "CREATE TRIGGER IF NOT EXISTS ", 1, +) + ";\n" + +_DUNBAR_OWNERSHIP_SQL = """ +CREATE TABLE IF NOT EXISTS dunbar_generated_relationships ( + relationship_id INTEGER PRIMARY KEY, + generation TEXT NOT NULL, + row_fingerprint BLOB NOT NULL CHECK (length(row_fingerprint) = 32), + FOREIGN KEY (relationship_id) REFERENCES entity_relationships(id) ON DELETE CASCADE +) +""" +_DUNBAR_OWNERSHIP_TRIGGERS = { + "dunbar_relationships_ai": """CREATE TRIGGER dunbar_relationships_ai AFTER INSERT ON entity_relationships BEGIN + DELETE FROM dunbar_generated_relationships WHERE relationship_id=new.id; + END""", + "dunbar_relationships_au": """CREATE TRIGGER dunbar_relationships_au AFTER UPDATE ON entity_relationships + WHEN old.id IS NOT new.id OR old.entity_a IS NOT new.entity_a OR old.entity_b IS NOT new.entity_b + OR old.relationship_type IS NOT new.relationship_type OR old.strength IS NOT new.strength + OR old.dunbar_layer IS NOT new.dunbar_layer OR old.last_interaction IS NOT new.last_interaction BEGIN + DELETE FROM dunbar_generated_relationships WHERE relationship_id=old.id OR relationship_id=new.id; + END""", + "dunbar_relationships_ad": """CREATE TRIGGER dunbar_relationships_ad AFTER DELETE ON entity_relationships BEGIN + DELETE FROM dunbar_generated_relationships WHERE relationship_id=old.id; + END""", +} + +_MESSAGES_TABLE_SQL = """CREATE TABLE IF NOT EXISTS messages ( id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT NOT NULL, sender TEXT DEFAULT '', @@ -42,7 +137,9 @@ embedding_separation BLOB DEFAULT NULL, directive INTEGER DEFAULT 0, metadata TEXT DEFAULT '{}' -); +);""" + +_SCHEMA_SQL = "\n-- Core messages table\n" + _MESSAGES_TABLE_SQL + """ -- FTS5 virtual table for full-text search CREATE VIRTUAL TABLE IF NOT EXISTS messages_fts USING fts5( @@ -61,11 +158,7 @@ DELETE FROM messages_fts WHERE rowid = old.id; END; -CREATE TRIGGER IF NOT EXISTS messages_au AFTER UPDATE ON messages BEGIN - DELETE FROM messages_fts WHERE rowid = old.id; - INSERT INTO messages_fts(rowid, content, sender, recipient, category, modality) - VALUES (new.id, new.content, new.sender, new.recipient, new.category, new.modality); -END; +""" + _MESSAGES_FTS_UPDATE_TRIGGER_SQL + """ -- Entity profiles (L0 Personality Engram) CREATE TABLE IF NOT EXISTS entity_profiles ( @@ -83,7 +176,9 @@ entity TEXT PRIMARY KEY, vector TEXT, message_count INTEGER DEFAULT 0, - updated_at TEXT + updated_at TEXT, + vector_sum BLOB DEFAULT NULL, + accumulator_version INTEGER NOT NULL DEFAULT 0 ); -- Fact timeline (L5 contradiction tracking) @@ -241,8 +336,315 @@ ); CREATE INDEX IF NOT EXISTS idx_rebuild_status_active ON rebuild_status(tier_group, status); + +CREATE TABLE IF NOT EXISTS maintenance_source_state ( + singleton INTEGER PRIMARY KEY CHECK (singleton = 1), + epoch TEXT NOT NULL, + revision INTEGER NOT NULL DEFAULT 0 CHECK (typeof(revision) = 'integer' AND revision >= 0), + insert_count INTEGER NOT NULL DEFAULT 0 CHECK (typeof(insert_count) = 'integer' AND insert_count >= 0), + correction_count INTEGER NOT NULL DEFAULT 0 CHECK (typeof(correction_count) = 'integer' AND correction_count >= 0), + max_seen_message_id INTEGER NOT NULL DEFAULT 0, + nonappend_revision INTEGER NOT NULL DEFAULT 0, + tracking_ready INTEGER NOT NULL DEFAULT 0 CHECK (tracking_ready IN (0, 1)) +); + +CREATE TABLE IF NOT EXISTS maintenance_layers ( + layer TEXT PRIMARY KEY, + builder_version INTEGER NOT NULL DEFAULT 1, + outcome TEXT NOT NULL DEFAULT 'pending' CHECK (outcome IN + ('pending', 'running', 'success', 'success_empty', 'failed', 'unavailable', 'disabled', 'abandoned')), + successful_epoch TEXT, + successful_revision INTEGER, + successful_dependency TEXT, + successful_coverage TEXT NOT NULL DEFAULT 'unverified' CHECK (successful_coverage IN + ('unverified', 'complete', 'legacy_contacts_unowned', 'vector_generation_unverified')), + attempted_epoch TEXT, + attempted_revision INTEGER, + attempted_dependency TEXT, + attempted_insert_count INTEGER CHECK (attempted_insert_count IS NULL OR + (typeof(attempted_insert_count) = 'integer' AND attempted_insert_count >= 0)), + full_rebuild_required INTEGER NOT NULL DEFAULT 1 CHECK (full_rebuild_required IN (0, 1)), + output_count INTEGER, + run_generation TEXT, + error_category TEXT +); """ +_MAINTENANCE_LAYERS = ( + "clusters", "summaries", "contradictions", "structured_facts", + "surprise", "episodes", "landmarks", "dunbar", +) + +_STYLE_OUTPUT_INVALIDATE_SQL = """UPDATE maintenance_layers SET + outcome='pending',full_rebuild_required=1,successful_coverage='unverified', + attempted_epoch=NULL,attempted_revision=NULL,attempted_dependency=NULL, + attempted_insert_count=NULL,run_generation=NULL,error_category=NULL + WHERE layer='style_vectors'""" +_STYLE_OUTPUT_TRIGGERS = { + "truememory_style_output_" + operation.lower(): + "CREATE TRIGGER truememory_style_output_" + operation.lower() + + " AFTER " + operation + " ON entity_style_vectors BEGIN " + + _STYLE_OUTPUT_INVALIDATE_SQL + "; END" + for operation in ("INSERT", "UPDATE", "DELETE") +} + + +def _style_output_tracking_ready(conn: sqlite3.Connection) -> bool: + expected = {name: " ".join(sql.split()) for name, sql in _STYLE_OUTPUT_TRIGGERS.items()} + installed = {name: " ".join(sql.strip().rstrip(";").split()) for name, sql in conn.execute( + "SELECT name,sql FROM sqlite_master WHERE type='trigger' AND name IN (?,?,?)", tuple(expected), + )} + return installed == expected and conn.execute( + "SELECT 1 FROM maintenance_layers WHERE layer='style_vectors'").fetchone() is not None + + +def _prepare_style_maintenance( + conn: sqlite3.Connection, *, _on_safe_failure: Callable[[sqlite3.OperationalError], None] | None = None, +) -> None: + """Explicitly enroll style output; ordinary schema initialization stays lazy.""" + expected = {name: " ".join(sql.split()) for name, sql in _STYLE_OUTPUT_TRIGGERS.items()} + + def installed() -> dict[str, str]: + return {name: " ".join(sql.strip().rstrip(";").split()) for name, sql in conn.execute( + "SELECT name,sql FROM sqlite_master WHERE type='trigger' AND name IN (?,?,?)", + tuple(expected), + )} + + owned = not conn.in_transaction + try: + if _style_output_tracking_ready(conn): + return + conn.execute("BEGIN IMMEDIATE" if owned else "SAVEPOINT truememory_style_tracking") + except sqlite3.OperationalError as error: + # No preparation writes precede admission. A failed admission is safe + # only while the original transaction boundary remains unchanged. + if _on_safe_failure is not None and conn.in_transaction == (not owned): + _on_safe_failure(error) + raise + try: + conn.execute("UPDATE maintenance_layers SET layer=layer WHERE 0") + conn.execute("INSERT OR IGNORE INTO maintenance_layers(layer) VALUES ('style_vectors')") + current = installed() + if current != expected: + for name, sql in _STYLE_OUTPUT_TRIGGERS.items(): + if current.get(name) != expected[name]: + conn.execute("DROP TRIGGER IF EXISTS " + name) + conn.execute(sql) + # Missing/replaced triggers make even v1 output unverified. This + # metadata-only invalidation also supersedes a failed attempt. + conn.execute(_STYLE_OUTPUT_INVALIDATE_SQL) + conn.execute("COMMIT" if owned else "RELEASE SAVEPOINT truememory_style_tracking") + except BaseException as error: + if conn.in_transaction: + if owned: + conn.rollback() + else: + conn.execute("ROLLBACK TO SAVEPOINT truememory_style_tracking") + conn.execute("RELEASE SAVEPOINT truememory_style_tracking") + if (_on_safe_failure is not None and isinstance(error, sqlite3.OperationalError) + and conn.in_transaction == (not owned)): + _on_safe_failure(error) + # An ended transaction, failed rollback or failed savepoint release + # cannot attest that preparation left the previous generation intact. + raise + + +_SCHEMA_SQL += _DUNBAR_OWNERSHIP_SQL + ";\n" + + +def _initialize_style_accumulator_schema(conn: sqlite3.Connection) -> None: + """Add raw-sum storage without upgrading or rebuilding legacy profiles.""" + required = {"vector_sum", "accumulator_version"} + columns = {row[1] for row in conn.execute("PRAGMA table_info(entity_style_vectors)")} + if required <= columns: + return + owned = not conn.in_transaction + conn.execute("BEGIN IMMEDIATE" if owned else "SAVEPOINT truememory_style_schema") + completed = False + try: + conn.execute("UPDATE entity_style_vectors SET entity=entity WHERE 0") + columns = {row[1] for row in conn.execute("PRAGMA table_info(entity_style_vectors)")} + if "vector_sum" not in columns: + conn.execute("ALTER TABLE entity_style_vectors ADD COLUMN vector_sum BLOB DEFAULT NULL") + if "accumulator_version" not in columns: + conn.execute("ALTER TABLE entity_style_vectors ADD COLUMN accumulator_version INTEGER NOT NULL DEFAULT 0") + conn.execute("COMMIT" if owned else "RELEASE SAVEPOINT truememory_style_schema") + completed = True + finally: + if not completed and conn.in_transaction: + if owned: + conn.rollback() + else: + conn.execute("ROLLBACK TO SAVEPOINT truememory_style_schema") + conn.execute("RELEASE SAVEPOINT truememory_style_schema") + + +def _dunbar_ownership_ready(conn: sqlite3.Connection) -> bool: + definitions = {name: " ".join(sql.split()) for name, sql in _DUNBAR_OWNERSHIP_TRIGGERS.items()} + installed = {row[0]: " ".join(row[1].strip().rstrip(";").split()) for row in conn.execute( + "SELECT name,sql FROM sqlite_master WHERE type='trigger' AND name IN " + "('dunbar_relationships_ai','dunbar_relationships_au','dunbar_relationships_ad')" + )} + return installed == definitions and conn.execute( + "SELECT 1 FROM sqlite_master WHERE type='table' AND name='dunbar_generated_relationships'" + ).fetchone() is not None + + +def _initialize_dunbar_ownership(conn: sqlite3.Connection) -> None: + """Repair ownership tracking without trusting rows from an untracked interval.""" + if _dunbar_ownership_ready(conn): + return + owned = not conn.in_transaction + conn.execute("BEGIN IMMEDIATE" if owned else "SAVEPOINT truememory_dunbar_schema") + completed = False + try: + if not owned: + conn.execute("UPDATE entity_relationships SET id=id WHERE 0") + if not _dunbar_ownership_ready(conn): + conn.execute(_DUNBAR_OWNERSHIP_SQL) + # Identical ID/value replacements cannot be detected retrospectively. + # Preserve those relationships and relinquish all uncertain ownership. + conn.execute("DELETE FROM dunbar_generated_relationships") + for name, definition in _DUNBAR_OWNERSHIP_TRIGGERS.items(): + conn.execute(f"DROP TRIGGER IF EXISTS {name}") + conn.execute(definition) + conn.execute("COMMIT" if owned else "RELEASE truememory_dunbar_schema") + completed = True + finally: + if not completed and conn.in_transaction: + if owned: + conn.rollback() + else: + conn.execute("ROLLBACK TO truememory_dunbar_schema") + conn.execute("RELEASE truememory_dunbar_schema") + + +_MAINTENANCE_SOURCE_FIELDS = ( + "id", "content", "sender", "recipient", "timestamp", "category", + "modality", "directive", "metadata", +) + + +def _maintenance_trigger_definitions(conn: sqlite3.Connection | None = None) -> dict[str, str]: + changed = _message_value_changed(_MAINTENANCE_SOURCE_FIELDS) + correction = """UPDATE maintenance_source_state SET + nonappend_revision = revision + 1, + max_seen_message_id = MAX(max_seen_message_id, new.id), + revision = revision + 1, correction_count = correction_count + 1 + WHERE singleton = 1;""" + return { + "messages_maintenance_ai": f"""CREATE TRIGGER messages_maintenance_ai AFTER INSERT ON messages BEGIN + UPDATE maintenance_source_state SET + nonappend_revision = CASE WHEN new.id <= max_seen_message_id OR {_MESSAGES_UNIQUE_GUARD} + THEN revision + 1 ELSE nonappend_revision END, + max_seen_message_id = MAX(max_seen_message_id, new.id), + revision = revision + 1, insert_count = insert_count + 1 + WHERE singleton = 1; + END""", + "messages_maintenance_au": f"""CREATE TRIGGER messages_maintenance_au AFTER UPDATE{_message_update_columns(conn, _MAINTENANCE_SOURCE_FIELDS)} ON messages + WHEN NOT {_MESSAGES_UNIQUE_GUARD} AND ({changed}) BEGIN + {correction} + END""", + "messages_maintenance_unique_au": f"""CREATE TRIGGER messages_maintenance_unique_au AFTER UPDATE ON messages + WHEN {_MESSAGES_UNIQUE_GUARD} BEGIN + {correction} + END""", + "messages_maintenance_ad": """CREATE TRIGGER messages_maintenance_ad AFTER DELETE ON messages BEGIN + UPDATE maintenance_source_state SET + nonappend_revision = revision + 1, + revision = revision + 1, correction_count = correction_count + 1 + WHERE singleton = 1; + END""", + } + + +def _initialize_maintenance_tracking(conn: sqlite3.Connection) -> None: + """Install revision tracking atomically; incomplete legacy schemas stay open.""" + definitions = _maintenance_trigger_definitions(conn) + + def available() -> bool: + columns = {row[1].lower() for row in conn.execute("PRAGMA table_xinfo(messages)")} + return set(_MAINTENANCE_SOURCE_FIELDS).issubset(columns) and _messages_have_rowid_id(conn) + + def installed() -> dict[str, str]: + placeholders = ",".join("?" for _ in definitions) + return {row[0]: " ".join(row[1].strip().rstrip(";").split()) for row in conn.execute( + "SELECT name, sql FROM sqlite_master WHERE type = 'trigger' " + f"AND name IN ({placeholders})", tuple(definitions), + )} + + expected = {name: " ".join(sql.split()) for name, sql in definitions.items()} + ready = conn.execute("SELECT tracking_ready FROM maintenance_source_state WHERE singleton = 1").fetchone() + layers = {row[0] for row in conn.execute("SELECT layer FROM maintenance_layers")} + layer_columns = {row[1] for row in conn.execute("PRAGMA table_info(maintenance_layers)")} + if (ready and ready[0] and available() and installed() == expected and set(_MAINTENANCE_LAYERS).issubset(layers) + and {"attempted_insert_count", "successful_coverage"}.issubset(layer_columns)): + return + + owned = not conn.in_transaction + conn.execute("BEGIN IMMEDIATE" if owned else "SAVEPOINT truememory_maintenance_schema") + completed = False + try: + # Recheck after writer admission: a concurrent opener may migrate first. + if not owned: + conn.execute("UPDATE maintenance_source_state SET tracking_ready=tracking_ready WHERE 0") + definitions = _maintenance_trigger_definitions(conn) + expected = {name: " ".join(sql.split()) for name, sql in definitions.items()} + if "attempted_insert_count" not in {row[1] for row in conn.execute("PRAGMA table_info(maintenance_layers)")}: + conn.execute( + "ALTER TABLE maintenance_layers ADD COLUMN attempted_insert_count INTEGER " + "CHECK (attempted_insert_count IS NULL OR " + "(typeof(attempted_insert_count) = 'integer' AND attempted_insert_count >= 0))" + ) + if "successful_coverage" not in {row[1] for row in conn.execute("PRAGMA table_info(maintenance_layers)")}: + conn.execute( + "ALTER TABLE maintenance_layers ADD COLUMN successful_coverage TEXT NOT NULL DEFAULT 'unverified' " + "CHECK (successful_coverage IN " + "('unverified','complete','legacy_contacts_unowned','vector_generation_unverified'))" + ) + supported = available() + initial_max = "MAX(0, COALESCE(MAX(id), 0)) FROM messages" if _messages_have_rowid_id(conn) else "0" + conn.execute( + "INSERT OR IGNORE INTO maintenance_source_state(singleton, epoch, max_seen_message_id) " + f"SELECT 1, lower(hex(randomblob(16))), {initial_max}" + ) + conn.executemany("INSERT OR IGNORE INTO maintenance_layers(layer) VALUES (?)", + ((layer,) for layer in _MAINTENANCE_LAYERS)) + current = installed() + if supported: + # Missing/replaced triggers or an untracked legacy interval cannot + # attest the preceding source history. Invalidate old tokens. + invalidate = current != expected or not conn.execute( + "SELECT tracking_ready FROM maintenance_source_state WHERE singleton = 1" + ).fetchone()[0] + if invalidate: + conn.execute("UPDATE maintenance_source_state SET tracking_ready = 0 WHERE singleton = 1") + conn.execute("UPDATE maintenance_layers SET outcome = 'pending', full_rebuild_required = 1") + conn.execute( + "UPDATE maintenance_source_state SET epoch = lower(hex(randomblob(16))), " + "revision = 0, insert_count = 0, correction_count = 0, nonappend_revision = 0, " + "max_seen_message_id = (SELECT MAX(0, COALESCE(MAX(id), 0)) FROM messages), tracking_ready = 1 " + "WHERE singleton = 1 AND tracking_ready = 0" + ) + for name, definition in definitions.items(): + if current.get(name) != expected[name]: + conn.execute(f"DROP TRIGGER IF EXISTS {name}") + conn.execute(definition) + else: + conn.execute("UPDATE maintenance_source_state SET tracking_ready = 0 WHERE singleton = 1 AND tracking_ready != 0") + for name in current: + conn.execute(f"DROP TRIGGER {name}") + log.warning("Source revision tracking unavailable for unsupported or incomplete messages schema") + conn.execute("COMMIT" if owned else "RELEASE truememory_maintenance_schema") + completed = True + finally: + if not completed and conn.in_transaction: + if owned: + conn.rollback() + else: + conn.execute("ROLLBACK TO truememory_maintenance_schema") + conn.execute("RELEASE truememory_maintenance_schema") + # single source of truth for the sqlite busy_timeout pragma. # Pre-fix, create_db used 5000ms and pipeline._set_busy_timeout used @@ -548,6 +950,64 @@ def _migrate_messages_schema(conn: sqlite3.Connection, db_path: str | Path) -> N pass +def _migrate_messages_fts_trigger(conn: sqlite3.Connection) -> None: + """Repair owned FTS triggers and refill legacy contents once, atomically.""" + def available() -> bool: + columns = {row[1].lower() for row in conn.execute("PRAGMA table_xinfo(messages)")} + return set(_MESSAGES_FTS_FIELDS).issubset(columns) and _messages_have_rowid_id(conn) + + if not available(): + log.warning("FTS synchronization repair unavailable for unsupported or incomplete messages schema") + return + definitions = _messages_fts_trigger_definitions(conn) + + def installed() -> dict[str, str]: + placeholders = ",".join("?" for _ in definitions) + return {row[0]: " ".join(row[1].strip().rstrip(";").split()) for row in conn.execute( + f"SELECT name, sql FROM sqlite_master WHERE type='trigger' AND name IN ({placeholders})", + tuple(definitions), + )} + + expected = {name: " ".join(sql.split()) for name, sql in definitions.items()} + if installed() == expected: + return + + owned = not conn.in_transaction + conn.execute("BEGIN IMMEDIATE" if owned else "SAVEPOINT truememory_fts_trigger") + completed = False + try: + # Another opener can complete this migration while BEGIN waits. + if not owned: + conn.execute("UPDATE messages SET id=id WHERE 0") + if not available(): + log.warning("FTS synchronization repair unavailable for unsupported or incomplete messages schema") + return + definitions = _messages_fts_trigger_definitions(conn) + expected = {name: " ".join(sql.split()) for name, sql in definitions.items()} + current = installed() + if current != expected: + for name, definition in definitions.items(): + if current.get(name) != expected[name]: + conn.execute(f"DROP TRIGGER IF EXISTS {name}") + conn.execute(definition) + # This FTS table owns its content. Its 'rebuild' command would + # rebuild the stale copy rather than restore current messages. + conn.execute("DELETE FROM messages_fts") + conn.execute( + "INSERT INTO messages_fts(rowid, content, sender, recipient, category, modality) " + "SELECT id, content, sender, recipient, category, modality FROM messages" + ) + conn.execute("COMMIT" if owned else "RELEASE truememory_fts_trigger") + completed = True + finally: + if not completed and conn.in_transaction: + if owned: + conn.rollback() + else: + conn.execute("ROLLBACK TO truememory_fts_trigger") + conn.execute("RELEASE truememory_fts_trigger") + + def create_db(db_path: str | Path) -> sqlite3.Connection: """ Create (or open) a TrueMemory database with the full schema. @@ -640,6 +1100,14 @@ def create_db(db_path: str | Path) -> sqlite3.Connection: _migrate_messages_schema(conn, db_path) conn.executescript(_SCHEMA_SQL) + try: + _migrate_messages_fts_trigger(conn) + _initialize_maintenance_tracking(conn) + _initialize_dunbar_ownership(conn) + _initialize_style_accumulator_schema(conn) + except sqlite3.Error: + conn.close() + raise # The directive index lives outside _SCHEMA_SQL on purpose: if a legacy # DB's migration could not add messages.directive (e.g. a rolled-back # ALTER), an unconditional CREATE INDEX inside executescript would turn a @@ -1047,7 +1515,7 @@ def insert_message(conn: sqlite3.Connection, msg: dict) -> int: return cursor.lastrowid -def delete_message(conn: sqlite3.Connection, msg_id: int) -> bool: +def delete_message(conn: sqlite3.Connection, msg_id: int, *, commit: bool = True) -> bool: """ Delete a single message and its vector embedding. @@ -1055,6 +1523,8 @@ def delete_message(conn: sqlite3.Connection, msg_id: int) -> bool: The vector embedding in ``vec_messages`` is also removed if the table exists. + ``commit=False`` leaves publication inside the caller's writer transaction. + Args: conn: Open database connection. msg_id: The message ID to delete. @@ -1129,7 +1599,8 @@ def delete_message(conn: sqlite3.Connection, msg_id: int) -> bool: for _ent in _entities: for _tbl in ("summaries", "entity_profiles", "entity_style_vectors"): try: - conn.execute(f"DELETE FROM {_tbl} WHERE entity = ?", (_ent,)) + conn.execute(f"DELETE FROM {_tbl} WHERE entity = ?", + (_ent.lower() if _tbl == "entity_style_vectors" else _ent,)) except sqlite3.OperationalError: pass try: @@ -1143,11 +1614,12 @@ def delete_message(conn: sqlite3.Connection, msg_id: int) -> bool: cursor = conn.execute("DELETE FROM messages WHERE id = ?", (msg_id,)) deleted = cursor.rowcount > 0 - conn.commit() + if commit: + conn.commit() return deleted -def update_message(conn: sqlite3.Connection, msg_id: int, **fields) -> bool: +def update_message(conn: sqlite3.Connection, msg_id: int, *, commit: bool = True, **fields) -> bool: """ Update fields on an existing message. @@ -1158,6 +1630,8 @@ def update_message(conn: sqlite3.Connection, msg_id: int, **fields) -> bool: The AFTER UPDATE trigger on ``messages`` automatically keeps the FTS5 index in sync. + ``commit=False`` leaves the source update inside the caller's writer fence. + Args: conn: Open database connection. msg_id: The message ID to update. @@ -1178,5 +1652,6 @@ def update_message(conn: sqlite3.Connection, msg_id: int, **fields) -> bool: cursor = conn.execute(f"UPDATE messages SET {set_clause} WHERE id = ?", values) # The AFTER UPDATE trigger on messages keeps FTS5 in sync automatically. - conn.commit() + if commit: + conn.commit() return cursor.rowcount > 0 diff --git a/truememory/telemetry.py b/truememory/telemetry.py index fb7dfe39..bd34858b 100644 --- a/truememory/telemetry.py +++ b/truememory/telemetry.py @@ -1,20 +1,25 @@ """ Fire-and-forget usage telemetry for TrueMemory. -Disabled via TRUEMEMORY_TELEMETRY=off or {"telemetry": false} in config. -Never blocks, never crashes, never slows down the user. All HTTP calls -have a 3-second timeout. If the endpoint is unreachable, events are -silently dropped. - -What is tracked: - - Tool call counts and latencies (which MCP tools are used) - - Session start/end events - - Tier, version, platform - - Email + UUID (on first registration) - -What is NEVER tracked: +Disabled at startup via TRUEMEMORY_TELEMETRY=off or {"telemetry": false} +in config. Events are queued and flushed in background threads; callers +can also flush synchronously. HTTP calls use a configured 3-second timeout. +Failed batches are dropped. Startup device discovery is synchronous. + +Built-in events are enabled by default and are not anonymous: + - Every event: name, persistent user UUID, timestamp + - Session start: tier, version, platform, architecture, Python version, + stable hashed device ID, and configured email on every session start + - Tool calls: tool name, latency, success or failure + - Registration: email and tier; tier changes: new and previous tier + +The UUID persists in config. The device ID is a truncated SHA-256 hash of +the OS machine identifier and stays stable while that identifier does. + +Built-in events exclude: - Query content or search terms - Memory content or stored facts + - Tool arguments, results, and exception text - File paths, API keys, or credentials """ @@ -58,7 +63,7 @@ def get_device_id() -> str: - """Return a SHA256-hashed machine ID for privacy-safe device counting.""" + """Return a stable, pseudonymous machine-ID hash for device counting.""" global _device_id_cache if _device_id_cache is not None: return _device_id_cache @@ -123,7 +128,7 @@ def init(config: dict) -> dict | None: config["user_id"] = _user_id _save_user_id(config) - # Track session start and do a synchronous flush to check for updates + # Queue session fields, then flush in a background thread for update checks. session_props = { "tier": config.get("tier", "edge"), "version": _get_version(), @@ -201,7 +206,7 @@ def track(event: str, properties: dict | None = None) -> None: def identify(email: str, properties: dict | None = None) -> None: - """Register user identity (first-run only).""" + """Emit an identity event when a valid email is configured.""" if not _enabled: return if not _is_valid_email(email): diff --git a/truememory/temporal.py b/truememory/temporal.py index b6d25672..d0413a17 100644 --- a/truememory/temporal.py +++ b/truememory/temporal.py @@ -25,6 +25,9 @@ import re import sqlite3 +import json +from collections.abc import Iterator +from contextlib import contextmanager from datetime import datetime, timedelta from truememory.storage import _row_to_dict, select_message_cols @@ -509,6 +512,248 @@ def detect_temporal_intent(query: str) -> dict: # Temporal search and timeline # --------------------------------------------------------------------------- +_LANDMARK_ROW_LIMIT = 1024 +_LANDMARK_CANDIDATE_LIMIT = 16 +_SOURCE_INTERVAL_ROW_LIMIT = 1024 + + +def _has_bounded_primary_key(conn: sqlite3.Connection, table: str) -> bool: + if table not in {"messages", "landmark_events"}: + return False + columns = conn.execute(f"PRAGMA table_info({table})").fetchall() + keys = [column for column in columns if column[5]] + return len(keys) == 1 and keys[0][1] == "id" and keys[0][2].upper() == "INTEGER" + + +def _source_interval_candidates(conn: sqlite3.Connection) -> tuple[list[dict], dict | None]: + """Inspect a bounded raw-PK batch before selecting provenance rows.""" + try: + if not _has_bounded_primary_key(conn, "messages"): + return [], {"status": "incomplete", "reason": "source_interval_lookup_unavailable", + "candidate_limit": _SOURCE_INTERVAL_ROW_LIMIT, + "visited_row_limit": _SOURCE_INTERVAL_ROW_LIMIT + 1} + rows = conn.execute( + "SELECT id,substr(metadata,1,65537),length(metadata) FROM messages " + "ORDER BY id LIMIT ?", (_SOURCE_INTERVAL_ROW_LIMIT + 1,), + ).fetchall() + except sqlite3.Error: + return [], {"status": "incomplete", "reason": "source_interval_lookup_unavailable", + "candidate_limit": _SOURCE_INTERVAL_ROW_LIMIT, + "visited_row_limit": _SOURCE_INTERVAL_ROW_LIMIT + 1} + diagnostic = None + if len(rows) > _SOURCE_INTERVAL_ROW_LIMIT: + diagnostic = {"status": "incomplete", "reason": "source_interval_inventory_limit", + "candidate_limit": _SOURCE_INTERVAL_ROW_LIMIT, + "visited_row_limit": _SOURCE_INTERVAL_ROW_LIMIT + 1} + candidates = [] + for message_id, raw, length in rows[:_SOURCE_INTERVAL_ROW_LIMIT]: + if (length or 0) > 65536: + diagnostic = diagnostic or {"status": "incomplete", "reason": "source_metadata_limit", + "candidate_limit": _SOURCE_INTERVAL_ROW_LIMIT, + "visited_row_limit": _SOURCE_INTERVAL_ROW_LIMIT + 1} + continue + try: + metadata = json.loads(raw) if isinstance(raw, str) else None + except (ValueError, TypeError): + continue + if not isinstance(metadata, dict): + continue + namespace = metadata.get("truememory_ingest") + if isinstance(namespace, dict) and type(namespace.get("version")) is int and namespace["version"] == 1: + candidates.append({"id": message_id, "metadata": metadata}) + return candidates, diagnostic + + +def _source_intervals(metadata: object) -> tuple[list[tuple[str, str]], bool] | None: + if not isinstance(metadata, dict) or "truememory_ingest" not in metadata: + return None + try: + from truememory.ingest.provenance import source_intervals + except ImportError: + return [], True + return source_intervals(metadata) + + +def _temporal_row_matches(row: dict, after: str | None, before_excl: str | None) -> bool: + if not after and not before_excl: + return True + intervals = _source_intervals(row.get("metadata")) + if intervals is not None: + return any( + (not after or end >= after) and (not before_excl or start < before_excl) + for start, end in intervals[0] + ) + timestamp = row.get("timestamp") or "" + return bool(timestamp) and (not after or timestamp >= after) and ( + not before_excl or timestamp < before_excl + ) + + +def _landmark_date(timestamp: object, metadata: object) -> str | None: + intervals = _source_intervals(metadata) + if intervals is not None: + # An extraction-input interval is not an exact semantic event anchor, + # even if the entire input happened on one calendar day. + return None + if not isinstance(timestamp, str) or not timestamp or len(timestamp) > 64: + return None + if len(timestamp) < 10 or not _ISO_DATE_RE.fullmatch(timestamp[:10]): + return None + try: + normalized = timestamp[:-1] + "+00:00" if timestamp.endswith("Z") else timestamp + parsed = datetime.fromisoformat(normalized) + except ValueError: + return None + # Use the encoded source calendar, never the querying machine's time zone. + return parsed.date().isoformat() + + +def _landmark_phrase(text: str) -> tuple[str, str | None]: + phrase = text.strip().rstrip("?. ") + quoted = len(phrase) >= 2 and phrase[0] == phrase[-1] and phrase[0] in {"'", '"'} + if quoted: + phrase = phrase[1:-1].strip() + qualifier = re.fullmatch(r"(.+?)['\u2019]s\s+(.+)", phrase) if not quoted else None + entity = qualifier.group(1).strip() if qualifier else None + phrase = qualifier.group(2).strip() if qualifier else phrase + return " ".join(phrase.casefold().split()), entity + + +def resolve_temporal_intent( + conn: sqlite3.Connection, query: str, sender: str | None = None, +) -> dict: + """Resolve one literal stored landmark without guessing an event date. + + Stored context is heuristic evidence. The returned citation identifies its + live source; it does not certify the date of the event described there. + Unresolved/ambiguous diagnostics preserve the original parser's window. + """ + intent = detect_temporal_intent(query) + clauses = list(re.finditer( + r"\b(after|before)\s+(.+?)(?=\s+(?:and\s+)?(?:after|before)\b|\?|$)", + query, re.IGNORECASE, + )) + if not clauses or intent.get("after") or intent.get("before") or parse_date_reference(query): + return intent + diagnostic = {"status": "unresolved", "reason": "no_matching_event", "candidates": []} + intent["landmark_resolution"] = diagnostic + if len(clauses) != 1: + diagnostic["reason"] = "multiple_named_anchors" + return intent + clause = clauses[0] + phrase, entity = _landmark_phrase(clause.group(2)) + diagnostic["phrase"] = phrase + if not phrase or len(phrase) > 256 or (entity and len(entity) > 128): + diagnostic["reason"] = "unsupported_phrase" + return intent + try: + if not _has_bounded_primary_key(conn, "landmark_events") or not _has_bounded_primary_key(conn, "messages"): + diagnostic["reason"] = "landmark_schema_unavailable" + return intent + rows = conn.execute( + "SELECT id, substr(event_name,1,513), length(event_name), timestamp, " + "substr(related_entities,1,4097), source_message_id " + "FROM landmark_events ORDER BY id LIMIT ?", (_LANDMARK_ROW_LIMIT + 1,), + ).fetchall() + except sqlite3.Error: + diagnostic["reason"] = "landmark_schema_unavailable" + return intent + if len(rows) > _LANDMARK_ROW_LIMIT: + diagnostic["reason"] = "landmark_inventory_limit" + return intent + pattern = re.compile(r"(? 65536 or (content_length or 0) > 4096 or not isinstance(content, str) or not pattern.search( + " ".join(content.casefold().split()) + ): + diagnostic["reason"] = "source_content_unresolved" + return intent + try: + related = json.loads(related_json) if isinstance(related_json, str) else None + source_metadata = json.loads(metadata) if isinstance(metadata, str) else metadata + except (ValueError, TypeError): + diagnostic["reason"] = "source_metadata_invalid" + return intent + if not isinstance(source_metadata, dict): + diagnostic["reason"] = "source_metadata_invalid" + return intent + if not isinstance(related, list) or not all(isinstance(value, str) for value in related): + diagnostic["reason"] = "related_entities_invalid" + return intent + if entity and entity.casefold() not in { + str(value or "").casefold() for value in [source_sender, recipient, *related] + }: + continue + date = _landmark_date(source_time, source_metadata) + if name_length > 512 or timestamp != source_time or date is None: + diagnostic["reason"] = "source_date_unresolved" + return intent + candidate = { + "landmark_id": landmark_id, "event_name": name, "source_message_id": source_id, + "timestamp": source_time, "date": date, "sender": source_sender, + "related_entities": related[:10], + "timestamp_basis": "source_input_scope" if _source_intervals(source_metadata) is not None + else "source_message_timestamp", + } + previous = candidates.get(source_id) + if previous and previous["date"] != date: + diagnostic["reason"] = "conflicting_source_dates" + return intent + candidates.setdefault(source_id, candidate) + diagnostic["candidates"] = list(candidates.values())[:_LANDMARK_CANDIDATE_LIMIT] + if len(candidates) != 1: + if candidates: + diagnostic.update(status="ambiguous", reason="multiple_sources", + candidates_truncated=len(candidates) > _LANDMARK_CANDIDATE_LIMIT) + return intent + candidate = next(iter(candidates.values())) + # Replay the unchanged parser so its 31/7/365-day windows remain identical. + anchored_query = query[:clause.start(2)] + candidate["date"] + query[clause.end(2):] + try: + resolved = detect_temporal_intent(anchored_query) + except (ValueError, OverflowError): + diagnostic["reason"] = "unsupported_date_window" + return intent + diagnostic.update(status="resolved", reason="unique_live_source") + resolved["landmark_resolution"] = diagnostic + return resolved + + +def attach_temporal_resolution(results: list[dict], intent: dict) -> list[dict]: + diagnostic = intent.get("landmark_resolution") + coverage = intent.get("source_interval_coverage") + if diagnostic is None and coverage is None: + return results + additions = {} + if diagnostic is not None: + additions["temporal_resolution"] = diagnostic + if coverage is not None: + additions["temporal_scope_coverage"] = coverage + return [dict(row, **additions) for row in results] + def search_temporal( conn: sqlite3.Connection, query: str, @@ -516,6 +761,8 @@ def search_temporal( hybrid_results: list[dict] | None = None, limit: int = 10, include_directives: bool = False, + landmark_sender: str | None = None, + resolved_intent: dict | None = None, ) -> list[dict]: """ Apply temporal filtering and re-ranking to search results. @@ -543,7 +790,7 @@ def search_temporal( Returns: List of message dicts, temporally filtered and ordered. """ - intent = detect_temporal_intent(query) + intent = resolved_intent if resolved_intent is not None else resolve_temporal_intent(conn, query, landmark_sender) # Merge input result sets (prefer hybrid if both are provided) results = [] @@ -553,7 +800,7 @@ def search_temporal( results = list(fts_results) if not intent["has_temporal"]: - return results[:limit] + return attach_temporal_resolution(results[:limit], intent) after = _validate_iso_date(intent["after"]) before = _validate_iso_date(intent["before"]) @@ -564,14 +811,8 @@ def search_temporal( before_excl = _exclusive_upper_bound(before) if before else None filtered = [] for r in results: - ts = r.get("timestamp", "") - if not ts: - continue - if after and ts < after: - continue - if before_excl and ts >= before_excl: - continue - filtered.append(r) + if _temporal_row_matches(r, after, before_excl): + filtered.append(r) results = filtered # --- If trajectory, sort chronologically --- @@ -583,6 +824,10 @@ def search_temporal( existing_ids = {r.get("id") for r in results} extra = get_timeline(conn, after=after, before=before, include_directives=include_directives) + for row in extra: + if row.get("temporal_scope_coverage"): + intent["source_interval_coverage"] = row["temporal_scope_coverage"] + break for msg in extra: if msg["id"] not in existing_ids: results.append(msg) @@ -592,7 +837,7 @@ def search_temporal( if intent["is_trajectory"] or intent["sort_by_time"]: results.sort(key=lambda r: r.get("timestamp", "")) - return results[:limit] + return attach_temporal_resolution(results[:limit], intent) def get_timeline( @@ -653,7 +898,34 @@ def get_timeline( sql = f"SELECT {select_message_cols(conn)} FROM messages{where} ORDER BY timestamp" rows = conn.execute(sql, params).fetchall() - return [_row_to_dict(r) for r in rows] + results = [_row_to_dict(r) for r in rows] + if after or before: + before_excl = _exclusive_upper_bound(before) if before else None + # Only recognized v1 provenance needs a second lookup: its scalar end + # can lie outside a window intersecting one of its individual scopes. + scope_rows, diagnostic = _source_interval_candidates(conn) + present = {row.get("id") for row in results} + columns = select_message_cols(conn) + for candidate in scope_rows: + if candidate["id"] in present or not _temporal_row_matches(candidate, after, before_excl): + continue + row = conn.execute(f"SELECT {columns} FROM messages WHERE id = ?", (candidate["id"],)).fetchone() + if row is None: + continue + message = _row_to_dict(row) + if entity is not None and entity.lower() not in { + (message.get("sender") or "").lower(), (message.get("recipient") or "").lower(), + }: + continue + if not include_directives and message.get("directive"): + continue + results.append(message) + present.add(message.get("id")) + results = [row for row in results if _temporal_row_matches(row, after, before_excl)] + results.sort(key=lambda row: row.get("timestamp", "")) + if diagnostic: + results = [dict(row, temporal_scope_coverage=diagnostic) for row in results] + return results # --------------------------------------------------------------------------- @@ -673,38 +945,17 @@ def _parse_naive(ts: str) -> datetime: return datetime.fromisoformat(stripped) -def detect_episodes(conn, gap_hours=6): - """ - Group messages into episodes using a time-gap heuristic. - Messages within gap_hours of each other belong to the same episode. - Stores episodes in the episodes table and updates episode_id on messages. - - Args: - conn: Open database connection. - gap_hours: Maximum gap (in hours) between consecutive messages - within the same episode (default 6). - - Returns: - Number of episodes detected. - """ - rows = conn.execute( - "SELECT id, timestamp FROM messages WHERE timestamp != '' ORDER BY timestamp" - ).fetchall() - +def _group_episode_rows(rows: list[tuple], gap_hours: float) -> list[list[tuple]]: + """Keep the existing lexical ordering and naive timestamp gap semantics.""" if not rows: - return 0 - - # Clear existing episodes - conn.execute("DELETE FROM episodes") - conn.execute("UPDATE messages SET episode_id = NULL") - + return [] episodes = [] current_episode_msgs = [rows[0]] gap_delta = timedelta(hours=gap_hours) for i in range(1, len(rows)): - msg_id, ts = rows[i] - prev_id, prev_ts = rows[i - 1] + ts = rows[i][1] + prev_ts = rows[i - 1][1] try: curr_dt = _parse_naive(ts) @@ -713,37 +964,120 @@ def detect_episodes(conn, gap_hours=6): if (curr_dt - prev_dt) > gap_delta: # New episode episodes.append(current_episode_msgs) - current_episode_msgs = [(msg_id, ts)] + current_episode_msgs = [rows[i]] else: - current_episode_msgs.append((msg_id, ts)) + current_episode_msgs.append(rows[i]) except (ValueError, TypeError): - current_episode_msgs.append((msg_id, ts)) + current_episode_msgs.append(rows[i]) # Don't forget the last episode if current_episode_msgs: episodes.append(current_episode_msgs) + return episodes + + +@contextmanager +def _episode_transaction(conn: sqlite3.Connection) -> Iterator[None]: + owned = not conn.in_transaction + conn.execute("BEGIN" if owned else "SAVEPOINT truememory_episodes") + completed = False + try: + yield + conn.execute("COMMIT" if owned else "RELEASE truememory_episodes") + completed = True + finally: + if not completed and conn.in_transaction: + if owned: + conn.rollback() + else: + conn.execute("ROLLBACK TO truememory_episodes") + conn.execute("RELEASE truememory_episodes") - # Store episodes and update messages - for ep_msgs in episodes: - if not ep_msgs: - continue +def _reconcile_episodes(conn: sqlite3.Connection, gap_hours: float) -> int: + rows = conn.execute( + "SELECT id, timestamp, episode_id FROM messages WHERE timestamp != '' ORDER BY timestamp" + ).fetchall() + assigned = conn.execute( + "SELECT id, episode_id FROM messages WHERE episode_id IS NOT NULL" + ).fetchall() + previous = conn.execute( + "SELECT id, start_time, end_time, message_count, summary FROM episodes" + ).fetchall() + groups = _group_episode_rows(rows, gap_hours) + members: dict[int, set[int]] = {} + for mid, episode_id in assigned: + members.setdefault(episode_id, set()).add(mid) + matching = { + frozenset(members[episode[0]]): episode + for episode in previous + if episode[0] in members and len(members[episode[0]]) == episode[3] + } + + kept: set[int] = set() + eligible: set[int] = set() + assignments = [] + for ep_msgs in groups: + member_ids = frozenset(message[0] for message in ep_msgs) + eligible.update(member_ids) start_time = ep_msgs[0][1] end_time = ep_msgs[-1][1] msg_count = len(ep_msgs) + old = matching.get(member_ids) + if old is None: + cursor = conn.execute( + "INSERT INTO episodes (start_time, end_time, message_count) VALUES (?, ?, ?)", + (start_time, end_time, msg_count), + ) + episode_id = cursor.lastrowid + else: + episode_id = old[0] + # Membership alone cannot prove a summary's source text unchanged. + # Preserve the old detector's invalidation without rewriting empties. + if tuple(old[1:4]) != (start_time, end_time, msg_count) or old[4] not in ("", None): + conn.execute( + "UPDATE episodes SET start_time = ?, end_time = ?, message_count = ?, summary = '' WHERE id = ?", + (start_time, end_time, msg_count, episode_id), + ) + kept.add(episode_id) + assignments.extend((episode_id, message[0], episode_id) + for message in ep_msgs if message[2] != episode_id) + + assignments.extend((None, mid, None) for mid, _ in assigned if mid not in eligible) + conn.executemany( + "UPDATE messages SET episode_id = ? WHERE id = ? AND episode_id IS NOT ?", + assignments, + ) + conn.executemany("DELETE FROM episodes WHERE id = ?", + ((episode[0],) for episode in previous if episode[0] not in kept)) + return len(groups) - cursor = conn.execute( - "INSERT INTO episodes (start_time, end_time, message_count) VALUES (?, ?, ?)", - (start_time, end_time, msg_count) - ) - episode_id = cursor.lastrowid - msg_ids = [m[0] for m in ep_msgs] - for mid in msg_ids: - conn.execute("UPDATE messages SET episode_id = ? WHERE id = ?", (episode_id, mid)) +def detect_episodes(conn: sqlite3.Connection, gap_hours: float = 6) -> int: + """Reconcile six-hour groups atomically, preserving unchanged episode IDs. - conn.commit() - return len(episodes) + Exact member sets keep their IDs. Merges, splits and membership changes + get new IDs; nonempty derived summaries are invalidated. Only changed rows + are written. A caller transaction remains open and owns the final commit. + """ + owned = not conn.in_transaction + for attempt in range(3): + version = conn.execute("PRAGMA data_version").fetchone()[0] if owned else None + try: + with _episode_transaction(conn): + return _reconcile_episodes(conn, gap_hours) + except sqlite3.OperationalError as error: + # WAL readers cannot upgrade a snapshot after another writer + # commits. Retry only our own transaction, at most twice; never + # restart a caller's snapshot or commit its unrelated writes. + code = getattr(error, "sqlite_errorcode", None) + stale = code == 517 # SQLITE_BUSY_SNAPSHOT + if code is None and str(error) == "database is locked" and owned: + # Python 3.10 does not expose extended SQLite error codes. + stale = conn.execute("PRAGMA data_version").fetchone()[0] != version + if not owned or not stale or attempt == 2: + raise + raise AssertionError("Episode snapshot retry limit was not enforced") def get_episode_messages(conn, episode_id): @@ -827,12 +1161,11 @@ def detect_landmark_events(conn): """ import json - conn.execute("DELETE FROM landmark_events") - - rows = conn.execute( + source_sql = ( "SELECT id, content, sender, recipient, timestamp FROM messages " "WHERE timestamp != '' ORDER BY timestamp" - ).fetchall() + ) + rows = conn.execute(source_sql).fetchall() # Landmark event patterns event_patterns = { @@ -866,7 +1199,7 @@ def detect_landmark_events(conn): ), } - count = 0 + events = [] for msg_id, content, sender, recipient, timestamp in rows: for event_type, pattern in event_patterns.items(): match = pattern.search(content) @@ -893,14 +1226,29 @@ def detect_landmark_events(conn): if noun not in common and noun.lower() not in [r.lower() for r in related]: related.append(noun) - conn.execute( - "INSERT INTO landmark_events " - "(event_name, timestamp, event_type, related_entities, source_message_id) " - "VALUES (?, ?, ?, ?, ?)", + events.append( (event_context, timestamp, event_type, json.dumps(related[:10]), msg_id) ) - count += 1 break # One event per message is enough - conn.commit() - return count + conn.execute("SAVEPOINT truememory_landmarks") + completed = False + try: + # Own the writer before validating the source used for computation. + conn.execute("DELETE FROM landmark_events WHERE 0") + if conn.execute(source_sql).fetchall() != rows: + raise sqlite3.OperationalError("Landmark source changed during computation; retry the operation") + conn.execute("DELETE FROM landmark_events") + conn.executemany( + "INSERT INTO landmark_events " + "(event_name, timestamp, event_type, related_entities, source_message_id) " + "VALUES (?, ?, ?, ?, ?)", + events, + ) + conn.execute("RELEASE truememory_landmarks") + completed = True + finally: + if not completed and conn.in_transaction: + conn.execute("ROLLBACK TO truememory_landmarks") + conn.execute("RELEASE truememory_landmarks") + return len(events) diff --git a/truememory/tier_switch/activation.py b/truememory/tier_switch/activation.py new file mode 100644 index 00000000..33fc834a --- /dev/null +++ b/truememory/tier_switch/activation.py @@ -0,0 +1,638 @@ +"""Database-only certification of an explicitly selected, inactive tier pair. + +Callers own maintenance, coherent old serving state, inactive target admission, +and the drained local serving boundary. None is inferred from a database row. +This module never applies config/runtime state or certifies legacy vectors. +""" + +from __future__ import annotations + +import hashlib +import json +import re +import sqlite3 +import uuid +from collections.abc import Iterator +from contextlib import contextmanager +from dataclasses import asdict, dataclass, replace + +from truememory.embedding_target import EmbeddingTarget +from truememory.tier_switch.cache import VectorCacheRegistry +from truememory.tier_switch.job import ( + TierJob, _retire_selected_tier_job_in_writer, check_selected_tier_job_in_writer, +) +from truememory.tier_switch.source import TierSourcePlan, require_current_completion_in_writer + +_SELECTED = "tier_selected_v1" +_INTENT = "tier_activation_v1" +_MAX_RECORD_BYTES = 16384 +_MAX_SCHEMA_BYTES = 65536 +_TRACKERS = {"canonical-v1", "rebuild-bridge-v1", "rebuild-bridge-conservative-v1"} +_METADATA_SQL = "CREATE TABLE metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TEXT)" +_REGISTRY_SQL = """CREATE TABLE vector_cache_registry ( + tier_group TEXT PRIMARY KEY, vec_table TEXT NOT NULL, sep_table TEXT NOT NULL, + last_embedded_id INTEGER DEFAULT 0, vector_count INTEGER DEFAULT 0, + model_name TEXT, embedding_dim INTEGER DEFAULT 256, last_updated REAL, created REAL +)""" + + +class TierActivationError(RuntimeError): + """Selection cannot be certified; no runtime/config success is implied.""" + + +@dataclass(frozen=True) +class ConfigGuard: + tier_present: bool + tier: str + generation: str | None + + def __post_init__(self) -> None: + if (type(self.tier_present) is not bool or type(self.tier) is not str + or self.tier not in {"edge", "base", "pro", "custom"} + or (not self.tier_present and self.tier != "edge") + or (self.generation is not None and not _hex(self.generation, 32))): + raise TierActivationError("Invalid expected config identity") + + +@dataclass(frozen=True) +class LegacyTierPolicy: + generation: str + intent_id: str + target: EmbeddingTarget + reranker_id: str + tables: tuple[str, str] + config_acknowledged: bool = False + + def __post_init__(self) -> None: + _policy_identity(self.target, self.reranker_id, self.tables) + if (not _hex(self.generation, 32) or not _hex(self.intent_id, 32) + or type(self.config_acknowledged) is not bool): + raise TierActivationError("Invalid legacy serving policy") + + +def _policy_identity(target: EmbeddingTarget, reranker_id: str, tables: tuple[str, str]) -> None: + if (type(target) is not EmbeddingTarget or target.tier not in {"base", "pro"} + or target.identity != ("qwen3_256", 256, "basepro") + or type(reranker_id) is not str or reranker_id != "Alibaba-NLP/gte-reranker-modernbert-base" + or type(tables) is not tuple or len(tables) != 2 or any(type(name) is not str for name in tables) + or tables not in {target.tables, ("vec_messages", "vec_messages_sep")}): + raise TierActivationError("Policy-only publication requires the unchanged base/pro embedding space") + + +def _hex(value: object, size: int) -> bool: + return type(value) is str and re.fullmatch(r"[0-9a-f]{" + str(size) + "}", value) is not None + + +def _text(value: object, limit: int) -> bool: + if type(value) is not str or not 1 <= len(value) <= limit or "\x00" in value: + return False + try: + return len(value.encode("utf-8")) <= limit * 4 + except UnicodeError: + return False + + +def _identity(target: EmbeddingTarget, reranker_id: str, job_id: str, + tracker: str, source_epoch: str, source_schema_signature: str) -> None: + if (type(target) is not EmbeddingTarget or not _text(target.model_id, 512) + or any(type(value) is not str for value in (target.tier, target.tier_group)) + or not _text(reranker_id, 512) + or re.fullmatch(r"[\w][\w.\-]*(/[\w][\w.\-]*)?", reranker_id) is None + or not _hex(job_id, 32) or type(tracker) is not str or tracker not in _TRACKERS + or not _text(source_epoch, 128) or not _hex(source_schema_signature, 64)): + raise TierActivationError("Invalid frozen activation identity") + EmbeddingTarget.from_wire(target.to_wire()) + + +@dataclass(frozen=True) +class ActivationIntent: + intent_id: str + expected_generation: str | None + target: EmbeddingTarget + reranker_id: str + job_id: str + tracker: str + source_epoch: str + source_schema_signature: str + state: str = "staged" + expected_config: ConfigGuard | None = None + status_id: int | None = None + previous_policy: LegacyTierPolicy | None = None + + def __post_init__(self) -> None: + _identity(self.target, self.reranker_id, self.job_id, self.tracker, + self.source_epoch, self.source_schema_signature) + if (not _hex(self.intent_id, 32) + or (self.expected_generation is not None and not _hex(self.expected_generation, 32)) + or type(self.state) is not str or self.state not in {"staged", "db_selected", "config_acknowledged"}): + raise TierActivationError("Invalid activation intent") + if (self.expected_config is not None and type(self.expected_config) is not ConfigGuard + or self.status_id is not None and (type(self.status_id) is not int or not 1 <= self.status_id < 2**63) + or self.previous_policy is not None and type(self.previous_policy) is not LegacyTierPolicy + or self.expected_config is None and (self.status_id is not None or self.previous_policy is not None) + or self.previous_policy is not None and self.expected_generation is not None): + raise TierActivationError("Invalid activation projection evidence") + + +@dataclass(frozen=True) +class PolicyIntent: + intent_id: str + expected_generation: str | None + expected_intent_id: str | None + target: EmbeddingTarget + reranker_id: str + expected_config: ConfigGuard + result_generation: str + tables: tuple[str, str] + state: str = "db_selected" + + def __post_init__(self) -> None: + _policy_identity(self.target, self.reranker_id, self.tables) + if (not _hex(self.intent_id, 32) or not _hex(self.result_generation, 32) + or self.expected_generation is not None and not _hex(self.expected_generation, 32) + or self.expected_intent_id is not None and not _hex(self.expected_intent_id, 32) + or self.result_generation == self.expected_generation + or self.intent_id == self.expected_intent_id + or type(self.expected_config) is not ConfigGuard + or self.expected_config.tier not in {"base", "pro"} + or type(self.state) is not str or self.state not in {"db_selected", "config_acknowledged"}): + raise TierActivationError("Invalid policy activation intent") + + +@dataclass(frozen=True) +class TierSelection: + generation: str + intent_id: str + target: EmbeddingTarget + reranker_id: str + tables: tuple[str, str] + job_id: str + tracker: str + source_epoch: str + source_schema_signature: str + manifest_generation: str + manifest_hash: str + receipt_hash: str + pair_digest: str + schema: str + vector_count: int + cursor: int | None + config_acknowledged: bool = False + + def __post_init__(self) -> None: + _identity(self.target, self.reranker_id, self.job_id, self.tracker, + self.source_epoch, self.source_schema_signature) + if (any(not _hex(value, 32) for value in (self.generation, self.intent_id, self.manifest_generation)) + or any(not _hex(value, 64) for value in (self.manifest_hash, self.receipt_hash, self.pair_digest, self.schema)) + or type(self.tables) is not tuple or self.tables != self.target.tables + or type(self.vector_count) is not int or not 0 <= self.vector_count <= 2**63 - 1 + or (self.cursor is not None and (type(self.cursor) is not int or not -(2**63) <= self.cursor < 2**63)) + or ((self.vector_count == 0) != (self.cursor is None)) + or type(self.config_acknowledged) is not bool): + raise TierActivationError("Invalid certified selection") + + +@dataclass(frozen=True) +class ActivationState: + selection: TierSelection | None + intent: ActivationIntent | PolicyIntent | None + legacy_policy: LegacyTierPolicy | None = None + + +def _config_wire(value: ConfigGuard) -> dict: + return dict(asdict(value), version=1) + + +def _parse_config(value: object) -> ConfigGuard: + if (type(value) is not dict or set(value) != {"version", "tier_present", "tier", "generation"} + or type(value["version"]) is not int or value["version"] != 1): + raise TierActivationError("Unsupported expected config descriptor") + return ConfigGuard(value["tier_present"], value["tier"], value["generation"]) + + +def _policy_wire(value: LegacyTierPolicy) -> dict: + return dict(asdict(value), version=1, target=value.target.to_wire()) + + +def _parse_policy(value: object) -> LegacyTierPolicy: + if (type(value) is not dict or set(value) != set(LegacyTierPolicy.__dataclass_fields__) | {"version"} + or type(value["version"]) is not int or value["version"] != 1 + or type(value["tables"]) is not list or len(value["tables"]) != 2): + raise TierActivationError("Unsupported previous legacy policy") + return LegacyTierPolicy(value["generation"], value["intent_id"], EmbeddingTarget.from_wire(value["target"]), + value["reranker_id"], tuple(value["tables"]), value["config_acknowledged"]) + + +def _dump(record: ActivationIntent | TierSelection | PolicyIntent) -> str: + value = asdict(record) + value["target"] = record.target.to_wire() + version = 1 + if type(record) is ActivationIntent: + if record.expected_config is None: + for field in ("expected_config", "status_id", "previous_policy"): + value.pop(field) + else: + version = 2 + value["kind"] = "rebuild" + value["expected_config"] = _config_wire(record.expected_config) + value["previous_policy"] = None if record.previous_policy is None else _policy_wire(record.previous_policy) + elif type(record) is PolicyIntent: + version = 2 + value["kind"] = "policy" + value["expected_config"] = _config_wire(record.expected_config) + raw = json.dumps(dict(value, version=version), sort_keys=True, separators=(",", ":"), allow_nan=False) + if len(raw.encode("utf-8")) > _MAX_RECORD_BYTES: + raise TierActivationError("Activation record exceeds protocol bounds") + return raw + + +def _unique(pairs: list[tuple[str, object]]) -> dict[str, object]: + value = dict(pairs) + if len(value) != len(pairs): + raise TierActivationError("Duplicate activation record field") + return value + + +def _parse(raw: str, kind: type[ActivationIntent] | type[TierSelection]) -> ActivationIntent | TierSelection | PolicyIntent: + try: + value = json.loads(raw, object_pairs_hook=_unique) + if type(value) is not dict or type(value.get("version")) is not int: + raise TierActivationError("Unsupported activation record schema") + version = value.pop("version") + expected = set(kind.__dataclass_fields__) + if kind is ActivationIntent and version == 1: + expected -= {"expected_config", "status_id", "previous_policy"} + elif kind is ActivationIntent and version == 2: + discriminator = value.pop("kind", None) + if discriminator == "policy": + kind = PolicyIntent + elif discriminator != "rebuild": + raise TierActivationError("Unsupported activation intent kind") + expected = set(kind.__dataclass_fields__) + elif version != 1: + raise TierActivationError("Unsupported activation record version") + if set(value) != expected: + raise TierActivationError("Unsupported activation record fields") + if version == 2: + value["expected_config"] = _parse_config(value["expected_config"]) + if kind is ActivationIntent and value["previous_policy"] is not None: + value["previous_policy"] = _parse_policy(value["previous_policy"]) + value["target"] = EmbeddingTarget.from_wire(value["target"]) + if kind in (TierSelection, PolicyIntent): + if type(value["tables"]) is not list or len(value["tables"]) != 2: + raise TierActivationError("Invalid selected table pair") + value["tables"] = tuple(value["tables"]) + return kind(**value) + except (ValueError, TypeError, KeyError, OverflowError, RecursionError) as exc: + raise TierActivationError("Malformed activation record") from exc + + +def _normalized(sql: str) -> str: + return re.sub(r"\s*([(),])\s*", r"\1", " ".join(sql.strip().rstrip(";").split())).lower() + + +def _schema_sql(conn: sqlite3.Connection, name: str) -> str: + shape = conn.execute( + "SELECT type, typeof(sql), length(CAST(sql AS BLOB)) FROM main.sqlite_master " + "WHERE name=? COLLATE NOCASE", (name,), + ).fetchone() + if shape is None or shape[:2] != ("table", "text") or not 1 <= shape[2] <= _MAX_SCHEMA_BYTES: + raise TierActivationError("Initialized bounded table schema is required") + return conn.execute("SELECT sql FROM main.sqlite_master WHERE name=? COLLATE NOCASE", (name,)).fetchone()[0] + + +def _guard_storage(conn: sqlite3.Connection) -> None: + if conn.execute("SELECT 1 FROM temp.sqlite_master LIMIT 1").fetchone() is not None: + raise TierActivationError("TEMP objects make activation storage ambiguous") + if conn.execute("SELECT 1 FROM pragma_database_list WHERE name NOT IN ('main','temp') LIMIT 1").fetchone(): + raise TierActivationError("Activation requires a single MAIN database") + for name, expected in (("metadata", _METADATA_SQL), ("vector_cache_registry", _REGISTRY_SQL)): + if _normalized(_schema_sql(conn, name)) != _normalized(expected): + raise TierActivationError("Unsupported activation storage schema") + if conn.execute( + "SELECT 1 FROM main.sqlite_master WHERE tbl_name=? COLLATE NOCASE " + "AND (type='trigger' OR (type='index' AND sql IS NOT NULL)) LIMIT 1", (name,), + ).fetchone(): + raise TierActivationError("Activation storage has unsupported side effects") + + +def _guard_pair(conn: sqlite3.Connection, target: EmbeddingTarget, *, tables: tuple[str, str] | None = None) -> None: + for name in target.tables if tables is None else tables: + declaration = ( + r"CREATE\s+VIRTUAL\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?" + + r'(?:"' + name + r'"|`' + name + r'`|\[' + name + r'\]|' + name + r')' + + r"\s+USING\s+vec0\s*\(\s*embedding\s+float\[" + str(target.dimension) + + r"\]\s+distance_metric\s*=\s*cosine\s*\)\s*" + ) + if re.fullmatch(declaration, _schema_sql(conn, name), re.IGNORECASE) is None: + raise TierActivationError("Selected pair must have the exact prepared vec0 schema") + + +def _read(conn: sqlite3.Connection, key: str, kind: type[ActivationIntent] | type[TierSelection] + ) -> ActivationIntent | TierSelection | PolicyIntent | None: + shape = conn.execute( + "SELECT typeof(value), length(CAST(value AS BLOB)) FROM main.metadata WHERE key=?", (key,), + ).fetchone() + if shape is None: + return None + if shape[0] != "text" or not 1 <= shape[1] <= _MAX_RECORD_BYTES: + raise TierActivationError("Activation record exceeds its type or byte bound") + raw = conn.execute("SELECT value FROM main.metadata WHERE key=?", (key,)).fetchone()[0] + return _parse(raw, kind) + + +def _put(conn: sqlite3.Connection, key: str, value: str) -> None: + # Preserve the primary key: REPLACE could cascade an incoming foreign key. + conn.execute( + "INSERT INTO main.metadata(key,value) VALUES (?,?) " + "ON CONFLICT(key) DO UPDATE SET value=excluded.value", (key, value), + ) + + +@contextmanager +def _transaction(conn: sqlite3.Connection, *, write: bool) -> Iterator[None]: + if conn.in_transaction: + raise TierActivationError("Activation requires an owned clean connection") + conn.execute("BEGIN IMMEDIATE" if write else "BEGIN") + try: + yield + if write: + conn.commit() + if conn.in_transaction: + raise TierActivationError("Activation transaction did not commit") + except BaseException: + if conn.in_transaction: + conn.rollback() + if conn.in_transaction: + raise TierActivationError("Activation rollback is uncertain") + raise + else: + # Cleanup failure is terminal. Keeping this outside the handler above + # prevents a second rollback after an uncertain first attempt. + if not write: + conn.rollback() + if conn.in_transaction: + raise TierActivationError("Activation snapshot did not close") + + +def _state(conn: sqlite3.Connection) -> ActivationState: + selected, intent = _read(conn, _SELECTED, TierSelection), _read(conn, _INTENT, ActivationIntent) + if selected is not None and intent is None: + raise TierActivationError("Selected record has no activation intent") + if type(intent) is PolicyIntent: + acknowledged = intent.state == "config_acknowledged" + if selected is None: + if intent.expected_generation is not None: + raise TierActivationError("Certified policy lost its selection") + return ActivationState(None, intent, LegacyTierPolicy( + intent.result_generation, intent.intent_id, intent.target, intent.reranker_id, + intent.tables, acknowledged, + )) + if ((selected.generation, selected.intent_id, selected.target, selected.reranker_id, + selected.tables, selected.config_acknowledged) + != (intent.result_generation, intent.intent_id, intent.target, intent.reranker_id, + intent.tables, acknowledged) or intent.expected_generation is None): + raise TierActivationError("Certified policy and selected record disagree") + return ActivationState(selected, intent) + if intent is not None and intent.state == "staged": + _expected(selected, intent.expected_generation) + if selected is not None and not selected.config_acknowledged: + raise TierActivationError("Pending config selection cannot have a new staged intent") + if intent.previous_policy is not None and ( + not intent.previous_policy.config_acknowledged + or intent.expected_config.tier != intent.previous_policy.target.tier + or intent.expected_config.generation != intent.previous_policy.generation + ): + raise TierActivationError("Staged intent has incoherent prior legacy policy evidence") + if intent is not None and intent.state != "staged": + if (selected is None or selected.intent_id != intent.intent_id + or selected.target != intent.target or selected.reranker_id != intent.reranker_id + or (selected.job_id, selected.tracker, selected.source_epoch, selected.source_schema_signature) + != (intent.job_id, intent.tracker, intent.source_epoch, intent.source_schema_signature) + or selected.config_acknowledged != (intent.state == "config_acknowledged")): + raise TierActivationError("Selected record and activation result disagree") + return ActivationState(selected, intent, intent.previous_policy if intent is not None and intent.state == "staged" else None) + + +def read_activation_state(conn: sqlite3.Connection) -> ActivationState: + """Read committed journal state separately after reopening a safe connection. + + This is not config/runtime reconciliation, fresh vector recertification, or + permission to reuse a connection with uncertain rollback state. + """ + with _transaction(conn, write=False): + _guard_storage(conn) + return _state(conn) + + +def _expected(selection: TierSelection | None, generation: str | None) -> None: + if generation is not None and not _hex(generation, 32): + raise TierActivationError("Invalid expected selection generation") + if (selection.generation if selection else None) != generation: + raise TierActivationError("Selected generation changed") + + +def stage_activation_intent(conn: sqlite3.Connection, job: TierJob, *, + expected_generation: str | None, reranker_id: str, + expected_config: ConfigGuard | None = None, + status_id: int | None = None) -> ActivationIntent: + """Commit intent before source planning; identical current intent is idempotent. + + None expects absent selection and permits only a later certified target. It + does not bootstrap/certify the legacy serving pair. A different job can + replace an abandoned staged intent only after selected-job admission has + replaced its marker. Conflicting intent for a live job is refused. + """ + intent = ActivationIntent(uuid.uuid4().hex, expected_generation, job.target, reranker_id, + job.job_id, job.tracker, job.source_epoch, job.source_schema_signature, + expected_config=expected_config, status_id=status_id) + with _transaction(conn, write=True): + _guard_storage(conn) + check_selected_tier_job_in_writer(conn, job) + state = _state(conn) + _expected(state.selection, expected_generation) + if state.selection is not None and not state.selection.config_acknowledged: + raise TierActivationError("Current selection awaits config acknowledgement") + if expected_config is not None and state.selection is not None and ( + expected_config.tier != state.selection.target.tier or expected_config.generation != state.selection.generation + ): + raise TierActivationError("Prior config does not identify the selected generation") + if state.legacy_policy is not None: + if (not state.legacy_policy.config_acknowledged or expected_config is None + or expected_config.tier != state.legacy_policy.target.tier + or expected_config.generation != state.legacy_policy.generation): + raise TierActivationError("Legacy policy requires acknowledged, guarded activation") + intent = replace(intent, previous_policy=state.legacy_policy) + if type(state.intent) is ActivationIntent and state.intent.job_id == job.job_id: + if state.intent == replace(intent, intent_id=state.intent.intent_id): + return state.intent + raise TierActivationError("A conflicting intent already owns this live job") + _put(conn, _INTENT, _dump(intent)) + return intent + + +def commit_certified_selection(conn: sqlite3.Connection, intent: ActivationIntent, + job: TierJob, plan: TierSourcePlan) -> TierSelection: + """Certify and select atomically, then retire this exact successful marker. + + Caller must drain serving operations, prove an inactive target, and retain + maintenance/stable-path ownership. No native/config/runtime action occurs. + Commit/rollback exceptions propagate; use separate safe readback to learn + whether durable selection occurred. A returned result is still config-pending. + """ + if type(intent) is not ActivationIntent or intent.state != "staged": + raise TierActivationError("A staged immutable intent is required") + if (intent.target != job.target + or (intent.job_id, intent.tracker, intent.source_epoch, intent.source_schema_signature) + != (job.job_id, job.tracker, job.source_epoch, job.source_schema_signature)): + raise TierActivationError("Activation intent belongs to a different job") + with _transaction(conn, write=True): + _guard_storage(conn) + check_selected_tier_job_in_writer(conn, job) + state = _state(conn) + _expected(state.selection, intent.expected_generation) + if state.intent != intent: + raise TierActivationError("Activation intent changed") + if (plan.manifest.model != intent.target.model_id or plan.manifest.dimension != intent.target.dimension + or plan.manifest.targets != intent.target.tables + or plan.manifest.source is None + or (plan.manifest.source.tracker, plan.manifest.source.revision.epoch, + plan.manifest.source.schema_signature) + != (intent.tracker, intent.source_epoch, intent.source_schema_signature)): + raise TierActivationError("Paired certificate belongs to a different target or source") + _guard_pair(conn, intent.target) + require_current_completion_in_writer(conn, plan) + selection = TierSelection( + uuid.uuid4().hex, intent.intent_id, intent.target, intent.reranker_id, intent.target.tables, + intent.job_id, intent.tracker, intent.source_epoch, intent.source_schema_signature, + plan.manifest.generation, hashlib.sha256(plan.saved_manifest.encode()).hexdigest(), + hashlib.sha256(plan.saved_receipt.encode()).hexdigest(), plan.digest, plan.schema, + plan.prefix_count + plan.manifest.outputs, plan.manifest.cursor, + ) + # The marker helper rechecks the exact row and refuses all incoming FKs. + # Both registry/metadata writes preserve their primary keys and cannot + # invoke triggers under the exact schemas accepted above. + _retire_selected_tier_job_in_writer(conn, job) + VectorCacheRegistry.set( + conn, intent.target.tier_group, vec_table=selection.tables[0], sep_table=selection.tables[1], + last_embedded_id=selection.cursor if selection.cursor is not None else 0, + vector_count=selection.vector_count, model_name=intent.target.model_id, + embedding_dim=intent.target.dimension, commit=False, + ) + _put(conn, "embed_model", intent.target.model_id) + _put(conn, "embed_dim", str(intent.target.dimension)) + _put(conn, _SELECTED, _dump(selection)) + _put(conn, _INTENT, _dump(replace(intent, state="db_selected"))) + return selection + + +def acknowledge_config(conn: sqlite3.Connection, *, generation: str) -> TierSelection: + """Generation-CAS acknowledgement after caller-confirmed config mirroring. + + This performs no config write and acknowledges no process's runtime state. + """ + if not _hex(generation, 32): + raise TierActivationError("A concrete selected generation is required") + with _transaction(conn, write=True): + _guard_storage(conn) + state = _state(conn) + _expected(state.selection, generation) + if state.intent.state == "staged" or state.intent.intent_id != state.selection.intent_id: + raise TierActivationError("A newer activation intent is pending") + if state.selection.config_acknowledged: + return state.selection + selected = replace(state.selection, config_acknowledged=True) + _put(conn, _SELECTED, _dump(selected)) + _put(conn, _INTENT, _dump(replace(state.intent, state="config_acknowledged"))) + return selected + + +def _guard_legacy_policy_pair(conn: sqlite3.Connection, target: EmbeddingTarget, tables: tuple[str, str], + *, certified: bool = False) -> None: + _guard_pair(conn, target, tables=tables) + for key, value in (("embed_model", target.model_id), ("embed_dim", str(target.dimension))): + row = conn.execute("SELECT typeof(value)='text' AND value=? FROM main.metadata WHERE key=?", (value, key)).fetchone() + if row is None or row[0] != 1: + raise TierActivationError("Legacy embedding metadata is not coherent with base/pro") + model_match = "model_name='qwen3_256'" if certified else "model_name IN ('qwen3_256','Qwen3-Embedding-0.6B')" + row = conn.execute( + "SELECT vec_table=? AND sep_table=? AND typeof(embedding_dim)='integer' AND embedding_dim=? " + f"AND {model_match} FROM main.vector_cache_registry WHERE tier_group='basepro'", (*tables, target.dimension), + ).fetchone() + if (row is None and (certified or tables != ("vec_messages", "vec_messages_sep"))) or (row is not None and row[0] != 1): + raise TierActivationError("Serving pair disagrees with its registry") + + + +def commit_config_only_transition( + conn: sqlite3.Connection, *, expected_selection_generation: str | None, + expected_intent_id: str | None, target: EmbeddingTarget, reranker_id: str, + expected_config: ConfigGuard, legacy_pair: tuple[str, str] | None = None, +) -> TierSelection | LegacyTierPolicy: + """Publish only base/pro policy, without a corpus read or vector certificate. + + Caller owns stable-path maintenance and exclusive serving. A legacy pair + must be its coherently observed serving pair, never inferred from counts. + Existing certificates are historical provenance, not recertified coverage. + """ + from truememory.tier_switch.job import _cancel_policy_job_in_writer, read_selected_job_marker + + if type(expected_config) is not ConfigGuard or expected_config.tier not in {"base", "pro"}: + raise TierActivationError("Policy transition requires actual prior config evidence") + if expected_intent_id is not None and not _hex(expected_intent_id, 32): + raise TierActivationError("Invalid expected prior activation intent") + with _transaction(conn, write=True): + _guard_storage(conn) + state = _state(conn) + _expected(state.selection, expected_selection_generation) + if (state.intent.intent_id if state.intent is not None else None) != expected_intent_id: + raise TierActivationError("Prior activation intent changed") + previous = state.selection or state.legacy_policy + if previous is not None and not previous.config_acknowledged: + raise TierActivationError("Prior serving policy awaits config acknowledgement") + tables = state.selection.tables if state.selection is not None else legacy_pair + _policy_identity(target, reranker_id, tables) + if target.tier == expected_config.tier: + raise TierActivationError("Policy-only transition must change base/pro public tier") + if previous is not None and ( + previous.target.tier != expected_config.tier or previous.generation != expected_config.generation + or previous.target.identity != target.identity + or previous.tables != tables or previous.reranker_id != reranker_id + ): + raise TierActivationError("Policy-only transition would change the serving embedding space") + _guard_legacy_policy_pair(conn, target, tables, certified=state.selection is not None) + if type(state.intent) is ActivationIntent and state.intent.state == "staged": + _cancel_policy_job_in_writer( + conn, job_id=state.intent.job_id, target=state.intent.target, tracker=state.intent.tracker, + source_epoch=state.intent.source_epoch, source_schema_signature=state.intent.source_schema_signature, + ) + else: + marker = read_selected_job_marker(conn) + if marker is not None and marker.state == "selected": + raise TierActivationError("An unrelated selected job cannot be superseded") + intent = PolicyIntent(uuid.uuid4().hex, expected_selection_generation, expected_intent_id, + target, reranker_id, expected_config, uuid.uuid4().hex, tables) + if state.selection is not None: + result = replace(state.selection, generation=intent.result_generation, intent_id=intent.intent_id, + target=target, config_acknowledged=False) + _put(conn, _SELECTED, _dump(result)) + else: + result = LegacyTierPolicy(intent.result_generation, intent.intent_id, target, reranker_id, tables) + _put(conn, _INTENT, _dump(intent)) + return result + + +def acknowledge_policy_config(conn: sqlite3.Connection, *, generation: str) -> LegacyTierPolicy: + """Acknowledge only this legacy policy's config mirror, never native state.""" + if not _hex(generation, 32): + raise TierActivationError("A concrete policy generation is required") + with _transaction(conn, write=True): + _guard_storage(conn) + state = _state(conn) + if type(state.intent) is not PolicyIntent or state.legacy_policy is None or state.legacy_policy.generation != generation: + raise TierActivationError("Legacy policy generation changed") + if state.legacy_policy.config_acknowledged: + return state.legacy_policy + _put(conn, _INTENT, _dump(replace(state.intent, state="config_acknowledged"))) + result = replace(state.legacy_policy, config_acknowledged=True) + return result diff --git a/truememory/tier_switch/cache.py b/truememory/tier_switch/cache.py index 50f67575..c3eee48a 100644 --- a/truememory/tier_switch/cache.py +++ b/truememory/tier_switch/cache.py @@ -75,21 +75,35 @@ def set( vector_count: int = 0, model_name: str | None = None, embedding_dim: int = 256, + commit: bool = True, ) -> None: - """Insert or replace a cache entry.""" + """Replace and commit by default; commit=False preserves the primary key. + + The additive mode uses UPSERT inside the caller's transaction so that + metadata publication cannot cause an incoming foreign-key delete cascade. + """ now = time.time() vt = vec_table or f"vec_messages_{group}" st = sep_table or f"vec_messages_sep_{group}" mn = model_name or model_name_for_group(group) + insertion = "INSERT OR REPLACE" if commit else "INSERT" + conflict = "" if commit else ( + " ON CONFLICT(tier_group) DO UPDATE SET " + "vec_table=excluded.vec_table, sep_table=excluded.sep_table, " + "last_embedded_id=excluded.last_embedded_id, vector_count=excluded.vector_count, " + "model_name=excluded.model_name, embedding_dim=excluded.embedding_dim, " + "last_updated=excluded.last_updated, created=excluded.created" + ) conn.execute( - "INSERT OR REPLACE INTO vector_cache_registry " + f"{insertion} INTO vector_cache_registry " "(tier_group, vec_table, sep_table, last_embedded_id, " "vector_count, model_name, embedding_dim, last_updated, created) " - "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)", + "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)" + conflict, (group, vt, st, last_embedded_id, vector_count, mn, embedding_dim, now, now), ) - conn.commit() + if commit: + conn.commit() @staticmethod def update_progress( @@ -97,15 +111,18 @@ def update_progress( group: str, last_embedded_id: int, vector_count: int, + *, + commit: bool = True, ) -> None: - """Update the progress fields after a batch commit.""" + """Update progress, optionally inside the caller's paired-vector transaction.""" conn.execute( "UPDATE vector_cache_registry " "SET last_embedded_id = ?, vector_count = ?, last_updated = ? " "WHERE tier_group = ?", (last_embedded_id, vector_count, time.time(), group), ) - conn.commit() + if commit: + conn.commit() @staticmethod def delete(conn: sqlite3.Connection, group: str) -> None: diff --git a/truememory/tier_switch/job.py b/truememory/tier_switch/job.py new file mode 100644 index 00000000..a5f0dbd0 --- /dev/null +++ b/truememory/tier_switch/job.py @@ -0,0 +1,505 @@ +"""Immutable background job identity for a cooperatively stable database path. + +This is an admission guard, not activation or a lock against arbitrary pathname +replacement. Capture requires a known-current caller connection. SQLite's Python +API exposes no database file descriptor to prove that historical association. +""" + +from __future__ import annotations + +import os +import re +import sqlite3 +import stat +import uuid +from collections.abc import Iterator +from contextlib import ExitStack, contextmanager +from dataclasses import dataclass, field +from pathlib import Path +from typing import Literal + +from truememory.embedding_target import EmbeddingTarget +from truememory.maintenance import MaintenanceUnavailableError, maintenance_owner +from truememory.rebuild_source import read_rebuild_revision + + +class TierJobIdentityError(RuntimeError): + """The background database identity cannot be established safely.""" + + +@dataclass(frozen=True) +class TierJob: + job_id: str + target: EmbeddingTarget + database_path: Path = field(repr=False) + file_identity: tuple[int, int] + tracker: str + source_epoch: str + source_schema_signature: str + + def __post_init__(self) -> None: + if (not isinstance(self.job_id, str) or re.fullmatch(r"[0-9a-f]{32}", self.job_id) is None + or not isinstance(self.target, EmbeddingTarget) + or not isinstance(self.database_path, Path) or not self.database_path.is_absolute() + or type(self.file_identity) is not tuple or len(self.file_identity) != 2 + or any(type(value) is not int for value in self.file_identity) + or self.file_identity[0] < 0 or self.file_identity[1] <= 0 + or self.tracker not in {"canonical-v1", "rebuild-bridge-v1", "rebuild-bridge-conservative-v1"} + or not isinstance(self.source_epoch, str) or not self.source_epoch + or not isinstance(self.source_schema_signature, str) + or re.fullmatch(r"[0-9a-f]{64}", self.source_schema_signature) is None): + raise TierJobIdentityError("Invalid background tier job descriptor") + + +@contextmanager +def _observe_file(path: Path) -> Iterator[tuple[int, int]]: + # Use fstat on every observation, including a fresh pathname reopen. + # ctime/mtime/size can change during legitimate writes and are not identity. + with ExitStack() as stack: + try: + flags = os.O_RDONLY | getattr(os, "O_NONBLOCK", 0) | getattr(os, "O_BINARY", 0) + descriptor = os.open(path, flags) + stack.callback(os.close, descriptor) + value = os.fstat(descriptor) + if (not stat.S_ISREG(value.st_mode) or type(value.st_dev) is not int + or type(value.st_ino) is not int or value.st_dev < 0 or value.st_ino <= 0): + raise TierJobIdentityError("Stable database file identity is unavailable") + except (OSError, ValueError): + raise TierJobIdentityError("Database file identity is unavailable") from None + yield value.st_dev, value.st_ino + + +def _connection_path(conn: sqlite3.Connection) -> Path: + if conn.in_transaction: + raise TierJobIdentityError("Background identity requires a clean connection") + databases = tuple(conn.execute("PRAGMA database_list")) + mains = [row[2] for row in databases if row[1] == "main"] + if len(mains) != 1 or not mains[0] or any(row[1] not in {"main", "temp"} for row in databases): + raise TierJobIdentityError("Background identity requires one existing file database") + # The tracker uses unqualified source tables. Refuse every TEMP object so + # temporary tables, views and triggers cannot redirect identity reads. + if conn.execute("SELECT 1 FROM sqlite_temp_master LIMIT 1").fetchone() is not None: + raise TierJobIdentityError("Temporary database objects make background identity ambiguous") + path = Path(mains[0]).expanduser().resolve(strict=True) + return path + + +def _source_identity(conn: sqlite3.Connection) -> tuple[str, str, str]: + conn.execute("BEGIN") + try: + tracker, revision, signature = read_rebuild_revision(conn) + return tracker, revision.epoch, signature + finally: + conn.rollback() + if conn.in_transaction: + raise TierJobIdentityError("Background identity could not release its read transaction") + + +def capture_tier_job(conn: sqlite3.Connection, target: EmbeddingTarget) -> TierJob: + """Capture a clean, known-current connection with tracking already installed. + + No connection or ownership token is retained. Private/shared-memory databases + cannot use this background reopen protocol. Caller owns stable-path admission. + """ + try: + path = _connection_path(conn) + with _observe_file(path) as identity: + tracker, epoch, signature = _source_identity(conn) + with _observe_file(path) as after: + if identity != after or _connection_path(conn) != path: + raise TierJobIdentityError("Database identity changed during job capture") + return TierJob(uuid.uuid4().hex, target, path, identity, tracker, epoch, signature) + except (OSError, ValueError, sqlite3.Error, MaintenanceUnavailableError): + raise TierJobIdentityError("Background database identity could not be captured") from None + + +def check_tier_job(conn: sqlite3.Connection, job: TierJob) -> None: + """Recheck before model admission or a future activation transaction. + + This short read snapshot ends here. Source-content freshness, current job + selection and final activation serialization remain separate caller duties. + """ + try: + if _connection_path(conn) != job.database_path: + raise TierJobIdentityError("Background database path changed") + with _observe_file(job.database_path) as before: + if before != job.file_identity: + raise TierJobIdentityError("Background database file changed") + if _source_identity(conn) != (job.tracker, job.source_epoch, job.source_schema_signature): + raise TierJobIdentityError("Background database source identity changed") + with _observe_file(job.database_path) as after: + if before != after or _connection_path(conn) != job.database_path: + raise TierJobIdentityError("Background database identity changed during validation") + except (OSError, ValueError, sqlite3.Error, MaintenanceUnavailableError): + raise TierJobIdentityError("Background database identity could not be validated") from None + + +def _close_connection(conn: sqlite3.Connection) -> None: + try: + conn.close() + except (OSError, sqlite3.Error): + raise TierJobIdentityError("Background database connection could not be closed") from None + + +@contextmanager +def open_tier_job(job: TierJob) -> Iterator[sqlite3.Connection]: + """Own and reopen an existing database in this worker thread, never create it. + + The connection and cooperative maintenance owner both end on BaseException. + The descriptor conveys no caller-thread maintenance ownership. + """ + with ExitStack() as stack: + try: + stack.enter_context(maintenance_owner(job.database_path)) + identity = stack.enter_context(_observe_file(job.database_path)) + if identity != job.file_identity: + raise TierJobIdentityError("Background database file changed before reopen") + conn = sqlite3.connect(job.database_path.as_uri() + "?mode=rw", uri=True) + stack.callback(_close_connection, conn) + check_tier_job(conn, job) + except (OSError, ValueError, sqlite3.Error, MaintenanceUnavailableError): + raise TierJobIdentityError("Background database could not be reopened") from None + yield conn + + +_SELECTION_TABLE = "truememory_tier_selected_job_v1" +_SELECTION_SQL = f"""CREATE TABLE {_SELECTION_TABLE} ( + singleton INTEGER PRIMARY KEY CHECK (singleton = 1), + job_id TEXT NOT NULL CHECK (typeof(job_id) = 'text' AND length(job_id) = 32), + state TEXT NOT NULL CHECK (state IN ('selected', 'cancelled', 'failed')), + tier TEXT NOT NULL CHECK (tier IN ('edge', 'base', 'pro', 'custom')), + model_id TEXT NOT NULL CHECK (typeof(model_id) = 'text' AND length(model_id) BETWEEN 1 AND 512 AND length(CAST(model_id AS BLOB)) <= 2048), + dimension INTEGER NOT NULL CHECK (typeof(dimension) = 'integer' AND dimension BETWEEN 1 AND 4096), + tier_group TEXT NOT NULL CHECK (tier_group IN ('edge', 'basepro', 'custom')), + tracker TEXT NOT NULL CHECK (tracker IN ('canonical-v1', 'rebuild-bridge-v1', 'rebuild-bridge-conservative-v1')), + source_epoch TEXT NOT NULL CHECK (typeof(source_epoch) = 'text' AND length(source_epoch) BETWEEN 1 AND 128 AND length(CAST(source_epoch AS BLOB)) <= 512), + source_schema_signature TEXT NOT NULL CHECK (typeof(source_schema_signature) = 'text' AND length(source_schema_signature) = 64) +)""" +_SELECTION_FIELDS = ("job_id", "tier", "model_id", "dimension", "tier_group", "tracker", + "source_epoch", "source_schema_signature") +_SELECTION_BOUNDS = (32, 6, 512, None, 7, 30, 128, 64) + + +class TierJobSelectionError(TierJobIdentityError): + """The durable selected job is unavailable or does not match this request.""" + + +@dataclass(frozen=True) +class SelectedJobMarker: + job_id: str + target: EmbeddingTarget + tracker: str + source_epoch: str + source_schema_signature: str + state: str + + +def _marker_in_snapshot(conn: sqlite3.Connection) -> SelectedJobMarker | None: + if conn.execute("SELECT 1 FROM temp.sqlite_master LIMIT 1").fetchone() is not None: + raise TierJobSelectionError("TEMP objects make selected marker identity ambiguous") + shape = conn.execute( + "SELECT typeof(sql), length(CAST(sql AS BLOB)) FROM main.sqlite_master " + "WHERE name=? COLLATE NOCASE", (_SELECTION_TABLE,), + ).fetchone() + if shape is None: + return None + if shape[0] != "text" or not 1 <= shape[1] <= 8192: + raise TierJobSelectionError("Selected job schema exceeds its protocol bounds") + _selection_schema(conn) + row = _selection_row(conn) + return None if row is None else SelectedJobMarker( + row[0], EmbeddingTarget(row[1], row[2], row[3], row[4]), row[5], row[6], row[7], row[8], + ) + + +def read_selected_job_marker(conn: sqlite3.Connection) -> SelectedJobMarker | None: + """Bounded marker read; borrow a caller snapshot without ending it. + + This is not a reopened-file identity certificate or permission to resume a + disappeared worker. Terminal IDs permit explicit normal job replacement. + """ + if conn.in_transaction: + return _marker_in_snapshot(conn) + with _selection_transaction(conn, write=False): + return _marker_in_snapshot(conn) + + +def _cancel_policy_job_in_writer( + conn: sqlite3.Connection, *, job_id: str, target: EmbeddingTarget, tracker: str, + source_epoch: str, source_schema_signature: str, +) -> None: + """Cancel only the exact superseded intent's marker; caller owns writer. + + Maintenance/stable-path ownership and writer acquisition are caller duties. + No source read, file observation, DELETE, native call, commit or rollback. + """ + if not conn.in_transaction: + raise TierJobSelectionError("Policy supersession requires writer ownership") + row = _marker_in_snapshot(conn) + if row is None or (row.job_id, row.target, row.tracker, row.source_epoch, row.source_schema_signature) != ( + job_id, target, tracker, source_epoch, source_schema_signature, + ): + raise TierJobSelectionError("Superseded activation job changed") + if row.state == "selected": + changed = conn.execute( + f"UPDATE main.{_SELECTION_TABLE} SET state='cancelled' " + "WHERE singleton=1 AND job_id=? AND state='selected'", (job_id,), + ).rowcount + if changed != 1: + raise TierJobSelectionError("Superseded activation job changed during cancellation") + + +def _selection_values(job: TierJob) -> tuple[str | int, ...]: + values = (job.job_id, job.target.tier, job.target.model_id, job.target.dimension, + job.target.tier_group, job.tracker, job.source_epoch, job.source_schema_signature) + for value, bound in zip(values, _SELECTION_BOUNDS): + if bound is None: + if type(value) is not int or not 1 <= value <= 4096: + raise TierJobSelectionError("Invalid selected job identity") + elif type(value) is not str or not 1 <= len(value) <= bound: + raise TierJobSelectionError("Selected job identity exceeds its protocol bounds") + return values + + +def _selection_schema(conn: sqlite3.Connection, *, create: bool = False) -> None: + row = conn.execute( + "SELECT type, sql FROM main.sqlite_master WHERE name = ? COLLATE NOCASE", (_SELECTION_TABLE,), + ).fetchone() + if row is None and create: + conn.execute(_SELECTION_SQL) + elif (row is None or row[0] != "table" or type(row[1]) is not str + or " ".join(row[1].strip().rstrip(";").split()) != " ".join(_SELECTION_SQL.split())): + raise TierJobSelectionError("Selected job storage has an unsupported schema") + if conn.execute( + "SELECT 1 FROM main.sqlite_master WHERE tbl_name = ? COLLATE NOCASE " + "AND type IN ('index', 'trigger') LIMIT 1", (_SELECTION_TABLE,), + ).fetchone() is not None: + raise TierJobSelectionError("Selected job storage has unsupported side effects") + + +def _selection_row(conn: sqlite3.Connection) -> tuple[str | int, ...] | None: + identifiers = [tuple(row) for row in conn.execute(f"SELECT singleton FROM main.{_SELECTION_TABLE} LIMIT 2")] + if not identifiers: + return None + if identifiers != [(1,)]: + raise TierJobSelectionError("Selected job storage contains an invalid singleton") + # Read only sizes/types before copying bounded marker values into Python. + probes = ", ".join(f"typeof({name}), length(CAST({name} AS BLOB))" for name in (*_SELECTION_FIELDS, "state")) + metadata = conn.execute(f"SELECT {probes} FROM main.{_SELECTION_TABLE} WHERE singleton = 1").fetchone() + if metadata is None: + raise TierJobSelectionError("Selected job storage changed during validation") + for index, bound in enumerate((*_SELECTION_BOUNDS, 9)): + kind, length = metadata[index * 2:index * 2 + 2] + if ((bound is None and kind != "integer") + or (bound is not None and (kind != "text" or type(length) is not int or not 1 <= length <= 4 * bound))): + raise TierJobSelectionError("Selected job storage contains invalid values") + fields = ", ".join((*_SELECTION_FIELDS, "state")) + row = tuple(conn.execute(f"SELECT {fields} FROM main.{_SELECTION_TABLE} WHERE singleton = 1").fetchone()) + if any(bound is not None and (type(value) is not str or not 1 <= len(value) <= bound) + for value, bound in zip(row, (*_SELECTION_BOUNDS, 9))): + raise TierJobSelectionError("Selected job storage contains oversized values") + if (re.fullmatch(r"[0-9a-f]{32}", row[0]) is None + or re.fullmatch(r"[0-9a-f]{64}", row[7]) is None + or row[5] not in {"canonical-v1", "rebuild-bridge-v1", "rebuild-bridge-conservative-v1"} + or row[8] not in {"selected", "cancelled", "failed"}): + raise TierJobSelectionError("Selected job storage contains an invalid identity") + EmbeddingTarget(row[1], row[2], row[3], row[4]) + return row + + +def _rollback_selection(conn: sqlite3.Connection) -> None: + try: + conn.rollback() + except (OSError, sqlite3.Error): + raise TierJobSelectionError("Selected job transaction rollback is uncertain") from None + if conn.in_transaction: + raise TierJobSelectionError("Selected job transaction rollback is uncertain") + + +@contextmanager +def _selection_transaction(conn: sqlite3.Connection, *, write: bool) -> Iterator[None]: + if conn.in_transaction: + raise TierJobSelectionError("Selected job admission requires a clean connection") + conn.execute("BEGIN IMMEDIATE" if write else "BEGIN") + try: + yield + if write: + conn.commit() + if conn.in_transaction: + raise TierJobSelectionError("Selected job transaction did not commit") + except BaseException: + if conn.in_transaction: + _rollback_selection(conn) + raise + else: + if not write: + _rollback_selection(conn) + + +def _selection_source(conn: sqlite3.Connection, job: TierJob) -> None: + databases = tuple(conn.execute("PRAGMA database_list")) + mains = [row[2] for row in databases if row[1] == "main"] + if (len(mains) != 1 or not mains[0] + or any(row[1] not in {"main", "temp"} for row in databases) + or Path(mains[0]).expanduser().resolve(strict=True) != job.database_path): + raise TierJobSelectionError("Selected job database path changed") + if conn.execute("SELECT 1 FROM sqlite_temp_master LIMIT 1").fetchone() is not None: + raise TierJobSelectionError("Temporary database objects make selected job identity ambiguous") + tracker, revision, signature = read_rebuild_revision(conn) + if (tracker, revision.epoch, signature) != (job.tracker, job.source_epoch, job.source_schema_signature): + raise TierJobSelectionError("Selected job source identity changed") + with _observe_file(job.database_path) as identity: + if identity != job.file_identity: + raise TierJobSelectionError("Selected job database file changed") + + +def select_tier_job( + conn: sqlite3.Connection, target: EmbeddingTarget, *, previous_job_id: str | None = None, +) -> TierJob: + """Commit a new selection before planning, without replacing a live job. + + A terminal marker can be replaced only by explicitly naming its previous ID. + The new ID is written through the accepted connection, not a pathname reopen. + """ + if previous_job_id is not None and (type(previous_job_id) is not str + or re.fullmatch(r"[0-9a-f]{32}", previous_job_id) is None): + raise TierJobSelectionError("Invalid previous selected job identity") + job = capture_tier_job(conn, target) + values = _selection_values(job) + try: + with _selection_transaction(conn, write=True): + _selection_source(conn, job) + _selection_schema(conn, create=True) + previous = _selection_row(conn) + if previous is None: + if previous_job_id is not None: + raise TierJobSelectionError("Previous selected job is no longer present") + fields = ", ".join(_SELECTION_FIELDS) + conn.execute( + f"INSERT INTO main.{_SELECTION_TABLE}(singleton, {fields}, state) " + "VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, 'selected')", values, + ) + else: + if previous[0] != previous_job_id or previous[8] == "selected": + raise TierJobSelectionError("Another job already owns the selection") + assignments = ", ".join(f"{name} = ?" for name in _SELECTION_FIELDS) + changed = conn.execute( + f"UPDATE main.{_SELECTION_TABLE} SET {assignments}, state = 'selected' " + "WHERE singleton = 1 AND job_id = ? AND state IN ('cancelled', 'failed')", + (*values, previous_job_id), + ).rowcount + if changed != 1: + raise TierJobSelectionError("Selected job changed during replacement") + _selection_source(conn, job) + return job + except (OSError, ValueError, sqlite3.Error, MaintenanceUnavailableError): + raise TierJobSelectionError("Background job could not be selected") from None + + +def check_selected_tier_job(conn: sqlite3.Connection, job: TierJob) -> None: + """Read a coherent source/marker snapshot; return with no transaction open.""" + values = _selection_values(job) + try: + check_tier_job(conn, job) + with _selection_transaction(conn, write=False): + _selection_source(conn, job) + _selection_schema(conn) + if _selection_row(conn) != (*values, "selected"): + raise TierJobSelectionError("Background job is no longer selected") + _selection_source(conn, job) + except (OSError, ValueError, sqlite3.Error, MaintenanceUnavailableError): + raise TierJobSelectionError("Selected background job could not be validated") from None + + +def check_selected_tier_job_in_writer(conn: sqlite3.Connection, job: TierJob) -> None: + """Read only inside the caller's owned BEGIN IMMEDIATE transaction. + + Python sqlite3 exposes whether a transaction exists, not its lock kind. + The caller must establish writer ownership; this helper neither acquires it + nor commits/rolls back caller work. Stable-path/maintenance ownership is + still required, just as for the public clean-connection check. + """ + if not conn.in_transaction: + raise TierJobSelectionError("Selected job certification requires caller writer ownership") + values = _selection_values(job) + shape = conn.execute( + "SELECT typeof(sql), length(CAST(sql AS BLOB)) FROM main.sqlite_master " + "WHERE name = ? COLLATE NOCASE", (_SELECTION_TABLE,), + ).fetchone() + if shape is None or shape[0] != "text" or not 1 <= shape[1] <= 8192: + raise TierJobSelectionError("Selected job schema exceeds its protocol bounds") + _selection_source(conn, job) + _selection_schema(conn) + if _selection_row(conn) != (*values, "selected"): + raise TierJobSelectionError("Background job is no longer selected") + _selection_source(conn, job) + + +def _retire_selected_tier_job_in_writer(conn: sqlite3.Connection, job: TierJob) -> None: + """Retire this exact successful marker only within certified selection SQL. + + Reject incoming foreign keys before DELETE, including when enforcement is + disabled on this connection. The bounded schema walk never copies table + definitions or unbounded names into Python. No commit or rollback occurs. + """ + check_selected_tier_job_in_writer(conn, job) + count = 0 + for (name,) in conn.execute( + "SELECT CASE WHEN typeof(name)='text' AND length(CAST(name AS BLOB)) BETWEEN 1 AND 512 " + "THEN name END FROM main.sqlite_master WHERE type='table' LIMIT 4097", + ): + count += 1 + if name is None or count > 4096: + raise TierJobSelectionError("Database schema exceeds safe marker retirement bounds") + if conn.execute( + 'SELECT 1 FROM pragma_foreign_key_list(?, \'main\') WHERE "table" = ? COLLATE NOCASE LIMIT 1', + (name, _SELECTION_TABLE), + ).fetchone() is not None: + raise TierJobSelectionError("Selected job marker has an incoming foreign key") + where = " AND ".join(f"{name} = ?" for name in _SELECTION_FIELDS) + changed = conn.execute( + f"DELETE FROM main.{_SELECTION_TABLE} WHERE singleton=1 AND state='selected' AND {where}", + _selection_values(job), + ).rowcount + if changed != 1: + raise TierJobSelectionError("Selected job changed before retirement") + + +@contextmanager +def open_selected_tier_job(job: TierJob) -> Iterator[sqlite3.Connection]: + """Reopen under worker ownership and validate the committed selection.""" + with open_tier_job(job) as conn: + check_selected_tier_job(conn, job) + yield conn + + +def end_selected_tier_job( + conn: sqlite3.Connection, job: TierJob, *, outcome: Literal["cancelled", "failed"], +) -> bool: + """End only this exact selected job; never modify a replacement's marker. + + This is a terminal write after abandoning any source plan, not page progress + or successful completion/activation. False means the expected job is absent. + """ + if outcome not in {"cancelled", "failed"}: + raise TierJobSelectionError("Unsupported terminal selected job outcome") + values = _selection_values(job) + try: + check_tier_job(conn, job) + with _selection_transaction(conn, write=True): + _selection_source(conn, job) + _selection_schema(conn) + row = _selection_row(conn) + if row != (*values, "selected"): + return False + conditions = " AND ".join(f"{name} = ?" for name in _SELECTION_FIELDS) + changed = conn.execute( + f"UPDATE main.{_SELECTION_TABLE} SET state = ? " + f"WHERE singleton = 1 AND {conditions} AND state = 'selected'", (outcome, *values), + ).rowcount + if changed != 1: + raise TierJobSelectionError("Selected job changed during termination") + _selection_source(conn, job) + return True + except (OSError, ValueError, sqlite3.Error, MaintenanceUnavailableError): + raise TierJobSelectionError("Selected background job could not be ended") from None diff --git a/truememory/tier_switch/manager.py b/truememory/tier_switch/manager.py index b0a57943..b33c6c3e 100644 --- a/truememory/tier_switch/manager.py +++ b/truememory/tier_switch/manager.py @@ -13,14 +13,16 @@ import threading import time from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from truememory.tier_switch.activation import ActivationIntent, ActivationState, ConfigGuard, LegacyTierPolicy, TierSelection + from truememory.tier_switch.job import TierJob + from truememory.tier_switch.source import TierSourcePlan + from truememory.tier_switch.worker import StatusCallback from truememory.tier_switch.cache import ( - VectorCacheRegistry, - get_messages_to_embed, - model_name_for_group, preflight_ram_check, - resolve_rebuild_action, - tier_group, ) from truememory.tier_switch.throttler import DynamicThrottler from truememory.tier_switch.worker import RebuildWorker @@ -37,47 +39,56 @@ class TierSwitchUnsupportedError(RuntimeError): def _detect_device() -> str: - """Detect the best available compute device.""" - try: - import torch - if torch.backends.mps.is_available(): - return "mps" - if torch.cuda.is_available(): - return "cuda" - except Exception: - pass - return "cpu" + """Use the same device override and auto policy as model loading.""" + from truememory.mps_utils import auto_detect_device, resolve_device + + return resolve_device(auto_detect_device()) or "cpu" def _get_db_path() -> Path: """Return the path to the TrueMemory database.""" - return _TRUEMEMORY_DIR / "memories.db" + return Path(os.environ.get("TRUEMEMORY_DB_PATH") or os.environ.get("TRUEMEMORY_DB") or _TRUEMEMORY_DIR / "memories.db") -def _open_db(db_path: Path | None = None) -> sqlite3.Connection: - """Open a new SQLite connection (thread-safe: each thread gets its own).""" +def _open_db(db_path: Path | None = None, *, initialize: bool = False) -> sqlite3.Connection: + """Validate existing journal storage before any connection or schema writes.""" from truememory.storage import DEFAULT_BUSY_TIMEOUT_MS, _validate_db_path + from truememory.tier_switch.activation import read_activation_state + + path = Path(_validate_db_path(db_path or _get_db_path())) + conn = (sqlite3.connect(path, check_same_thread=False) if initialize else + sqlite3.connect(path.absolute().as_uri() + "?mode=rw", uri=True, check_same_thread=False)) + try: + objects = conn.execute( + "SELECT 1 FROM main.sqlite_master WHERE name NOT LIKE 'sqlite_%' LIMIT 1", + ).fetchone() + if objects is None: + if not initialize: + raise TierSwitchUnsupportedError("Database has not been initialized") + conn.close() + from truememory.storage import create_db + conn = create_db(path) + else: + read_activation_state(conn) + if conn.execute("SELECT 1 FROM main.sqlite_master WHERE type='table' AND name='messages'").fetchone() is None: + raise TierSwitchUnsupportedError("Existing source schema cannot be initialized by a tier switch") + conn.execute(f"PRAGMA busy_timeout={DEFAULT_BUSY_TIMEOUT_MS}") + conn.execute("PRAGMA foreign_keys=ON") + conn.execute("PRAGMA synchronous=NORMAL") + _load_tier_extension(conn) + return conn + except BaseException: + conn.close() + raise - path = db_path or _get_db_path() - conn = sqlite3.connect(_validate_db_path(path), check_same_thread=False) - conn.execute("PRAGMA journal_mode=WAL") - conn.execute(f"PRAGMA busy_timeout={DEFAULT_BUSY_TIMEOUT_MS}") - conn.execute("PRAGMA foreign_keys=ON") - conn.execute("PRAGMA synchronous=NORMAL") - - # M-23: guard the sqlite-vec load. On macOS system Python (and any build - # compiled without SQLITE_ENABLE_LOAD_EXTENSION) ``enable_load_extension`` - # is absent → bare access raised AttributeError, crashing the tier switch - # (reached from truememory_configure, the CLI upgrade-tier path, and a - # background thread that swallowed it). A tier switch is meaningless - # without vec support, so fail with actionable platform guidance instead. + +def _load_tier_extension(conn: sqlite3.Connection) -> None: try: import sqlite_vec conn.enable_load_extension(True) sqlite_vec.load(conn) conn.enable_load_extension(False) except (AttributeError, ImportError, OSError, sqlite3.OperationalError) as exc: - conn.close() raise TierSwitchUnsupportedError( "Tier switching requires the sqlite-vec extension, but this " "Python's sqlite3 cannot load it " @@ -87,8 +98,6 @@ def _open_db(db_path: Path | None = None) -> sqlite3.Connection: "support (e.g. python.org, Homebrew, or pyenv) and retry." ) from exc - return conn - class RebuildManager: """Orchestrates tier-switch re-embedding (singleton for MCP).""" @@ -112,82 +121,85 @@ def __init__(self): # pre-thread failure clears it instead of bricking the manager. self._claimed: bool = False self._active_status_id: int = 0 + self._active_job_id: str | None = None self._state_lock = threading.Lock() + self._cancel_requested = threading.Event() + self._active_db_path: Path | None = None + self._last_outcome: str | None = None + self._requested_tier: str | None = None + self._last_action: str | None = None + self._served_tier: str | None = None + self._config_path: Path = _TRUEMEMORY_DIR / "config.json" + self._config_lock_path: Path | None = None def start_rebuild( - self, - target_tier: str, - force: bool = False, - backup_path: Path | None = None, - db_path: Path | None = None, + self, target_tier: str, force: bool = False, + backup_path: Path | None = None, db_path: Path | None = None, *, + config_path: Path | None = None, config_lock_path: Path | None = None, ) -> int: - """Start an async rebuild in a background thread (MCP path). + """Stage an inactive job, then hand only its immutable identity to a thread.""" + from truememory.maintenance import maintenance_owner - Returns a status_id for progress queries. - """ - # M-51: reserve the slot with a claim flag rather than parking the - # long-lived MCP caller thread in ``_active_thread``. Previously a raise - # in any pre-thread step below (e.g. M-23 in _open_db, preflight) left - # ``_active_thread = current_thread()`` set; since that thread stays - # alive for the process lifetime, ``is_alive()`` was permanently True - # and every subsequent start_rebuild no-op'd — bricking tier switching. - # Only the real worker Thread should occupy the slot; release the claim - # on any pre-thread failure. with self._state_lock: - if self._active_thread and self._active_thread.is_alive(): - return self._active_status_id - if self._claimed: + if self._claimed or self._active_thread and self._active_thread.is_alive(): + if self._requested_tier != target_tier: + raise TierSwitchUnsupportedError("Another requested tier is already being prepared") return self._active_status_id + self._requested_tier = target_tier + self._active_status_id = 0 + self._active_job_id = None + self._last_action = None + self._last_outcome = "preparing" + self._served_tier = None self._claimed = True - + self._cancel_requested.clear() + self._config_path = config_path or _TRUEMEMORY_DIR / "config.json" + self._config_lock_path = config_lock_path + path = Path(db_path) if db_path is not None else _get_db_path() + conn = None + thread = None + self._active_db_path = path try: - conn = _open_db(db_path) - to_group = tier_group(target_tier) - - ok, msg = preflight_ram_check(to_group) - if not ok: - conn.close() - raise RuntimeError(msg) - - action = resolve_rebuild_action(conn, to_group, force) - messages, is_full = get_messages_to_embed(conn, to_group, force) - - if not messages and not is_full: - self._apply_config_switch(target_tier, conn) + if force: + raise TierSwitchUnsupportedError("Active force/reset requires an alternate inactive pair and is not supported") + if self._try_legacy_noop(path, target_tier): + return 0 + with maintenance_owner(path): + if self._bootstrap_empty_configuration(path, target_tier, force): + return 0 + conn = _open_db(path, initialize=True) + prepared = self._prepare_job(conn, target_tier, force, backup_path=backup_path) conn.close() - with self._state_lock: - self._claimed = False + conn = None + if prepared is None: return 0 - - status_id = self._create_status_row( - conn, to_group, target_tier, action, len(messages), - ) - self._active_status_id = status_id - - if backup_path is None and is_full: - backup_path = backup_db(db_path) - - conn.close() - - thread = threading.Thread( - target=self._rebuild_thread, - args=(target_tier, to_group, messages, is_full, status_id, - backup_path, db_path), - daemon=True, - name=f"tier-switch-{to_group}", - ) + job, intent, status_id = prepared + thread = threading.Thread(target=self._rebuild_thread, args=(job, intent, status_id), + daemon=True, name=f"tier-switch-{job.target.tier_group}") with self._state_lock: + self._active_status_id = status_id + self._active_job_id = job.job_id + self._active_db_path = job.database_path self._active_thread = thread - self._claimed = False thread.start() + return status_id except BaseException: - # Pre-thread failure: release the claim so the next call can retry. - # The worker Thread never started, so it never occupied the slot. + if conn is not None: + conn.close() + conn = None + if self._last_outcome == "activation_pending": + self._refresh_committed_serving(path) + else: + self._last_outcome = "interrupted" + if thread is not None and not thread.is_alive(): + with self._state_lock: + self._active_thread = None + raise + finally: + if conn is not None: + conn.close() with self._state_lock: self._claimed = False - raise - - return status_id def run_rebuild_sync( self, @@ -227,178 +239,527 @@ def run_rebuild_sync( lock_fd.close() def _run_rebuild_sync_inner( - self, - target_tier: str, - force: bool = False, - progress_callback=None, + self, target_tier: str, force: bool = False, progress_callback=None, db_path: Path | None = None, ) -> bool: - conn = _open_db(db_path) - to_group = tier_group(target_tier) - - ok, msg = preflight_ram_check(to_group) - if not ok: - conn.close() - log.error("Pre-flight failed: %s", msg) - return False - - action = resolve_rebuild_action(conn, to_group, force) - messages, is_full = get_messages_to_embed(conn, to_group, force) - - if not messages and not is_full: - self._apply_config_switch(target_tier, conn) - conn.close() + from truememory.maintenance import maintenance_owner + + if force: + raise TierSwitchUnsupportedError("Active force/reset requires an alternate inactive pair and is not supported") + path = Path(db_path) if db_path is not None else _get_db_path() + self._cancel_requested.clear() + self._active_db_path = path + if self._try_legacy_noop(path, target_tier): return True + with maintenance_owner(path): + if self._bootstrap_empty_configuration(path, target_tier, force): + return self._last_outcome == "complete" + conn = _open_db(path, initialize=True) + try: + prepared = self._prepare_job(conn, target_tier, force) + if prepared is None: + return True + job, intent, status_id = prepared + self._active_status_id, self._active_db_path = status_id, job.database_path + self._active_job_id = job.job_id + return self._run_job(conn, job, intent, status_id, progress_callback) + except BaseException: + conn.close() + conn = None + if self._last_outcome == "activation_pending": + self._refresh_committed_serving(path) + else: + self._last_outcome = "interrupted" + raise + finally: + self._active_worker = None + if conn is not None: + conn.close() - status_id = self._create_status_row( - conn, to_group, target_tier, action, len(messages), - ) - - if is_full: - backup_db(db_path) - - device = _detect_device() - if to_group == "edge": - device = "cpu" - throttler = DynamicThrottler(device=device) - - worker = RebuildWorker( - conn=conn, - target_tier=target_tier, - target_group=to_group, - throttler=throttler, - status_id=status_id, - status_callback=progress_callback, - ) - self._active_worker = worker - + def _refresh_committed_serving(self, path: Path) -> None: + """Read back on a fresh validated connection after discarding ambiguity.""" + from truememory.tier_switch.activation import read_activation_state + self._served_tier = None try: - success, processed = worker.run(messages, is_full) + conn = _open_db(path) + try: + state = read_activation_state(conn) + result = state.selection or state.legacy_policy + if result is not None: + self._served_tier = result.target.tier + finally: + conn.close() except Exception: - log.exception("Sync rebuild failed") - success = False - - if success: - self._finalize_rebuild(conn, target_tier, to_group) + log.warning("Committed tier readback remains unavailable", exc_info=True) + + @staticmethod + def _apply_bootstrap_identity(target: object, reranker_id: str) -> None: + """Configure only proven-unloaded slots; caller holds exclusive serving.""" + from truememory import vector_search, reranker + from truememory.tier_config import get_tier_config + if vector_search._model is not None or reranker._model is not None: + raise TierSwitchUnsupportedError("First-run identity projection requires unloaded model slots") + if reranker_id != get_tier_config(target.tier)["reranker"]: + raise TierSwitchUnsupportedError("First-run reranker identity must match its tier") + # Onboarding has no database policy to authorize policy-tagged slots. + vector_search.set_embedding_model(target.tier) + reranker.set_active_tier(target.tier) + os.environ["TRUEMEMORY_EMBED_MODEL"] = target.tier + + def _try_legacy_noop(self, path: Path, target_tier: str) -> bool: + """Recognize an unchanged legacy tier without competing for maintenance.""" + try: + path.stat() + except FileNotFoundError: + return False + from truememory import vector_search, reranker + from truememory.embedding_target import EmbeddingTarget + from truememory.tier_config import get_tier_config + from truememory.tier_switch.activation import _guard_storage, _state + from truememory.tier_switch.job import read_selected_job_marker + from truememory.tier_switch.projection import CONFIG_WRITE_LOCK, ConfigFileLock, _guard, _read_config + from truememory.tier_switch.runtime import _legacy_key, serving_operation + from truememory.tier_switch.serving import operation_lease + + target = EmbeddingTarget.capture(target_tier) + if (vector_search._frozen_embedding_target is not None + or getattr(vector_search, "_runtime_policy_tier", None) is not None): + return False + lock_path = self._config_lock_path or self._config_path.with_name(self._config_path.name + ".lock") + # The shared lease permits ordinary maintenance and pins the process + # identity. All database reads use one owned read-only snapshot. + with operation_lease(_legacy_key(), cancelled=self._cancel_requested): + conn = sqlite3.connect(path.absolute().as_uri() + "?mode=ro", uri=True) + try: + conn.execute("BEGIN") + if conn.execute("SELECT 1 FROM main.sqlite_master WHERE name NOT LIKE 'sqlite_%' LIMIT 1").fetchone() is None: + return False + _guard_storage(conn) + state = _state(conn) + if state.selection is not None or state.legacy_policy is not None or state.intent is not None: + return False + if read_selected_job_marker(conn) is not None: + return False + with ConfigFileLock(lock_path, strict=True), CONFIG_WRITE_LOCK: + guard = _guard(_read_config(self._config_path)) + if guard.generation is not None or guard.tier != target.tier: + return False + old_target, _ = self._serving_identity(conn, state, guard) + if old_target != target: + return False + with serving_operation(conn, cancelled=self._cancel_requested) as operation: + if (operation.tier != target.tier + or operation.reranker_id != get_tier_config(target.tier)["reranker"] + or reranker._frozen_reranker_id is not None): + raise TierSwitchUnsupportedError("Legacy runtime identity must be coherent before a tier no-op") + self._served_tier = target.tier + self._last_action = "noop" + self._last_outcome = "complete" + return True + finally: + try: + if conn.in_transaction: + conn.rollback() + finally: + conn.close() + + def _bootstrap_empty_configuration(self, path: Path, target_tier: str, force: bool) -> bool: + """Configure a proven uninitialized store without inventing vector state.""" + from truememory.embedding_target import EmbeddingTarget + from truememory.tier_switch.projection import CONFIG_WRITE_LOCK, ConfigFileLock, _read_config, _write_config + from truememory.tier_switch.serving import exclusive_activation + + if force: + return False + target = EmbeddingTarget.capture(target_tier) + with exclusive_activation(cancelled=self._cancel_requested): + try: + path.stat() + present = True + except FileNotFoundError: + present = False + if present: + conn = sqlite3.connect(path.absolute().as_uri() + "?mode=ro", uri=True) + try: + if conn.execute("SELECT 1 FROM main.sqlite_master WHERE name NOT LIKE 'sqlite_%' LIMIT 1").fetchone(): + return False + finally: + conn.close() + from truememory import vector_search, reranker + from truememory.tier_config import get_tier_config + if (getattr(vector_search, "_frozen_embedding_target", None) is not None + or getattr(vector_search, "_runtime_policy_tier", None) is not None + or getattr(reranker, "_frozen_reranker_id", None) is not None): + raise TierSwitchUnsupportedError("First-run configuration cannot inherit another database's controlled runtime") + served_tier = vector_search.resolve_tier() + observed = EmbeddingTarget.capture(served_tier) + observed_reranker = reranker.get_current_reranker_name() + coherent = ((vector_search.EMBEDDING_MODEL, vector_search._embedding_dim) == (observed.model_id, observed.dimension) + and observed_reranker == get_tier_config(observed.tier)["reranker"]) + self._served_tier = served_tier if coherent else None + ready = coherent and served_tier == target.tier + if not ready and (vector_search._model is not None or reranker._model is not None): + raise TierSwitchUnsupportedError("First-run tier configuration requires unloaded model slots; restart before changing tier") + lock_path = self._config_lock_path or self._config_path.with_name(self._config_path.name + ".lock") + with ConfigFileLock(lock_path, strict=True), CONFIG_WRITE_LOCK: + config = _read_config(self._config_path) + if "tier_activation_generation" in config: + raise TierSwitchUnsupportedError("An activation-tagged config cannot bootstrap a missing store") + self._last_action = "onboarding" + old_env = os.environ.get("TRUEMEMORY_EMBED_MODEL") + if not ready and not coherent: + raise TierSwitchUnsupportedError("First-run legacy identity must be coherent before lazy projection") + requested_config = dict(config, tier=target_tier) + try: + if not ready: + self._last_outcome = "activation_pending" + self._apply_bootstrap_identity(target, get_tier_config(target.tier)["reranker"]) + if ((vector_search.EMBEDDING_MODEL, vector_search._embedding_dim) != (target.model_id, target.dimension) + or vector_search.resolve_tier() != target.tier + or reranker.get_current_reranker_name() != get_tier_config(target.tier)["reranker"]): + raise TierSwitchUnsupportedError("First-run lazy model identity did not reconcile") + self._served_tier = target.tier + _write_config(self._config_path, requested_config) + except BaseException: + # Replace can fail before or after publication. Restore only + # when a locked reread proves the original file remains. + if not ready and _read_config(self._config_path) == config: + self._apply_bootstrap_identity(observed, observed_reranker) + if old_env is None: + os.environ.pop("TRUEMEMORY_EMBED_MODEL", None) + else: + os.environ["TRUEMEMORY_EMBED_MODEL"] = old_env + self._served_tier = observed.tier + raise + self._last_outcome = "complete" + return True - self._active_worker = None - conn.close() - return success + def _prepare_job(self, conn: sqlite3.Connection, target_tier: str, force: bool, *, backup_path: Path | None = None) -> "tuple[TierJob, ActivationIntent, int] | None": + from truememory.embedding_target import EmbeddingTarget + from truememory.rebuild_source import ensure_rebuild_tracking + from truememory.tier_config import get_tier_config + from truememory.tier_switch.activation import ( + commit_config_only_transition, read_activation_state, stage_activation_intent, + ) + from truememory.tier_switch.job import read_selected_job_marker, select_tier_job + from truememory.tier_switch.projection import capture_config_guard + from truememory.tier_switch.serving import exclusive_activation + from truememory.vector_search import init_prepared_target_tables + + self._last_outcome = "preparing" + self._last_action = None + if force: + raise TierSwitchUnsupportedError("Active force/reset requires an alternate inactive pair and is not supported") + target = EmbeddingTarget.capture(target_tier) + reranker_id = get_tier_config(target_tier)["reranker"] + with exclusive_activation(cancelled=self._cancel_requested): + state = read_activation_state(conn) + guard = capture_config_guard(config_path=self._config_path, lock_path=self._config_lock_path) + previous = state.selection or state.legacy_policy + if previous is not None: + self._served_tier = previous.target.tier + if previous is not None and not previous.config_acknowledged: + self._last_action = "config_only" if type(state.intent).__name__ == "PolicyIntent" else "reconcile" + if previous.target != target: + raise TierSwitchUnsupportedError("The committed tier must be reconciled before another switch") + self._project_result(conn, previous, state.intent, policy=state.legacy_policy is not None or type(state.intent).__name__ == "PolicyIntent") + return None + old_target, old_tables = self._serving_identity(conn, state, guard) + self._served_tier = old_target.tier + if old_target == target: + self._last_action = "noop" + if previous is not None: + from truememory.tier_switch.runtime import apply_runtime_policy + self._last_outcome = "activation_pending" + apply_runtime_policy(previous) + self._last_outcome = "complete" + return None + if old_target.identity == target.identity and {old_target.tier, target.tier} == {"base", "pro"}: + self._last_action = "config_only" + self._last_outcome = "activation_pending" + self._served_tier = None + result = commit_config_only_transition( + conn, expected_selection_generation=state.selection.generation if state.selection else None, + expected_intent_id=state.intent.intent_id if state.intent else None, target=target, + reranker_id=reranker_id, expected_config=guard, legacy_pair=old_tables, + ) + self._project_result(conn, result, read_activation_state(conn).intent, policy=True) + return None + if set(old_tables).intersection(target.tables) or old_target.tier_group == target.tier_group: + raise TierSwitchUnsupportedError("Rebuilding the serving pair requires an alternate inactive slot") + ok, message = preflight_ram_check(target.tier_group) + if not ok: + raise RuntimeError(message) + marker = read_selected_job_marker(conn) + if marker is not None and marker.state == "selected": + raise TierSwitchUnsupportedError("A selected job still owns the inactive pair; cancel or reconcile it first") + self._check_inactive_target(conn, target) + self._guard_status(conn) + self._last_action = "rebuild" + ensure_rebuild_tracking(conn) + init_prepared_target_tables(conn, target) + status_id = self._create_status_row(conn, target.tier_group, target.tier, "streamed", 0, + backup_path=backup_path) + self._active_status_id = status_id + job = select_tier_job(conn, target, previous_job_id=marker.job_id if marker else None) + self._active_job_id = job.job_id + intent = stage_activation_intent(conn, job, + expected_generation=state.selection.generation if state.selection else None, + reranker_id=reranker_id, expected_config=guard, status_id=status_id) + return job, intent, status_id + + @staticmethod + def _check_inactive_target(conn: sqlite3.Connection, target: object) -> None: + """Refuse incompatible or unreceipted rows before tracking/table writes.""" + from truememory.tier_switch.activation import _guard_pair + from truememory.tier_switch.source import read_tier_progress + + existing = [] + nonempty = False + for name in target.tables: + if conn.execute("SELECT 1 FROM main.sqlite_master WHERE name=? COLLATE NOCASE", (name,)).fetchone(): + _guard_pair(conn, target, tables=(name,)) + existing.append(name) + nonempty |= conn.execute(f'SELECT 1 FROM main."{name}" LIMIT 1').fetchone() is not None + if nonempty: + if len(existing) != 2 or read_tier_progress(conn, model_id=target.model_id, + dimension=target.dimension, targets=target.tables) is None: + raise TierSwitchUnsupportedError("Nonempty inactive vectors require their exact paired receipt") + + @staticmethod + def _serving_identity(conn: sqlite3.Connection, state: "ActivationState", guard: "ConfigGuard") -> tuple: + from truememory.embedding_target import EmbeddingTarget + from truememory import vector_search + + previous = state.selection or state.legacy_policy + if previous is not None: + if guard.tier != previous.target.tier or guard.generation != previous.generation: + raise TierSwitchUnsupportedError("Config and authoritative serving generation disagree") + return previous.target, previous.tables + target = EmbeddingTarget.capture(guard.tier) + if vector_search.EMBEDDING_MODEL != target.model_id or vector_search._embedding_dim != target.dimension: + raise TierSwitchUnsupportedError("Legacy model configuration must be coherent before tier switching") + tables = (vector_search._active_vec_table(conn), vector_search._active_sep_table(conn)) + if tables not in (target.tables, ("vec_messages", "vec_messages_sep")): + raise TierSwitchUnsupportedError("Legacy serving table pair is ambiguous") + require_identity = target.tier in {"base", "pro", "custom"} + if require_identity or conn.execute("SELECT 1 FROM main.messages LIMIT 1").fetchone() is not None: + for key, expected in (("embed_model", target.model_id), ("embed_dim", str(target.dimension))): + row = conn.execute("SELECT typeof(value)='text' AND value=? FROM main.metadata WHERE key=?", (expected, key)).fetchone() + if row is None or row[0] != 1: + raise TierSwitchUnsupportedError("Legacy source has no coherent serving embedding identity") + return target, tables + + def _run_job(self, conn: sqlite3.Connection, job: "TierJob", intent: "ActivationIntent", status_id: int, + progress_callback: "StatusCallback | None" = None) -> bool: + from truememory.tier_switch.activation import read_activation_state + from truememory.tier_switch.job import check_selected_tier_job, end_selected_tier_job + from truememory.tier_switch.source import initialize_tier_source, plan_tier_source + from truememory.tier_switch.serving import exclusive_activation + + check_selected_tier_job(conn, job) + if read_activation_state(conn).intent != intent: + raise TierSwitchUnsupportedError("Staged activation changed before worker admission") + _load_tier_extension(conn) + device = "cpu" if job.target.tier_group == "edge" else _detect_device() + worker = RebuildWorker(conn, job.target.tier, job.target.tier_group, + DynamicThrottler(device=device), status_id, progress_callback) + with self._state_lock: + self._active_worker = worker + if self._cancel_requested.is_set(): + worker.cancel() + expires = time.monotonic() + 9000 + plan = plan_tier_source(conn, model_id=job.target.model_id, dimension=job.target.dimension, targets=job.target.tables) + if plan.action != "resume" and plan.action != "complete": + plan = initialize_tier_source(conn, plan) + result = worker.run_source(job.target, plan, timeout=max(0.001, expires - time.monotonic())) + if (result.status != "complete" or not result.captured_range_complete + or not result.current_source_complete or self._cancel_requested.is_set()): + outcome = "cancelled" if self._cancel_requested.is_set() or result.status == "cancelled" else "failed" + end_selected_tier_job(conn, job, outcome=outcome) + self._terminal_status(conn, status_id, outcome, result.plan.manifest.consumed, result.plan.manifest.total) + self._last_outcome = outcome + return False + with exclusive_activation(deadline=expires, cancelled=self._cancel_requested): + self._finalize_rebuild(conn, job.target.tier, job.target.tier_group, intent=intent, job=job, plan=result.plan) + self._last_outcome = "activation_pending" + self._terminal_status(conn, status_id, "complete", result.plan.manifest.consumed, result.plan.manifest.total) + self._last_outcome = "complete" + return True + + def _project_result(self, conn: sqlite3.Connection, result: "TierSelection | LegacyTierPolicy", + intent: object, *, policy: bool = False) -> None: + from truememory.tier_switch.activation import acknowledge_config, acknowledge_policy_config, LegacyTierPolicy + from truememory.tier_switch.projection import mirror_activation + from truememory.tier_switch.runtime import apply_frozen_selection, apply_runtime_policy + + self._last_outcome = "activation_pending" + self._served_tier = result.target.tier + mirror_activation(result, expected_config=intent.expected_config, config_path=self._config_path, lock_path=self._config_lock_path) + if policy: + apply_runtime_policy(result) + else: + apply_frozen_selection(result) + if type(result) is LegacyTierPolicy: + acknowledge_policy_config(conn, generation=result.generation) + else: + acknowledge_config(conn, generation=result.generation) + self._served_tier = result.target.tier + self._last_outcome = "complete" + + @staticmethod + def _terminal_status(conn: sqlite3.Connection, status_id: int, status: str, processed: int, total: int) -> None: + from truememory.tier_switch.activation import _transaction + with _transaction(conn, write=True): + RebuildManager._guard_status(conn) + conn.execute("UPDATE rebuild_status SET status=?, processed_messages=?, total_messages=?, progress_pct=?, completed_at=? WHERE id=?", + (status, processed, total, 100.0 if status == "complete" else 100.0 * processed / max(1,total), time.time(), status_id)) def get_status(self, status_id: int = 0) -> dict: - """Query rebuild progress from the rebuild_status table.""" + """Read one journal/marker/manifest snapshot, without corpus audits.""" + from truememory.tier_switch.activation import ActivationIntent, _state, _transaction + from truememory.tier_switch.job import read_selected_job_marker + from truememory.tier_switch.source import read_tier_progress + try: - conn = _open_db() + conn = _open_db(self._active_db_path) except Exception: return {"status": "unknown", "error": "cannot open db"} - try: - sid = status_id or self._active_status_id - if not sid: - row = conn.execute( - "SELECT * FROM rebuild_status ORDER BY id DESC LIMIT 1" - ).fetchone() - else: - row = conn.execute( - "SELECT * FROM rebuild_status WHERE id = ?", (sid,) - ).fetchone() - - if not row: - return {"status": "no_rebuild_found"} - - cols = [ - "id", "tier_group", "target_tier", "status", "action", - "total_messages", "processed_messages", "progress_pct", - "eta_seconds", "batch_size", "throughput_ips", "ram_pct", - "pressure", "error", "started_at", "completed_at", - "backup_path", "last_heartbeat", - ] - return dict(zip(cols, row)) + with _transaction(conn, write=False): + self._guard_status(conn) + sid = status_id or self._active_status_id + columns = ("id", "tier_group", "target_tier", "status", "action", "total_messages", "processed_messages", + "progress_pct", "eta_seconds", "batch_size", "throughput_ips", "ram_pct", "pressure", "error", + "started_at", "completed_at", "backup_path", "last_heartbeat") + text_columns = {"tier_group", "target_tier", "status", "action", "error", "backup_path"} + fields = ", ".join( + f"CASE WHEN typeof({name})='text' AND length(CAST({name} AS BLOB))<=4096 THEN {name} ELSE NULL END" + if name in text_columns else + f"CASE WHEN typeof({name}) IN ('integer','real') THEN {name} ELSE NULL END" + for name in columns + ) + row = conn.execute(f"SELECT {fields} FROM main.rebuild_status WHERE id=?", (sid,)).fetchone() if sid else conn.execute( + f"SELECT {fields} FROM main.rebuild_status ORDER BY id DESC LIMIT 1").fetchone() + if not row: + return {"status": "no_rebuild_found"} + result = dict(zip(columns, row)) + state = _state(conn) + intent = state.intent + if type(intent) is ActivationIntent and intent.status_id == result["id"]: + if (intent.target.tier, intent.target.tier_group) != (result["target_tier"], result["tier_group"]): + raise TierSwitchUnsupportedError("Status row and frozen intent disagree") + if intent.state != "staged": + result["status"] = "complete" if state.selection.config_acknowledged else "activation_pending" + result["activated"] = state.selection.config_acknowledged + else: + marker = read_selected_job_marker(conn) + if marker is None or (marker.job_id, marker.target, marker.tracker, marker.source_epoch, marker.source_schema_signature) != ( + intent.job_id, intent.target, intent.tracker, intent.source_epoch, intent.source_schema_signature): + raise TierSwitchUnsupportedError("Status job no longer owns its source progress") + progress = read_tier_progress(conn, model_id=intent.target.model_id, + dimension=intent.target.dimension, targets=intent.target.tables) + if progress is not None: + if (progress.tracker, progress.source_epoch, progress.source_schema_signature) != ( + intent.tracker, intent.source_epoch, intent.source_schema_signature): + raise TierSwitchUnsupportedError("Progress source belongs to another job") + result.update(processed_messages=progress.processed, total_messages=progress.total, + progress_pct=100.0 * progress.processed / max(1,progress.total)) + result["activated"] = False + result["job_id"] = marker.job_id + result["recovery"] = ( + f"If interrupted, run truememory-ingest cancel-rebuild --status-id {result['id']} " + f"--job-id {marker.job_id}, then retry the original tier request explicitly. " + "Use the same database path. A live maintenance owner prevents cancellation." + ) + if marker.state != "selected": + result["status"] = marker.state + elif self._last_outcome == "interrupted" and result["id"] == self._active_status_id: + result["status"] = "interrupted" + elif result["status"] in {"pending", "running"}: + marker = read_selected_job_marker(conn) + result["status"] = "superseded" if intent is not None else "interrupted" + result["activated"] = False + if marker is not None and marker.state == "selected": + result["selected_job_id"] = marker.job_id + result["recovery"] = "This status does not prove selected marker ownership. Do not use its ID with cancel-rebuild; a matching journal/status binding is required." + return result + except Exception as error: + return {"status": "unknown", "error": type(error).__name__} finally: conn.close() - def cancel(self, status_id: int = 0): - """Signal the active worker to stop.""" - with self._state_lock: - worker = self._active_worker - if worker is not None: - worker.cancel() + def cancel(self, status_id: int = 0, *, expected_job_id: str | None = None, + db_path: Path | None = None) -> dict: + """Signal local work, or explicitly cancel one durable interrupted marker.""" + from truememory.maintenance import maintenance_owner + from truememory.tier_switch.activation import ActivationIntent, _transaction, read_activation_state + from truememory.tier_switch.job import _cancel_policy_job_in_writer, read_selected_job_marker - def _rebuild_thread( - self, - target_tier: str, - to_group: str, - messages: list[dict], - is_full: bool, - status_id: int, - backup_path: Path | None, - db_path: Path | None, - ): - """Background thread target — creates its own DB connection.""" - conn = _open_db(db_path) - try: - device = _detect_device() - if to_group == "edge": - device = "cpu" - throttler = DynamicThrottler(device=device) - - worker = RebuildWorker( - conn=conn, - target_tier=target_tier, - target_group=to_group, - throttler=throttler, - status_id=status_id, - ) - with self._state_lock: - self._active_worker = worker + with self._state_lock: + active = self._claimed or self._active_thread is not None or self._active_worker is not None + if active: + if expected_job_id is not None and expected_job_id != self._active_job_id: + raise TierSwitchUnsupportedError("Cancellation job does not identify the active job") + if status_id and status_id != self._active_status_id: + raise TierSwitchUnsupportedError("Cancellation status does not identify the active job") + self._cancel_requested.set() + if self._active_worker is not None: + self._active_worker.cancel() + return {"status": "cancellation_requested", "status_id": self._active_status_id} + path = db_path or self._active_db_path or _get_db_path() + with maintenance_owner(path): + conn = _open_db(path) + try: + state = read_activation_state(conn) + marker = read_selected_job_marker(conn) + if marker is None or marker.state != "selected": + return {"status": "no_selected_job"} + intent = state.intent + bound = (type(intent) is ActivationIntent and intent.state == "staged" + and (intent.job_id, intent.target, intent.tracker, intent.source_epoch, intent.source_schema_signature) + == (marker.job_id, marker.target, marker.tracker, marker.source_epoch, marker.source_schema_signature)) + if expected_job_id != marker.job_id or (status_id and (not bound or intent.status_id != status_id)): + raise TierSwitchUnsupportedError("Interrupted cancellation requires the exact current job_id and matching status") + with _transaction(conn, write=True): + _cancel_policy_job_in_writer(conn, job_id=marker.job_id, target=marker.target, + tracker=marker.tracker, source_epoch=marker.source_epoch, + source_schema_signature=marker.source_schema_signature) + return {"status": "cancelled", "job_id": marker.job_id} + finally: + conn.close() - success, processed = worker.run(messages, is_full) - - if success: - self._finalize_rebuild(conn, target_tier, to_group) - log.info("Background rebuild complete: tier=%s", target_tier) - else: - log.warning("Background rebuild failed: tier=%s", target_tier) + def _rebuild_thread(self, job: "TierJob", intent: "ActivationIntent", status_id: int) -> None: + """Reopen the exact accepted job under worker-owned maintenance.""" + from truememory.tier_switch.job import open_selected_tier_job + try: + with open_selected_tier_job(job) as conn: + self._run_job(conn, job, intent, status_id) except Exception: - log.exception("Background rebuild thread error") + # Do not issue status SQL on a possibly uncertain connection. + if self._last_outcome == "activation_pending": + self._refresh_committed_serving(job.database_path) + else: + self._last_outcome = "interrupted" + log.exception("Background tier activation did not complete") finally: with self._state_lock: self._active_worker = None self._active_thread = None - conn.close() - def _finalize_rebuild( - self, conn: sqlite3.Connection, target_tier: str, to_group: str, - ): - """Apply config changes after a successful rebuild.""" - from truememory.vector_search import ( - _write_embedder_metadata, - set_embedding_model, - ) + def _finalize_rebuild(self, conn: sqlite3.Connection, target_tier: str, to_group: str, *, + intent: "ActivationIntent | None" = None, job: "TierJob | None" = None, + plan: "TierSourcePlan | None" = None) -> None: + """A paired current-source certificate is the only activation authority.""" + from truememory.tier_switch.activation import commit_certified_selection - vec_table = f"vec_messages_{to_group}" - try: - row = conn.execute( - f"SELECT MAX(rowid), COUNT(*) FROM {vec_table}" - ).fetchone() - last_id = row[0] or 0 if row else 0 - vec_count = row[1] or 0 if row else 0 - except sqlite3.OperationalError: - last_id = 0 - vec_count = 0 - - VectorCacheRegistry.set( - conn, - to_group, - model_name=model_name_for_group(to_group), - last_embedded_id=last_id, - vector_count=vec_count, - ) - - set_embedding_model(target_tier) - _write_embedder_metadata(conn) - self._apply_config_switch(target_tier, conn) + if intent is None or job is None or plan is None or (target_tier, to_group) != (job.target.tier, job.target.tier_group): + raise TierSwitchUnsupportedError("Tier activation requires an exact paired completion certificate") + self._last_outcome = "activation_pending" + self._served_tier = None + result = commit_certified_selection(conn, intent, job, plan) + self._project_result(conn, result, intent) def _apply_config_switch( self, target_tier: str, conn: sqlite3.Connection, @@ -414,6 +775,8 @@ def _apply_config_switch( WHOLE read-modify-write so the tier write composes with — instead of racing — other config writers. """ + if conn is not None: + raise TierSwitchUnsupportedError("Legacy config-only helper cannot activate a database tier") import tempfile from truememory.mcp_server import _config_file_lock @@ -458,18 +821,38 @@ def _create_status_row( target_tier: str, action: str, total: int, + *, backup_path: Path | None = None, ) -> int: """Insert a new rebuild_status row and return its id.""" + from truememory.tier_switch.activation import _transaction now = time.time() - cursor = conn.execute( - "INSERT INTO rebuild_status " - "(tier_group, target_tier, status, action, total_messages, " - "started_at, last_heartbeat) " - "VALUES (?, ?, 'running', ?, ?, ?, ?)", - (to_group, target_tier, action, total, now, now), - ) - conn.commit() - return cursor.lastrowid + with _transaction(conn, write=True): + self._guard_status(conn) + cursor = conn.execute( + "INSERT INTO rebuild_status " + "(tier_group, target_tier, status, action, total_messages, started_at, last_heartbeat, backup_path) " + "VALUES (?, ?, 'running', ?, ?, ?, ?, ?)", + (to_group, target_tier, action, total, now, now, str(backup_path) if backup_path is not None else None), + ) + status_id = cursor.lastrowid + return status_id + + @staticmethod + def _guard_status(conn: sqlite3.Connection) -> None: + from truememory.tier_switch.activation import _normalized, _schema_sql + if conn.execute("SELECT 1 FROM temp.sqlite_master WHERE name='rebuild_status' COLLATE NOCASE LIMIT 1").fetchone(): + raise TierSwitchUnsupportedError("TEMP status shadows are unsupported") + expected = """CREATE TABLE rebuild_status ( + id INTEGER PRIMARY KEY AUTOINCREMENT, tier_group TEXT NOT NULL, target_tier TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', action TEXT, total_messages INTEGER DEFAULT 0, + processed_messages INTEGER DEFAULT 0, progress_pct REAL DEFAULT 0, eta_seconds REAL DEFAULT 0, + batch_size INTEGER DEFAULT 0, throughput_ips REAL DEFAULT 0, ram_pct REAL DEFAULT 0, + pressure REAL, error TEXT, started_at REAL, completed_at REAL, backup_path TEXT, last_heartbeat REAL + )""" + if _normalized(_schema_sql(conn, "rebuild_status")) != _normalized(expected): + raise TierSwitchUnsupportedError("Unsupported rebuild status schema") + if conn.execute("SELECT 1 FROM main.sqlite_master WHERE type='trigger' AND tbl_name='rebuild_status' COLLATE NOCASE LIMIT 1").fetchone(): + raise TierSwitchUnsupportedError("Status triggers cannot participate in tier publication") def backup_db(db_path: Path | None = None) -> Path: diff --git a/truememory/tier_switch/projection.py b/truememory/tier_switch/projection.py new file mode 100644 index 00000000..b83ba27d --- /dev/null +++ b/truememory/tier_switch/projection.py @@ -0,0 +1,215 @@ +"""Strict config projection of an already committed database serving decision. + +This is a cooperating-writer CAS, not atomicity across JSON and SQLite. Database +selection remains authoritative after any config write or acknowledgement fault. +""" + +from __future__ import annotations + +import json +import os +import tempfile +import threading +import time +import uuid +from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from truememory.tier_switch.activation import ConfigGuard, LegacyTierPolicy, TierSelection + +CONFIG_WRITE_LOCK = threading.Lock() +_MAX_CONFIG_BYTES = 1024 * 1024 +_GENERATION_KEY = "tier_activation_generation" + + +class ConfigProjectionConflict(RuntimeError): + """Config is unavailable or has changed since the durable intent.""" + + +class ConfigFileLock: + """Shared blocking lock; old writers retain optional best-effort behavior.""" + + def __init__(self, path: Path, *, strict: bool = False) -> None: + self.path = path + self.strict = strict + self._fd = None + + def __enter__(self) -> ConfigFileLock: + try: + self.path.parent.mkdir(parents=True, exist_ok=True) + self._fd = open(self.path, "a+b") + if os.name == "nt": + import msvcrt + self._fd.seek(0) + msvcrt.locking(self._fd.fileno(), msvcrt.LK_LOCK, 1) + else: + import fcntl + fcntl.flock(self._fd, fcntl.LOCK_EX) + except OSError as error: + if self._fd is not None: + try: + self._fd.close() + except OSError: + pass + self._fd = None + if self.strict: + raise ConfigProjectionConflict("Config lock is unavailable") from error + return self + + def __exit__(self, *exc: object) -> bool: + if self._fd is None: + return False + try: + if os.name == "nt": + import msvcrt + self._fd.seek(0) + try: + msvcrt.locking(self._fd.fileno(), msvcrt.LK_UNLCK, 1) + except OSError: + if self.strict: + raise + else: + import fcntl + fcntl.flock(self._fd, fcntl.LOCK_UN) + finally: + try: + self._fd.close() + except OSError: + if self.strict: + raise + finally: + self._fd = None + return False + + +def _paths(config_path: Path | None, lock_path: Path | None) -> tuple[Path, Path]: + config = Path.home() / ".truememory" / "config.json" if config_path is None else Path(config_path) + return config, config.with_name(config.name + ".lock") if lock_path is None else Path(lock_path) + + +def _unique(pairs: list[tuple[str, object]]) -> dict: + value = dict(pairs) + if len(value) != len(pairs): + raise ConfigProjectionConflict("Duplicate config field") + return value + + +def _read_config(path: Path) -> dict: + try: + with path.open("rb") as stream: + raw = stream.read(_MAX_CONFIG_BYTES + 1) + except FileNotFoundError: + return {} + if len(raw) > _MAX_CONFIG_BYTES: + raise ConfigProjectionConflict("Config exceeds the projection byte bound") + try: + value = json.loads(raw.decode("utf-8-sig"), object_pairs_hook=_unique, + parse_constant=lambda _: (_ for _ in ()).throw(ValueError("Nonfinite config"))) + except (ValueError, UnicodeError, RecursionError) as error: + raise ConfigProjectionConflict("Config is not a bounded JSON object") from error + if type(value) is not dict: + raise ConfigProjectionConflict("Config is not a JSON object") + return value + + +def _guard(config: dict) -> ConfigGuard: + from truememory.tier_switch.activation import ConfigGuard, TierActivationError + + try: + return ConfigGuard("tier" in config, config.get("tier", "edge"), config.get(_GENERATION_KEY)) + except TierActivationError as error: + raise ConfigProjectionConflict("Config has an invalid tier or generation") from error + + +def _replace(src: str, dst: str) -> None: + for attempt in range(10): + try: + os.replace(src, dst) + return + except PermissionError: + if attempt == 9: + raise + time.sleep(0.03) + + +def _write_config(path: Path, config: dict) -> None: + try: + raw = (json.dumps(config, indent=2, ensure_ascii=True, allow_nan=False) + "\n").encode("utf-8") + except (ValueError, TypeError, RecursionError) as error: + raise ConfigProjectionConflict("Config fields are not serializable") from error + if len(raw) > _MAX_CONFIG_BYTES: + raise ConfigProjectionConflict("Config exceeds the projection byte bound") + path.parent.mkdir(parents=True, exist_ok=True) + fd, temporary = tempfile.mkstemp(prefix=".config.tmp.", suffix=".json", dir=str(path.parent)) + try: + with os.fdopen(fd, "wb") as stream: + stream.write(raw) + stream.flush() + os.fsync(stream.fileno()) + os.chmod(temporary, 0o600) + _replace(temporary, str(path)) + except BaseException: + try: + os.unlink(temporary) + except OSError: + pass + raise + + +def capture_config_guard(*, config_path: Path | None = None, + lock_path: Path | None = None) -> ConfigGuard: + """Capture actual prior tier and seed a generation without changing policy.""" + config_path, lock_path = _paths(config_path, lock_path) + with ConfigFileLock(lock_path, strict=True), CONFIG_WRITE_LOCK: + config = _read_config(config_path) + guard = _guard(config) + if guard.generation is None: + config[_GENERATION_KEY] = uuid.uuid4().hex + _write_config(config_path, config) + guard = _guard(config) + return guard + + +def patch_config_fields(fields: dict, *, config_path: Path | None = None, + lock_path: Path | None = None) -> dict: + """Merge independent settings under the same lock as activation projection.""" + if type(fields) is not dict or any(type(key) is not str for key in fields): + raise ConfigProjectionConflict("Config patch must be an object with string keys") + if {"tier", _GENERATION_KEY}.intersection(fields): + raise ConfigProjectionConflict("Policy fields require a committed activation") + config_path, lock_path = _paths(config_path, lock_path) + with ConfigFileLock(lock_path, strict=True), CONFIG_WRITE_LOCK: + config = _read_config(config_path) + config.update(fields) + _write_config(config_path, config) + return config.copy() + + +def mirror_activation(result: TierSelection | LegacyTierPolicy, *, + expected_config: ConfigGuard | None, + config_path: Path | None = None, lock_path: Path | None = None) -> ConfigGuard: + """CAS the tier/generation only; UNKNOWN old intents cannot authorize a write. + + An already-applied exact result is idempotent. Other settings come from the + fresh locked file. This does not reconcile native runtime or acknowledge DB. + """ + from truememory.tier_switch.activation import ConfigGuard, LegacyTierPolicy, TierSelection + + if type(result) not in (TierSelection, LegacyTierPolicy): + raise ConfigProjectionConflict("A typed committed serving result is required") + if expected_config is not None and type(expected_config) is not ConfigGuard: + raise ConfigProjectionConflict("Invalid expected config guard") + config_path, lock_path = _paths(config_path, lock_path) + with ConfigFileLock(lock_path, strict=True), CONFIG_WRITE_LOCK: + config = _read_config(config_path) + actual = _guard(config) + desired = ConfigGuard(True, result.target.tier, result.generation) + if actual == desired: + return actual + if expected_config is None or actual != expected_config: + raise ConfigProjectionConflict("Config changed since the activation intent") + config["tier"] = result.target.tier + config[_GENERATION_KEY] = result.generation + _write_config(config_path, config) + return desired diff --git a/truememory/tier_switch/runtime.py b/truememory/tier_switch/runtime.py new file mode 100644 index 00000000..2cdf3402 --- /dev/null +++ b/truememory/tier_switch/runtime.py @@ -0,0 +1,827 @@ +"""Process-local projection and operation snapshots of durable tier selection. + +Database selection is authoritative. This module never writes configuration, +selection metadata or vector rows, and stores no per-database model cache. +""" + +from __future__ import annotations + +import os +import re +import sqlite3 +import sys +import threading +import time +from collections.abc import Callable, Iterator +from contextlib import contextmanager +from dataclasses import dataclass, field +from functools import wraps +from typing import TYPE_CHECKING, ParamSpec, TypeVar + +from truememory.tier_switch import serving +from truememory.tier_switch.activation import TierSelection, _guard_storage, _state, read_activation_state + +if TYPE_CHECKING: + from truememory.tier_switch.activation import LegacyTierPolicy + +P = ParamSpec("P") +R = TypeVar("R") + + +class TierRuntimeError(RuntimeError): + """The process cannot safely serve the captured database selection.""" + + +class ConnectionWriteLock: + """Identify Engine-owned SQL without claiming ownership of caller SQL.""" + + def __init__(self, engine: object) -> None: + import weakref + self._engine = weakref.ref(engine) + self._lock = threading.Lock() + self._owner: tuple[int, object, bool] | None = None + + def acquire(self, blocking: bool = True, timeout: float = -1) -> bool: + acquired = self._lock.acquire(blocking, timeout) + if not acquired: + return False + try: + engine = self._engine() + conn = None if engine is None else engine.conn + try: + borrowed = conn is not None and conn.in_transaction + except sqlite3.ProgrammingError as exc: + if str(exc) != "Cannot operate on a closed database.": + raise + borrowed = False + self._owner = (threading.get_ident(), conn, borrowed) + except BaseException: + self._lock.release() + raise + return True + + def owns_other_transaction(self, conn: sqlite3.Connection) -> bool: + owner = self._owner + return (owner is not None and owner[0] != threading.get_ident() + and owner[1] is conn and not owner[2]) + + def owned_by_current_thread(self) -> bool: + owner = self._owner + return owner is not None and owner[0] == threading.get_ident() + + def release(self) -> None: + owner = self._owner + if owner is None or owner[0] != threading.get_ident(): + raise RuntimeError("Only the Engine writer owner can release its lock") + self._owner = None + self._lock.release() + + def locked(self) -> bool: + return self._lock.locked() + + def __enter__(self) -> ConnectionWriteLock: + self.acquire() + return self + + def __exit__(self, *exc: object) -> None: + self.release() + + +_local = threading.local() +_acknowledged: tuple[str, ...] | None = None +_pid = os.getpid() + + +def _after_fork() -> None: + global _local, _acknowledged, _pid + _local = threading.local() + _acknowledged = None + _pid = os.getpid() + + +if hasattr(os, "register_at_fork"): + os.register_at_fork(after_in_child=_after_fork) + + +def _reranker_id(value: str) -> str: + if (type(value) is not str or not 1 <= len(value) <= 512 + or re.fullmatch(r"[\w][\w.\-]*(/[\w][\w.\-]*)?", value) is None): + raise TierRuntimeError("An explicit bounded reranker identity is required") + return value + + +def selection_key(selection: TierSelection, reranker_id: str | None = None) -> tuple[str, ...]: + if type(selection) is not TierSelection: + raise TierRuntimeError("An immutable certified selection is required") + return (selection.generation, selection.source_epoch, selection.target.model_id, + str(selection.target.dimension), selection.target.tier, + selection.reranker_id if reranker_id is None else _reranker_id(reranker_id), + *selection.tables) + + +def _check(deadline: float | None, cancelled: threading.Event | None) -> None: + if cancelled is not None and cancelled.is_set(): + raise serving.ServingLeaseCancelled("Runtime admission was cancelled") + if deadline is not None and time.monotonic() >= deadline: + raise serving.ServingLeaseTimeout("Runtime admission deadline expired") + + +def _remaining(deadline: float | None) -> float | None: + _check(deadline, None) + return None if deadline is None else deadline - time.monotonic() + + +def _modules() -> tuple[object, object]: + from truememory import reranker, vector_search + return vector_search, reranker + + +def _read_selection(conn: sqlite3.Connection) -> TierSelection | None: + # Absence preserves the legacy path without requiring the new journal's + # registry schema or inventing a certificate for existing vectors. + if not conn.execute("SELECT 1 FROM main.sqlite_master WHERE name='metadata' COLLATE NOCASE").fetchone(): + return None + if not conn.execute( + "SELECT 1 FROM main.metadata WHERE key IN ('tier_selected_v1','tier_activation_v1') LIMIT 1" + ).fetchone(): + return None + if conn.in_transaction: + _guard_storage(conn) + return _state(conn).selection + return read_activation_state(conn).selection + + +def _read_policy(conn: sqlite3.Connection) -> LegacyTierPolicy | None: + if not conn.execute("SELECT 1 FROM main.sqlite_master WHERE name='metadata' COLLATE NOCASE").fetchone(): + return None + if not conn.execute("SELECT 1 FROM main.metadata WHERE key='tier_activation_v1'").fetchone(): + return None + if conn.in_transaction: + _guard_storage(conn) + state = _state(conn) + else: + state = read_activation_state(conn) + return getattr(state, "legacy_policy", None) + + +def policy_key(policy: LegacyTierPolicy, reranker_id: str | None = None) -> tuple[str, ...]: + from truememory.tier_switch.activation import LegacyTierPolicy + if type(policy) is not LegacyTierPolicy: + raise TierRuntimeError("Runtime policy requires an immutable journal result") + effective = policy.reranker_id if reranker_id is None else _reranker_id(reranker_id) + return ("policy", policy.generation, policy.target.model_id, str(policy.target.dimension), + policy.target.tier, effective, *policy.tables) + + +def _authority_key(selection: TierSelection | None, policy: LegacyTierPolicy | None) -> tuple[str, ...] | None: + return selection_key(selection) if selection is not None else policy_key(policy) if policy is not None else None + + +def _policy_ready(policy: LegacyTierPolicy, effective: str) -> bool: + vector, reranker = _modules() + return (_acknowledged == policy_key(policy, effective) and _pid == os.getpid() + and vector.EMBEDDING_MODEL == policy.target.model_id and vector._embedding_dim == policy.target.dimension + and vector._frozen_embedding_target is None and vector._runtime_policy_tier == policy.target.tier + and vector._model is not None and reranker._model is not None and reranker._model_certified + and reranker._model_name == effective and reranker._active_tier == policy.target.tier + and reranker._frozen_reranker_id == effective) + + +def apply_runtime_policy(result: TierSelection | LegacyTierPolicy, *, deadline: float | None = None, + cancelled: threading.Event | None = None) -> None: + """Project a same-space policy without encode, probe, or native construction.""" + global _acknowledged, _pid + certified = type(result) is TierSelection + key = selection_key(result) if certified else policy_key(result) + if current_runtime_operation() is not None: + raise TierRuntimeError("Policy projection cannot upgrade a serving operation") + with serving.exclusive_activation(deadline=deadline, cancelled=cancelled): + _acknowledged = None + vector, reranker = _modules() + _check(deadline, cancelled) + vector.apply_embedding_policy(result.target, certified=certified, deadline=deadline) + _check(deadline, cancelled) + reranker.apply_reranker_policy(result.target.tier, result.reranker_id, deadline=deadline, cancelled=cancelled) + _check(deadline, cancelled) + _pid = os.getpid() + if vector._model is not None and reranker._model is not None and reranker._model_certified: + _acknowledged = key + + +def _prepare_legacy_policy(policy: LegacyTierPolicy, effective: str, *, deadline: float | None, + cancelled: threading.Event | None) -> None: + global _acknowledged, _pid + with serving.exclusive_activation(deadline=deadline, cancelled=cancelled): + if _policy_ready(policy, effective): + return + _acknowledged = None + vector, reranker = _modules() + _check(deadline, cancelled) + vector.apply_embedding_policy(policy.target, certified=False, deadline=deadline, load=True) + _check(deadline, cancelled) + reranker.apply_frozen_reranker(policy.target.tier, effective, deadline=deadline, cancelled=cancelled) + _check(deadline, cancelled) + _pid, _acknowledged = os.getpid(), policy_key(policy, effective) + + +def _legacy_key(reranker_id: str | None = None) -> tuple[str, ...]: + vector, reranker = _modules() + return ("legacy", vector.EMBEDDING_MODEL, str(vector._embedding_dim), + vector.resolve_tier(), reranker.get_current_reranker_name() if reranker_id is None else reranker_id) + + +def _ready(selection: TierSelection, reranker_id: str) -> bool: + vector, reranker = _modules() + return (_pid == os.getpid() and _acknowledged == selection_key(selection, reranker_id) + and vector._frozen_embedding_target == selection.target + and vector.EMBEDDING_MODEL == selection.target.model_id + and vector._embedding_dim == selection.target.dimension + and vector._model is not None and reranker._model is not None + and reranker._model_certified + and reranker._model_name == reranker_id + and reranker._frozen_reranker_id == reranker_id + and reranker._active_tier == selection.target.tier) + + +def runtime_acknowledgement() -> tuple[str, ...] | None: + """Process-local readiness only; a config acknowledgement is unrelated.""" + return _acknowledged if _pid == os.getpid() else None + + +def apply_frozen_selection( + selection: TierSelection, *, deadline: float | None = None, + cancelled: threading.Event | None = None, reranker_id: str | None = None, +) -> None: + """Project outside SQL; the caller may already own exclusive activation. + + Failure after database publication leaves activation pending. No previous + process acknowledgement survives partial projection. This function has no + connection argument; its caller must establish the clean-SQL precondition. + """ + global _acknowledged, _pid + effective = selection.reranker_id if reranker_id is None else _reranker_id(reranker_id) + key = selection_key(selection, effective) + if getattr(_local, "operation", None) is not None: + raise TierRuntimeError("Runtime projection cannot upgrade a serving operation") + with serving.exclusive_activation(deadline=deadline, cancelled=cancelled): + _check(deadline, cancelled) + if _ready(selection, effective): + return + _acknowledged = None + vector, reranker = _modules() + _check(deadline, cancelled) + vector.apply_frozen_embedding_target(selection.target, timeout=_remaining(deadline)) + _check(deadline, cancelled) + reranker.apply_frozen_reranker(selection.target.tier, effective, deadline=deadline, cancelled=cancelled) + _check(deadline, cancelled) + _pid = os.getpid() + _acknowledged = key + + +@dataclass(frozen=True, repr=False) +class ServingOperation: + selection: TierSelection | None + tables: tuple[str, str] + tier: str + reranker_id: str + key: tuple[str, ...] = field(repr=False) + _connection: sqlite3.Connection = field(repr=False, compare=False) + _lease: serving.OperationLease = field(repr=False, compare=False) + _database: tuple[str, object] = field(repr=False, compare=False) + borrowed: bool = False + policy: LegacyTierPolicy | None = None + _connection_lock: object | None = field(default=None, repr=False, compare=False) + _defer_maintenance: bool = field(default=False, repr=False, compare=False) + + def fork_child(self) -> RuntimeChild: + if current_operation(self._connection) is not self: + raise TierRuntimeError("Only the admitted operation can reserve a runtime child") + return RuntimeChild(self, _local.lease.fork_child()) + + +@dataclass(frozen=True, repr=False) +class RuntimeChild: + _operation: ServingOperation + _reservation: serving.ChildReservation + + @contextmanager + def join(self, conn: sqlite3.Connection | None = None, *, deadline: float | None = None, + cancelled: threading.Event | None = None) -> Iterator[ServingOperation]: + if getattr(_local, "operation", None) is not None: + raise TierRuntimeError("Runtime child must enter on an unbound thread") + with self._reservation.join(deadline=deadline, cancelled=cancelled) as lease: + original = self._operation + connection = original._connection if conn is None else conn + if connection is not original._connection: + if connection.in_transaction or _database_identity(connection) != original._database: + raise TierRuntimeError("Runtime child requires the captured clean database") + selected = _read_selection(connection) + policy = _read_policy(connection) if selected is None else None + if _authority_key(selected, policy) != _authority_key(original.selection, original.policy): + raise TierRuntimeError("Runtime child database selection changed") + operation = ServingOperation(original.selection, original.tables, original.tier, + original.reranker_id, original.key, connection, lease, original._database, + original.borrowed, original.policy, + original._connection_lock if connection is original._connection else None, + original._defer_maintenance) + lease.check_admission() + with _bound(operation): + yield operation + + def close(self) -> None: + self._reservation.close() + + +def current_operation(conn: sqlite3.Connection) -> ServingOperation | None: + operation = getattr(_local, "operation", None) + if operation is None: + return None + # A second connection must start its own explicit operation. This avoids + # silently treating a different database or snapshot as the parent's DB. + if operation._connection is not conn: + raise TierRuntimeError("Serving operation belongs to a different connection") + _local.lease.selection_key # validates PID, lifetime and owning thread + return operation + + +def _database_identity(conn: sqlite3.Connection) -> tuple[str, object]: + from truememory.maintenance import connection_database_path + path = connection_database_path(conn) + return ("memory", conn) if path is None else ("file", str(path)) + + +def current_runtime_operation() -> ServingOperation | None: + """Read this thread's admitted public tier/model policy without SQL.""" + operation = getattr(_local, "operation", None) + return None if operation is None else current_operation(operation._connection) + + +@contextmanager +def _bound(operation: ServingOperation, lease: serving.OperationLease | None = None) -> Iterator[None]: + previous = getattr(_local, "operation", None) + previous_lease = getattr(_local, "lease", None) + _local.operation = operation + _local.lease = operation._lease if lease is None else lease + try: + yield + finally: + _local.operation = previous + _local.lease = previous_lease + + +@contextmanager +def _connection_read( + conn: sqlite3.Connection | None, lock: object | None, deadline: float | None, + cancelled: threading.Event | None, *, transaction_owner: object | None = None, + allow_closed: bool = False, +) -> Iterator[None]: + if lock is None: + _check(deadline, cancelled) + yield + return + acquired = False + try: + try: + borrowed = conn is not None and conn.in_transaction + except sqlite3.ProgrammingError as exc: + if not allow_closed or str(exc) != "Cannot operate on a closed database.": + raise + borrowed = False + owner = lock if transaction_owner is None else transaction_owner + if (borrowed and not ( + isinstance(owner, ConnectionWriteLock) and owner.owns_other_transaction(conn))): + _check(deadline, cancelled) + acquired = lock.acquire(blocking=False) + if not acquired: + raise TierRuntimeError("Borrowed connection admission is busy") + else: + while not acquired: + _check(deadline, cancelled) + interval = _remaining(deadline) + if cancelled is not None: + interval = 0.05 if interval is None else min(interval, 0.05) + acquired = lock.acquire() if interval is None else lock.acquire(timeout=min(interval, threading.TIMEOUT_MAX)) + _check(deadline, cancelled) + yield + finally: + if acquired: + lock.release() + + +@contextmanager +def serving_operation( + conn: sqlite3.Connection, *, deadline: float | None = None, + cancelled: threading.Event | None = None, connection_lock: object | None = None, + reranker_id: str | None = None, _defer_maintenance: bool = False, +) -> Iterator[ServingOperation]: + serving._control(None, deadline, cancelled) + if reranker_id is not None: + _reranker_id(reranker_id) + current = current_operation(conn) + if current is not None: + if reranker_id is not None and reranker_id != current.reranker_id: + raise TierRuntimeError("Nested operation requested a different reranker") + with serving.operation_lease(current.key, deadline=deadline, cancelled=cancelled) as lease: + with _bound(current, lease): + yield current + return + # Engine serializes this short snapshot with its connection writer. The + # writer is released before gate admission or any model preparation. + with _connection_read(conn, connection_lock, deadline, cancelled): + selection = _read_selection(conn) + policy = _read_policy(conn) if selection is None else None + borrowed = conn.in_transaction + if selection is not None: + effective = selection.reranker_id if reranker_id is None else reranker_id + if not _ready(selection, effective): + if borrowed: + raise TierRuntimeError("Runtime reconciliation requires a clean connection") + apply_frozen_selection(selection, deadline=deadline, cancelled=cancelled, reranker_id=effective) + key = selection_key(selection, effective) + elif policy is not None: + effective = policy.reranker_id if reranker_id is None else reranker_id + if not _policy_ready(policy, effective): + if borrowed: + raise TierRuntimeError("Policy reconciliation requires a clean connection") + _prepare_legacy_policy(policy, effective, deadline=deadline, cancelled=cancelled) + key = policy_key(policy, effective) + else: + vector, _ = _modules() + if vector._frozen_embedding_target is not None or getattr(vector, "_runtime_policy_tier", None) is not None: + raise TierRuntimeError("Legacy database requires explicit coherent runtime selection") + if reranker_id is not None: + _, reranker = _modules() + if reranker._model is None or reranker._model_name != reranker_id: + if borrowed: + raise TierRuntimeError("Reranker loading requires a clean connection") + with serving.exclusive_activation(deadline=deadline, cancelled=cancelled): + try: + reranker.get_reranker(model_name=reranker_id, deadline=deadline, cancelled=cancelled) + except Exception as error: + # Legacy retrieval already reports optional reranker unavailability. + raise_if_serving_rejection(error) + key = _legacy_key(reranker_id) + with serving.operation_lease(key, deadline=deadline, cancelled=cancelled, blocking=not borrowed) as lease: + # A competing process can commit selection while this thread waits. + # Refuse rather than loop through unbounded native reconciliation. + with _connection_read(conn, connection_lock, deadline, cancelled): + observed = _read_selection(conn) + observed_policy = _read_policy(conn) if observed is None else None + vector, reranker = _modules() + tables = (selection.tables if selection is not None else policy.tables if policy is not None else + (vector._active_vec_table(conn), vector._active_sep_table(conn))) + database = _database_identity(conn) + if _authority_key(observed, observed_policy) != _authority_key(selection, policy): + raise TierRuntimeError("Database selection changed during runtime admission") + vector, reranker = _modules() + if selection is not None: + if not _ready(selection, effective): + raise TierRuntimeError("Process selection changed during runtime admission") + tables, tier, effective = selection.tables, selection.target.tier, effective + elif policy is not None: + if not _policy_ready(policy, effective): + raise TierRuntimeError("Process policy changed during runtime admission") + tables, tier = policy.tables, policy.target.tier + else: + if key != _legacy_key(reranker_id): + raise TierRuntimeError("Legacy runtime changed during admission") + tier = vector.resolve_tier() + effective = reranker.get_current_reranker_name() if reranker_id is None else reranker_id + operation = ServingOperation(selection, tables, tier, effective, key, conn, lease, + database, borrowed, policy, connection_lock, _defer_maintenance) + lease.check_admission() + with _bound(operation): + yield operation + + +def require_model_load_allowed() -> None: + operation = getattr(_local, "operation", None) + if operation is not None: + if operation.borrowed: + raise TierRuntimeError("Model loading requires a clean connection") + if operation.selection is not None or operation.policy is not None: + raise TierRuntimeError("Controlled model must be prepared before serving admission") + lock = operation._connection_lock + conn = operation._connection + if isinstance(lock, ConnectionWriteLock): + if lock.owns_other_transaction(conn): + return + if not lock.owned_by_current_thread(): + acquired = lock.acquire(blocking=False) + if acquired: + try: + if conn.in_transaction: + raise TierRuntimeError("Model loading requires a clean connection") + finally: + lock.release() + return + # The owner may have changed between the first observation + # and the nonblocking probe. Never use an earlier SQL flag. + if lock.owns_other_transaction(conn): + return + if conn.in_transaction: + raise TierRuntimeError("Model loading requires a clean connection") + + +def database_operation(function: Callable[P, R]) -> Callable[P, R]: + """Wrap a direct connection-first search without changing its arguments.""" + @wraps(function) + def wrapped(conn: sqlite3.Connection, *args: object, **kwargs: object) -> R: + with serving_operation(conn): + return function(conn, *args, **kwargs) + return wrapped + + +def open_engine_for_operation(engine: object) -> None: + """Bound fresh public admission without waiting inside an admitted call.""" + from truememory.maintenance import MaintenanceBusyError, wait_for_automatic_owner + from truememory.storage import DEFAULT_BUSY_TIMEOUT_MS + deadline = time.monotonic() + DEFAULT_BUSY_TIMEOUT_MS / 1000 + if isinstance(engine._write_lock, ConnectionWriteLock) and engine._write_lock.owned_by_current_thread(): + raise MaintenanceBusyError("Fresh public opening requires an unbound writer") + + def retry_allowed(error: MaintenanceBusyError) -> bool: + cached = getattr(error, "_reconnect_engine", None) + if cached is None: + return engine.conn is None + return (cached is engine and engine.conn is not None + and getattr(error, "_reconnect_handle", None) is engine.conn) + + failure = None + if engine.conn is not None: + try: + engine._open_connection_handle() + return + except MaintenanceBusyError as error: + if getattr(error, "_reconnect_engine", None) is not engine or not retry_allowed(error): + raise + failure = error + + def outside_admission() -> None: + lock = engine._write_lock + owned = (lock.owned_by_current_thread() if isinstance(lock, ConnectionWriteLock) + else lock.locked()) + try: + admitted = getattr(_local, "operation", None) is not None or serving.current_thread_admitted() + except (TierRuntimeError, serving.ServingLeaseError): + admitted = True + if owned or admitted: + raise MaintenanceBusyError("Fresh public opening requires an unbound caller") + + while True: + if failure is not None: + outside_admission() + if time.monotonic() >= deadline: + raise failure + if not retry_allowed(failure): + raise failure + wait_for_automatic_owner(failure, deadline=deadline) + outside_admission() + if not retry_allowed(failure): + raise failure + try: + engine._open_connection_handle() + return + except MaintenanceBusyError as error: + failure = error + if not retry_allowed(error): + raise + + +def _legacy_initialization_is_local(conn: sqlite3.Connection, operation: ServingOperation) -> bool: + """A nested initializer may attach to a stable pair, but cannot migrate it.""" + known = ("vec_messages", "vec_messages_sep", "vec_messages_edge", + "vec_messages_sep_edge", "vec_messages_basepro", "vec_messages_sep_basepro", + "vec_messages_custom", "vec_messages_sep_custom") + if any(table not in known for table in operation.tables): + raise TierRuntimeError("Nested initialization requires a supported captured pair") + rows = dict(conn.execute( + "SELECT name, CASE WHEN length(CAST(sql AS BLOB)) <= 16384 THEN sql END " + "FROM sqlite_master WHERE type='table' AND name IN (?,?,?,?,?,?,?,?)", known).fetchall()) + if not any(table in rows for table in operation.tables) and operation.tables == known[:2]: + return False + for table in operation.tables: + sql = rows.get(table) + dimension = re.search(r"float\[(\d+)\]", sql or "") + if (not sql or not dimension or str(int(dimension.group(1))) != operation.key[2] + or "distance_metric=cosine" not in sql.replace(" ", "").replace("'", "").lower() + or conn.execute("SELECT 1 FROM sqlite_master WHERE name IN (?,?)", + (table + "_cos_stage", table + "_cos_stage_done")).fetchone()): + raise TierRuntimeError("Nested initialization requires completed vector migration") + if operation.tables == known[:2] and conn.execute("SELECT 1 FROM vec_messages LIMIT 1").fetchone(): + raise TierRuntimeError("Initialize legacy vector tables before entering a serving operation") + if conn.execute("SELECT 1 FROM sqlite_master WHERE name='metadata' AND type='table'").fetchone(): + metadata = dict(conn.execute( + "SELECT key, CASE WHEN length(CAST(value AS BLOB)) <= 512 THEN value END " + "FROM metadata WHERE key IN ('embed_model','embed_dim')").fetchall()) + if (("embed_model" in metadata and metadata["embed_model"] != operation.key[1]) + or ("embed_dim" in metadata and metadata["embed_dim"] != str(operation.key[2]))): + raise TierRuntimeError("Nested initialization requires the captured embedding identity") + return True + + +@contextmanager +def engine_serving_operation( + engine: object, *, reranker_override: str | None = None, + _suppress_maintenance: bool = False, +) -> Iterator[ServingOperation]: + """Finish cold initialization before capturing tables for the public body.""" + open_engine_for_operation(engine) + parent = current_operation(engine.conn) + owns_scope = parent is None + cold = not (getattr(engine, "_runtime_initialized", True) or getattr(engine, "ready", False)) + initialized = False + try: + if cold: + with serving_operation(engine.conn, connection_lock=engine._write_lock, + reranker_id=reranker_override, _defer_maintenance=owns_scope) as preparing: + engine._initialize_connection(_suppress_maintenance=True, _preparing=owns_scope) + initialized = True + if parent is not None and current_operation(engine.conn) is not preparing: + raise TierRuntimeError("Initialization changed the captured serving operation") + with serving_operation(engine.conn, connection_lock=engine._write_lock, + reranker_id=reranker_override, _defer_maintenance=owns_scope) as operation: + yield operation + finally: + if owns_scope and not _suppress_maintenance and (initialized or not cold): + if initialized: + engine._maybe_startup_consolidate() + else: + engine._maybe_auto_consolidate() + + +def engine_handle_operation(function: Callable[P, R]) -> Callable[P, R]: + """Pin a handle-only operation without vector initialization or scheduling.""" + @wraps(function) + def wrapped(engine: object, *args: object, **kwargs: object) -> R: + open_engine_for_operation(engine) + with serving_operation(engine.conn, connection_lock=engine._write_lock): + return function(engine, *args, **kwargs) + return wrapped + + +def engine_operation(function: Callable[P, R]) -> Callable[P, R]: + """Initialize before pinning the public call's immutable table selection.""" + @wraps(function) + def wrapped(engine: object, *args: object, **kwargs: object) -> R: + with engine_serving_operation(engine): + return function(engine, *args, **kwargs) + return wrapped + + +def raise_if_serving_rejection(error: BaseException) -> None: + """Prevent identity/admission refusals from becoming availability fallbacks.""" + classes: list[type[BaseException]] = [TierRuntimeError, serving.ServingLeaseError, serving.ServingLeaseTimeout] + names = { + "truememory.tier_switch.writer": ("WriterSelectionChanged",), + "truememory.maintenance": ("MaintenanceBusyError",), + "truememory.tier_switch.activation": ("TierActivationError",), + "truememory.rebuild_source": ("RebuildSourceChanged",), + "truememory.vector_search": ("VectorPublicationChanged",), + "truememory.embedding_target": ("EmbeddingTargetError",), + "truememory.model_client": ("ProtocolMismatchError", "ModelServerBusyError", "ServingIdentityMismatchError"), + } + for module_name, attributes in names.items(): + module = sys.modules.get(module_name) + if module is not None: + for attribute in attributes: + value = getattr(module, attribute, None) + if isinstance(value, type) and issubclass(value, BaseException): + classes.append(value) + if isinstance(error, tuple(classes)): + raise error + + +def require_legacy_vector_mutation(conn: sqlite3.Connection) -> None: + """Refuse legacy vector DDL/metadata mutation on a selected database.""" + if _read_selection(conn) is not None or _read_policy(conn) is not None: + raise TierRuntimeError("Controlled vector storage requires certified publication") + + +@contextmanager +def fact_operation(memory: object) -> Iterator[ServingOperation | None]: + """Pin gate, dedup and storage for one fact; extraction remains outside.""" + engine = getattr(memory, "_engine", None) + if engine is None or not callable(getattr(engine, "_open_connection_handle", None)): + # Third-party source-only Memory doubles retain their legacy contract. + yield None + return + with engine_serving_operation(engine) as operation: + yield operation + + +@contextmanager +def maintenance_serving_operation( + conn: sqlite3.Connection, *, cancelled: threading.Event | None = None, + deadline: float | None = None, connection_lock: object | None = None, +) -> Iterator[bool]: + """Called only under maintenance ownership; controlled derived vectors defer.""" + serving._control(None, deadline, cancelled) + _check(deadline, cancelled) + current = current_operation(conn) + if current is not None: + if current.selection is not None or current.policy is not None: + yield False + return + else: + with _connection_read(conn, connection_lock, deadline, cancelled): + controlled = _read_selection(conn) is not None or _read_policy(conn) is not None + if controlled: + yield False + return + with serving_operation(conn, cancelled=cancelled, deadline=deadline, connection_lock=connection_lock): + yield True + + +@contextmanager +def destructive_ingest_operation(engine: object) -> Iterator[None]: + """Reject controlled databases before the legacy destructive import path.""" + from pathlib import Path + from truememory.maintenance import maintenance_owner + path = Path(engine.db_path) + with maintenance_owner(None if str(path) == ":memory:" else path), serving.exclusive_activation(): + conn = engine.conn + owned = False + try: + if conn is None and str(path) != ":memory:" and path.exists(): + conn = sqlite3.connect(path.resolve().as_uri() + "?mode=ro", uri=True) + owned = True + if conn is not None: + if conn.in_transaction: + raise TierRuntimeError("Destructive ingestion requires a clean connection") + if _read_selection(conn) is not None or conn.execute( + "SELECT 1 FROM main.sqlite_master WHERE type='table' AND name='metadata' COLLATE NOCASE" + ).fetchone() and conn.execute( + "SELECT 1 FROM main.metadata WHERE key='tier_activation_v1' LIMIT 1" + ).fetchone(): + raise TierRuntimeError("Controlled databases cannot use destructive ingestion") + finally: + if owned: + conn.close() + yield + + +def open_serving_connection(db_path: str | os.PathLike[str], + legacy_factory: Callable[[object], sqlite3.Connection]) -> sqlite3.Connection: + """Resolve one database identity before owning its absence/schema decision.""" + from pathlib import Path + if str(db_path) == ":memory:": + return legacy_factory(db_path) + return _open_serving_connection_at_path(Path(db_path).resolve(), legacy_factory) + + +def _open_serving_connection_at_path( + path: os.PathLike[str], legacy_factory: Callable[[object], sqlite3.Connection], +) -> sqlite3.Connection: + """Open a canonical path; an Engine caller already owns maintenance. + + Same-thread owner reentry does not reacquire the process/file lock. The + caller must pass the same resolved Path used for its outer ownership. + """ + from truememory.maintenance import maintenance_owner + if str(path) == ":memory:": + return legacy_factory(path) + from truememory.storage import DEFAULT_BUSY_TIMEOUT_MS + with maintenance_owner(path): + if not path.exists(): + return legacy_factory(path) + conn = sqlite3.connect(path.as_uri() + "?mode=rw", uri=True, check_same_thread=False) + try: + present = conn.execute( + "SELECT 1 FROM main.sqlite_master WHERE name='metadata' COLLATE NOCASE" + ).fetchone() + controlled = present and conn.execute( + "SELECT 1 FROM main.metadata WHERE key IN ('tier_selected_v1','tier_activation_v1') LIMIT 1" + ).fetchone() + if controlled: + read_activation_state(conn) + for sql in (f"PRAGMA busy_timeout={DEFAULT_BUSY_TIMEOUT_MS}", "PRAGMA foreign_keys=ON", + "PRAGMA synchronous=NORMAL", "PRAGMA cache_size=-64000", "PRAGMA mmap_size=268435456"): + conn.execute(sql) + return conn + except BaseException: + conn.close() + raise + conn.close() + return legacy_factory(path) + + +@contextmanager +def legacy_vector_mutation(conn: sqlite3.Connection) -> Iterator[None]: + """Hold the existing nonblocking maintenance owner across legacy mutation. + + Legacy initialization can already hold a serving read. Owner contention + refuses immediately instead of waiting or upgrading that read lease. + Caller SQL is neither committed nor rolled back by this admission helper. + """ + from truememory.maintenance import connection_database_path, maintenance_owner + with maintenance_owner(connection_database_path(conn)): + require_legacy_vector_mutation(conn) + yield + + +@contextmanager +def legacy_rebuild_operation(conn: sqlite3.Connection) -> Iterator[None]: + """Legacy rebuilds own maintenance before serving or model work.""" + with legacy_vector_mutation(conn), serving_operation(conn): + yield diff --git a/truememory/tier_switch/sensors.py b/truememory/tier_switch/sensors.py index 4d5a2202..20d9dbd2 100644 --- a/truememory/tier_switch/sensors.py +++ b/truememory/tier_switch/sensors.py @@ -6,11 +6,15 @@ """ import logging +import math import subprocess +import sys import time log = logging.getLogger(__name__) +THERMAL_PROBE_TIMEOUT = 0.25 + def read_mps_memory(cap_gb: float) -> dict: """Read MPS driver-allocated memory and classify against cap.""" @@ -18,14 +22,17 @@ def read_mps_memory(cap_gb: float) -> dict: import torch if not (hasattr(torch.backends, "mps") and torch.backends.mps.is_available()): - return {"used_gb": 0.0, "ratio": 0.0, "status": "ok"} + return {"used_gb": None, "ratio": None, "status": "unknown"} used_bytes = torch.mps.driver_allocated_memory() used_gb = used_bytes / (1024**3) except (ImportError, AttributeError, RuntimeError): - return {"used_gb": 0.0, "ratio": 0.0, "status": "ok"} + return {"used_gb": None, "ratio": None, "status": "unknown"} + + if not math.isfinite(used_gb) or used_gb < 0 or not math.isfinite(cap_gb) or cap_gb <= 0: + return {"used_gb": None, "ratio": None, "status": "unknown"} - ratio = used_gb / cap_gb if cap_gb > 0 else 0.0 + ratio = used_gb / cap_gb if ratio >= 0.95: status = "critical" @@ -43,20 +50,23 @@ class GrowthRateTracker: def __init__(self, cap_gb: float): self._cap_gb = cap_gb self._prev_gb: float = 0.0 - self._prev_time: float = 0.0 + self._prev_time: float | None = None def update(self, current_gb: float) -> dict: """Record a new reading and return growth rate status.""" - now = time.time() + now = time.monotonic() - if self._prev_time == 0.0: + if not math.isfinite(current_gb) or current_gb < 0: + return {"slope_gb_per_20s": None, "slope_pct": None, "status": "unknown"} + + if self._prev_time is None: self._prev_gb = current_gb self._prev_time = now - return {"slope_gb_per_20s": 0.0, "slope_pct": 0.0, "status": "ok"} + return {"slope_gb_per_20s": None, "slope_pct": None, "status": "unknown"} dt = now - self._prev_time if dt <= 0: - return {"slope_gb_per_20s": 0.0, "slope_pct": 0.0, "status": "ok"} + return {"slope_gb_per_20s": None, "slope_pct": None, "status": "unknown"} raw_slope = (current_gb - self._prev_gb) / dt slope_20s = raw_slope * 20.0 @@ -76,29 +86,50 @@ def update(self, current_gb: float) -> dict: def read_thermal_pressure() -> dict: - """Read macOS thermal pressure via pmset.""" + """Read macOS thermal pressure without treating missing data as healthy.""" + if sys.platform != "darwin": + return {"scheduler_limit": None, "status": "unsupported", "required": False} try: proc = subprocess.run( ["pmset", "-g", "therm"], capture_output=True, text=True, - timeout=5, + timeout=THERMAL_PROBE_TIMEOUT, ) - output = proc.stdout - limit = 100 - for line in output.splitlines(): - if "CPU_Scheduler_Limit" in line or "CPU_Speed_Limit" in line: - parts = line.split("=") - if len(parts) >= 2: - limit = int(parts[-1].strip()) - break - except (subprocess.TimeoutExpired, FileNotFoundError, OSError, ValueError): - return {"scheduler_limit": 100, "status": "ok"} - + except (subprocess.TimeoutExpired, FileNotFoundError, OSError): + return {"scheduler_limit": None, "status": "unknown"} + + if proc.returncode != 0: + return {"scheduler_limit": None, "status": "unknown"} + limits: list[int] = [] + invalid_field = False + for line in proc.stdout.splitlines(): + if "CPU_Scheduler_Limit" in line or "CPU_Speed_Limit" in line: + parts = line.split("=") + if len(parts) != 2: + invalid_field = True + continue + try: + limit = int(parts[1].strip()) + except ValueError: + invalid_field = True + continue + if not 0 <= limit <= 100: + invalid_field = True + continue + limits.append(limit) + + if not limits: + return {"scheduler_limit": None, "status": "unknown"} + limit = min(limits) + + # Incomplete telemetry cannot establish health or erase known throttling. if limit <= 70: status = "critical" elif limit < 100: status = "warning" + elif invalid_field: + return {"scheduler_limit": None, "status": "unknown"} else: status = "ok" diff --git a/truememory/tier_switch/serving.py b/truememory/tier_switch/serving.py new file mode 100644 index 00000000..dacf7a77 --- /dev/null +++ b/truememory/tier_switch/serving.py @@ -0,0 +1,438 @@ +"""Process-local serving admission; no model, database or caller integration. + +Readers and reserved children pin one caller-supplied selection key. Exclusive +activation drains those registrations. This gate does not validate a database +generation or apply a selection, and is not a cross-process publication lock. +""" + +from __future__ import annotations + +import math +import os +import threading +import time +import weakref +from collections import deque +from collections.abc import Iterator +from contextlib import AbstractContextManager, contextmanager +from dataclasses import dataclass + +SelectionKey = tuple[str, ...] +_CANCEL_POLL_SECONDS = 0.05 + + +class ServingLeaseError(RuntimeError): + """Serving admission or an opaque token's lifecycle is invalid.""" + + +class ServingLeaseTimeout(TimeoutError): + """The serving admission deadline expired.""" + + +class ServingLeaseCancelled(ServingLeaseError): + """Serving admission was cancelled before work could start.""" + + +@dataclass(frozen=True) +class _Control: + deadline: float | None + events: tuple[threading.Event, ...] + + +def _control(timeout: float | None, deadline: float | None, cancelled: threading.Event | None) -> _Control: + for value in (timeout, deadline): + try: + if value is not None and (type(value) not in (int, float) or not math.isfinite(value)): + raise ServingLeaseError("Serving admission requires a finite time bound") + except OverflowError: + raise ServingLeaseError("Serving admission requires a finite time bound") from None + if timeout is not None and timeout < 0: + raise ServingLeaseError("Serving admission timeout must be nonnegative") + if cancelled is not None and type(cancelled) is not threading.Event: + raise ServingLeaseError("Serving cancellation requires a threading Event") + if timeout is not None: + relative = time.monotonic() + timeout + deadline = relative if deadline is None else min(relative, deadline) + return _Control(deadline, () if cancelled is None else (cancelled,)) + + +def _combine(parent: _Control, child: _Control) -> _Control: + bounds = tuple(value for value in (parent.deadline, child.deadline) if value is not None) + events = parent.events + tuple(event for event in child.events if all(event is not old for old in parent.events)) + return _Control(min(bounds) if bounds else None, events) + + +def _check(control: _Control) -> None: + if any(threading.Event.is_set(event) for event in control.events): + raise ServingLeaseCancelled("Serving admission was cancelled") + if control.deadline is not None and time.monotonic() >= control.deadline: + raise ServingLeaseTimeout("Serving admission deadline expired") + + +def _wait_seconds(control: _Control) -> float | None: + seconds = None if control.deadline is None else max(0.0, control.deadline - time.monotonic()) + if control.events: + seconds = _CANCEL_POLL_SECONDS if seconds is None else min(seconds, _CANCEL_POLL_SECONDS) + return None if seconds is None else min(seconds, threading.TIMEOUT_MAX) + + +def _selection_key(value: SelectionKey) -> SelectionKey: + # Exact built-in types exclude user equality/hash callbacks under the lock. + if (type(value) is not tuple or not 1 <= len(value) <= 8 + or any(type(part) is not str or not 1 <= len(part) <= 512 for part in value)): + raise ServingLeaseError("Serving selection requires a bounded immutable key") + return value + + +class _OpaqueToken: + __slots__ = ("_gate", "_closed", "__weakref__") + + def __new__(cls) -> _OpaqueToken: + raise ServingLeaseError("Serving tokens are issued only by their gate") + + def __setattr__(self, name: str, value: object) -> None: + raise ServingLeaseError("Serving tokens are opaque") + + def __repr__(self) -> str: + return "" + + +class OperationLease(_OpaqueToken): + __slots__ = () + + @property + def selection_key(self) -> SelectionKey: + return _issuer(self)._lease_key(self) + + def fork_child(self) -> ChildReservation: + """Reserve one child now; close it if the task never enters join().""" + return _issuer(self)._fork_child(self) + + def check_admission(self) -> None: + """Recheck inherited controls before a caller's deferred body admission.""" + _issuer(self)._check_lease_admission(self) + + +class ChildReservation(_OpaqueToken): + __slots__ = () + + @contextmanager + def join( + self, *, timeout: float | None = None, deadline: float | None = None, + cancelled: threading.Event | None = None, + ) -> Iterator[OperationLease]: + """Consume once on admission; an unadmitted reservation still needs close().""" + gate = _issuer(self) + with gate._join_child(self, _control(timeout, deadline, cancelled)) as lease: + yield lease + + def close(self) -> None: + """Release an unused reservation; never release a running joined child.""" + _issuer(self)._close_child(self) + + +def _issuer(token: _OpaqueToken) -> ServingGate: + gate = getattr(token, "_gate", None) + if type(token) not in (OperationLease, ChildReservation) or type(gate) is not ServingGate: + raise ServingLeaseError("Invalid serving admission token") + gate._check_process() + return gate + + +@dataclass(frozen=True) +class _Reader: + thread: threading.Thread + key: SelectionKey + control: _Control + writer_owned: bool + reservation: ChildReservation | None = None + + +@dataclass +class _Reserved: + key: SelectionKey + control: _Control + joined: OperationLease | None = None + + +class ServingGate: + """Concurrent readers, FIFO exclusive writers, and explicit pinned children. + + An explicit gate is invalid after fork. Module-level functions receive a new + child-process gate through register_at_fork; inherited tokens remain invalid. + """ + + def __init__(self) -> None: + self._pid = os.getpid() + self._condition = threading.Condition(threading.Lock()) + self._local = threading.local() + self._readers: dict[OperationLease, _Reader] = {} + self._children: dict[ChildReservation, _Reserved] = {} + self._retired_children: weakref.WeakSet[ChildReservation] = weakref.WeakSet() + self._writers: dict[object, threading.Thread] = {} + self._waiting_writers: deque[object] = deque() + + def _check_process(self) -> None: + if os.getpid() != self._pid: + raise ServingLeaseError("Serving state cannot be inherited by another process") + + @contextmanager + def _locked(self, control: _Control | None = None, *, blocking: bool = True) -> Iterator[None]: + self._check_process() + acquired = False + try: + while not acquired: + if not blocking: + if control is not None: + _check(control) + acquired = self._condition.acquire(blocking=False) + if not acquired: + raise ServingLeaseError("Serving admission is busy") + elif control is None: + acquired = self._condition.acquire() + else: + _check(control) + seconds = _wait_seconds(control) + acquired = self._condition.acquire() if seconds is None else self._condition.acquire(timeout=seconds) + self._check_process() + if control is not None: + _check(control) + yield + finally: + if acquired: + self._condition.release() + + def _token(self, kind: type[OperationLease] | type[ChildReservation]) -> OperationLease | ChildReservation: + token = object.__new__(kind) + object.__setattr__(token, "_gate", self) + object.__setattr__(token, "_closed", False) + return token + + def _thread_reader(self, thread: threading.Thread) -> _Reader | None: + if thread is not threading.current_thread(): + raise ServingLeaseError("Serving thread state belongs to another thread") + readers = getattr(self._local, "readers", ()) + return readers[-1] if readers else None + + def _writer_thread(self) -> threading.Thread | None: + return next(iter(self._writers.values()), None) + + def current_thread_admitted(self) -> bool: + """Inspect nesting without waiting behind another gate operation.""" + with self._locked(blocking=False): + thread = threading.current_thread() + return self._thread_reader(thread) is not None or self._writer_thread() is thread + + def _check_key(self, key: SelectionKey) -> None: + current = next((reader.key for reader in self._readers.values()), None) + if current is None: + current = next((child.key for child in self._children.values()), None) + if current is not None and current != key: + raise ServingLeaseError("Serving selection differs from an admitted operation") + + def _owned_reader(self, lease: OperationLease) -> _Reader: + if type(lease) is not OperationLease or getattr(lease, "_gate", None) is not self: + raise ServingLeaseError("Invalid serving operation lease") + reader = self._readers.get(lease) + if reader is None or reader.thread is not threading.current_thread(): + raise ServingLeaseError("Serving operation lease is closed or belongs to another thread") + return reader + + def _register_reader(self, lease: OperationLease, reader: _Reader) -> None: + readers = (*getattr(self._local, "readers", ()), reader) + self._readers[lease] = reader + self._local.readers = readers + + def _lease_key(self, lease: OperationLease) -> SelectionKey: + with self._locked(): + return self._owned_reader(lease).key + + def _check_lease_admission(self, lease: OperationLease) -> None: + self._check_process() + control = self._owned_reader(lease).control + with self._locked(control): + _check(self._owned_reader(lease).control) + + def _release_reader(self, lease: OperationLease) -> None: + with self._locked(): + reader = self._readers.get(lease) + if reader is not None: + self._owned_reader(lease) + del self._readers[lease] + self._local.readers = tuple(item for item in self._local.readers if item is not reader) + if reader.reservation is not None: + del self._children[reader.reservation] + self._retired_children.add(reader.reservation) + object.__setattr__(reader.reservation, "_closed", True) + object.__setattr__(lease, "_closed", True) + self._condition.notify_all() + + @contextmanager + def operation_lease( + self, selection_key: SelectionKey, *, timeout: float | None = None, + deadline: float | None = None, cancelled: threading.Event | None = None, + blocking: bool = True, + ) -> Iterator[OperationLease]: + if type(blocking) is not bool: + raise ServingLeaseError("Serving admission blocking must be boolean") + key, control = _selection_key(selection_key), _control(timeout, deadline, cancelled) + lease = self._token(OperationLease) + thread = threading.current_thread() + self._check_process() + parent = self._thread_reader(thread) + if parent is not None: + control = _combine(parent.control, control) + entered = False + try: + with self._locked(control, blocking=blocking): + entered = True + while True: + _check(control) + self._check_key(key) + writer = self._writer_thread() + if writer is thread or (writer is None and (parent is not None or not self._waiting_writers)): + self._register_reader(lease, _Reader(thread, key, control, writer is thread or bool(parent and parent.writer_owned))) + break + if not blocking: + raise ServingLeaseError("Serving admission is busy") + self._condition.wait(_wait_seconds(control)) + yield lease + finally: + if entered: + self._release_reader(lease) + + def _fork_child(self, lease: OperationLease) -> ChildReservation: + self._check_process() + # The owner's record is immutable and cannot be released by another + # thread. Revalidate membership after acquiring the condition as well. + control = self._owned_reader(lease).control + with self._locked(control): + reader = self._owned_reader(lease) + _check(reader.control) + if reader.writer_owned: + raise ServingLeaseError("Writer-owned reads cannot grant cross-thread admission") + child = self._token(ChildReservation) + self._children[child] = _Reserved(reader.key, reader.control) + return child + + def _close_child(self, child: ChildReservation) -> None: + with self._locked(): + if type(child) is not ChildReservation or getattr(child, "_gate", None) is not self: + raise ServingLeaseError("Invalid serving child reservation") + reserved = self._children.get(child) + if reserved is None: + if child not in self._retired_children: + raise ServingLeaseError("Invalid serving child reservation") + return + if reserved.joined is not None: + raise ServingLeaseError("A joined child must release its own operation lease") + del self._children[child] + self._retired_children.add(child) + object.__setattr__(child, "_closed", True) + self._condition.notify_all() + + @contextmanager + def _join_child(self, child: ChildReservation, control: _Control) -> Iterator[OperationLease]: + self._check_process() + if type(child) is not ChildReservation or getattr(child, "_gate", None) is not self: + raise ServingLeaseError("Invalid serving child reservation") + # Snapshot immutable inherited bounds before mutex admission. The + # reservation is revalidated under the condition before consumption. + snapshot = self._children.get(child) + if snapshot is None: + raise ServingLeaseError("Serving child reservation is closed or already joined") + control = _combine(snapshot.control, control) + thread = threading.current_thread() + parent = self._thread_reader(thread) + if parent is not None: + control = _combine(parent.control, control) + lease = self._token(OperationLease) + entered = False + try: + with self._locked(control): + entered = True + reserved = self._children.get(child) + if reserved is None or reserved.joined is not None: + raise ServingLeaseError("Serving child reservation is closed or already joined") + control = _combine(reserved.control, control) + _check(control) + self._check_key(reserved.key) + if self._writers: + raise ServingLeaseError("A reserved child cannot inherit writer ownership") + self._register_reader(lease, _Reader(thread, reserved.key, control, False, child)) + reserved.joined = lease + yield lease + finally: + if entered: + self._release_reader(lease) + + @contextmanager + def exclusive_activation( + self, *, timeout: float | None = None, deadline: float | None = None, + cancelled: threading.Event | None = None, + ) -> Iterator[None]: + control = _control(timeout, deadline, cancelled) + ticket = object() + thread = threading.current_thread() + entered = False + try: + with self._locked(control): + entered = True + if self._writer_thread() is thread: + self._writers[ticket] = thread + else: + if self._thread_reader(thread) is not None: + raise ServingLeaseError("A serving read lease cannot be upgraded to activation") + self._waiting_writers.append(ticket) + while True: + _check(control) + if (not self._writers and not self._readers and not self._children + and self._waiting_writers[0] is ticket): + self._waiting_writers.popleft() + self._writers[ticket] = thread + break + self._condition.wait(_wait_seconds(control)) + yield + finally: + if entered: + with self._locked(): + owner = self._writers.get(ticket) + if owner is not None and owner is not threading.current_thread(): + raise ServingLeaseError("Activation lease belongs to another thread") + self._writers.pop(ticket, None) + if ticket in self._waiting_writers: + self._waiting_writers.remove(ticket) + self._condition.notify_all() + + +_DEFAULT_GATE = ServingGate() + + +def _after_fork() -> None: + global _DEFAULT_GATE + _DEFAULT_GATE = ServingGate() + + +if hasattr(os, "register_at_fork"): + os.register_at_fork(after_in_child=_after_fork) + + +def operation_lease( + selection_key: SelectionKey, *, timeout: float | None = None, deadline: float | None = None, + cancelled: threading.Event | None = None, blocking: bool = True, +) -> AbstractContextManager[OperationLease]: + """Acquire the process-wide serving boundary for one logical operation.""" + return _DEFAULT_GATE.operation_lease(selection_key, timeout=timeout, deadline=deadline, cancelled=cancelled, blocking=blocking) + + +def exclusive_activation( + *, timeout: float | None = None, deadline: float | None = None, + cancelled: threading.Event | None = None, +) -> AbstractContextManager[None]: + """Drain process-wide operations before a caller's activation work.""" + return _DEFAULT_GATE.exclusive_activation(timeout=timeout, deadline=deadline, cancelled=cancelled) + + +def current_thread_admitted() -> bool: + """Report a reader/exclusive scope on this thread, or refuse a busy gate.""" + return _DEFAULT_GATE.current_thread_admitted() diff --git a/truememory/tier_switch/source.py b/truememory/tier_switch/source.py new file mode 100644 index 00000000..03a0ab4e --- /dev/null +++ b/truememory/tier_switch/source.py @@ -0,0 +1,561 @@ +"""Model-free, bounded source checkpoints for caller-owned inactive tier pairs. + +The caller owns the maintenance lease, exclusive cooperating pair writer, target +schema/model validation, and database identity across reopen. Tracking, metadata, +and both target tables must exist before planning. This adapter neither clears +tables nor activates them. It rejects borrowed transactions and concurrent writes +until the caller replans. Receipts detect changed stored bytes, not a hostile SQL +writer capable of forging both data and receipts. +""" + +import hashlib +import json +import re +import sqlite3 +import struct +from collections.abc import Iterator, Sequence +from contextlib import contextmanager +from dataclasses import asdict, dataclass, replace + +from truememory.maintenance import SourceRevision +from truememory.rebuild_source import RebuildManifest, RebuildSource, RebuildSourceChanged, capture_source, manifest_key + +_PAGE_ROWS = 64 +_MAX_METADATA_BYTES = 65536 +_SEED = hashlib.sha256(b"truememory-tier-pair-chain-v1").hexdigest() +_FIELDS = ("id", "content", "sender", "recipient", "timestamp") +_ID_MIN, _ID_MAX = -(2**63), 2**63 - 1 +_HEX = re.compile(r"[0-9a-f]{64}\Z") +SQLiteValue = str | bytes | int | float | None + + +class TierSourceUntrusted(RebuildSourceChanged): + """A fresh empty pair or explicit revalidation is required.""" + + +@dataclass(frozen=True) +class TierSourcePlan: + action: str + manifest: RebuildManifest + schema: str + digest: str + prefix_count: int + prefix_cursor: int | None + connection: sqlite3.Connection + data_version: int + changes: int + saved_manifest: str | None + saved_receipt: str | None + + +@dataclass(frozen=True) +class TierSourcePage: + generation: str + cursor: int | None + consumed: int + rows: tuple[tuple[SQLiteValue, ...], ...] + + def messages(self) -> list[dict[str, SQLiteValue]]: + """A bounded copy suitable for the existing completion/separation format.""" + return [dict(zip(_FIELDS, row)) for row in self.rows] + + +@dataclass(frozen=True) +class TierSourceCompletion: + plan: TierSourcePlan + captured_range_complete: bool + current_source_complete: bool + + +def _json(value: object) -> str: + return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False) + + +def _hash(value: object) -> str: + return hashlib.sha256(_json(value).encode("utf-8")).hexdigest() + + +def _integer(value: object, *, minimum: int = 0, maximum: int = _ID_MAX) -> bool: + return type(value) is int and minimum <= value <= maximum + + +def _identifier(value: object) -> bool: + return _integer(value, minimum=_ID_MIN) + + +def _quote(name: str) -> str: + return '"' + name.replace('"', '""') + '"' + + +def _identity(model_id: str, dimension: int, targets: tuple[str, str]) -> None: + if (not isinstance(model_id, str) or not model_id or len(model_id) > 4096 + or not _integer(dimension, minimum=1, maximum=4096)): + raise ValueError("A concrete model and integer dimension in 1..4096 are required") + if (type(targets) is not tuple or len(targets) != 2 + or not all(isinstance(name, str) for name in targets)): + raise ValueError("An exact ordered completion/separation pair is required") + match = re.fullmatch(r"vec_messages_([a-z][a-z0-9_]{0,63})", targets[0]) + if match is None or targets[1] != "vec_messages_sep_" + match[1]: + raise ValueError("Target names must be a canonical paired tier group") + + +def _guard_schema(conn: sqlite3.Connection, targets: tuple[str, ...]) -> str: + guarded = (*targets, "messages", "metadata", "maintenance_source_state", "truememory_rebuild_source_v1") + marks = ",".join("?" for _ in guarded) + if conn.execute( + f"SELECT 1 FROM temp.sqlite_master WHERE name COLLATE NOCASE IN ({marks}) OR " + f"(type='trigger' AND tbl_name COLLATE NOCASE IN ({marks})) LIMIT 1", (*guarded, *guarded), + ).fetchone(): + raise TierSourceUntrusted("TEMP shadows or triggers invalidate tier source ownership") + if conn.execute( + "SELECT 1 FROM main.sqlite_master WHERE type='trigger' AND tbl_name COLLATE NOCASE IN (?,?,?) LIMIT 1", + (*targets, "metadata"), + ).fetchone(): + raise TierSourceUntrusted("Tier pair and receipt tables must have no side-effect triggers") + definitions = [] + for name in (*targets, "metadata"): + row = conn.execute("SELECT type, rootpage, sql FROM main.sqlite_master WHERE name=?", (name,)).fetchone() + if row is None or row[0] != "table" or not row[2]: + raise TierSourceUntrusted("Initialized MAIN target and metadata tables are required") + columns = [tuple(item) for item in conn.execute(f"PRAGMA main.table_xinfo({_quote(name)})")] + if name in targets and "embedding" not in {column[1] for column in columns}: + raise TierSourceUntrusted("Target table has no embedding column") + if name == "metadata" and ([column[1] for column in columns if column[5]] != ["key"] + or "value" not in {column[1] for column in columns}): + raise TierSourceUntrusted("Metadata requires a unique key and value column") + definitions.append((name, tuple(row), columns)) + return _hash([conn.execute("PRAGMA main.schema_version").fetchone()[0], definitions]) + + +def _metadata(conn: sqlite3.Connection, key: str) -> str | None: + shape = conn.execute( + "SELECT typeof(value), length(CAST(value AS BLOB)) FROM main.metadata WHERE key=?", (key,), + ).fetchone() + if shape is None: + return None + if shape[0] != "text" or shape[1] > _MAX_METADATA_BYTES: + raise TierSourceUntrusted("Tier source metadata has an unsupported type or size") + return conn.execute("SELECT value FROM main.metadata WHERE key=?", (key,)).fetchone()[0] + + +def _receipt_key(targets: tuple[str, ...]) -> str: + return "tier_pair_receipt_v1:" + ":".join(targets) + + +def _manifest_json(manifest: RebuildManifest) -> str: + return _json(dict(asdict(manifest), version=1)) + + +def _unique_object(pairs: list[tuple[str, object]]) -> dict[str, object]: + result = dict(pairs) + if len(result) != len(pairs): + raise ValueError("Duplicate metadata field") + return result + + +def _parse_manifest(raw: str, model: str, dimension: int, targets: tuple[str, str]) -> RebuildManifest: + try: + value = json.loads(raw, object_pairs_hook=_unique_object) + expected = set(RebuildManifest.__dataclass_fields__) | {"version"} + if (type(value) is not dict or set(value) != expected + or type(value["version"]) is not int or value.pop("version") != 1): + raise ValueError("manifest shape") + source = value["source"] + if type(source) is not dict or set(source) != set(RebuildSource.__dataclass_fields__): + raise ValueError("source shape") + revision = source["revision"] + if (type(revision) is not list or len(revision) != 6 or not isinstance(revision[0], str) + or not revision[0] or not all(_integer(revision[index]) for index in (1, 2, 3, 5)) + or not _identifier(revision[4]) or revision[5] > revision[1] + or revision[2] + revision[3] != revision[1]): + raise ValueError("source revision") + source["revision"] = SourceRevision(*revision) + if (source["tracker"] not in ("canonical-v1", "rebuild-bridge-v1", "rebuild-bridge-conservative-v1") + or not isinstance(source["schema_signature"], str) or not _HEX.fullmatch(source["schema_signature"]) + or not _integer(source["total"])): + raise ValueError("source descriptor") + for field in ("after_id", "high_id"): + if source[field] is not None and not _identifier(source[field]): + raise ValueError("source identifier") + if ((source["total"] == 0) != (source["high_id"] is None) + or (source["high_id"] is not None and source["after_id"] is not None + and source["high_id"] <= source["after_id"])): + raise ValueError("source range") + if (value["model"] != model or type(value["dimension"]) is not int or value["dimension"] != dimension + or value["targets"] != list(targets) or value["input_digest"] is not None + or not isinstance(value["generation"], str) or not re.fullmatch(r"[0-9a-f]{32}", value["generation"]) + or not all(_integer(value[field]) for field in ("total", "consumed", "outputs", "schema_version")) + or value["total"] != source["total"] or value["outputs"] != value["consumed"] + or value["consumed"] > value["total"] or type(value["complete"]) is not bool + or (value["complete"] and value["consumed"] != value["total"])): + raise ValueError("manifest invariant") + cursor = value["cursor"] + if value["consumed"] == 0: + if cursor != source["after_id"] or type(cursor) is not type(source["after_id"]): + raise ValueError("initial cursor") + elif (not _identifier(cursor) or cursor > source["high_id"] + or (source["after_id"] is not None and cursor <= source["after_id"]) + or ((value["consumed"] == value["total"]) != (cursor == source["high_id"]))): + raise ValueError("consumed cursor") + value["source"] = RebuildSource(**source) + value["targets"] = tuple(value["targets"]) + return RebuildManifest(**value) + except (ValueError, TypeError, KeyError, OverflowError, RecursionError) as error: + raise TierSourceUntrusted("Malformed or incompatible tier source manifest") from error + + +def _receipt(plan: TierSourcePlan) -> str: + return _json({"version": 1, "manifest": _hash(json.loads(_manifest_json(plan.manifest))), + "schema": plan.schema, "digest": plan.digest, "prefix_count": plan.prefix_count, + "prefix_cursor": plan.prefix_cursor}) + + +def _parse_receipt(raw: str, manifest: RebuildManifest, schema: str) -> tuple[str, int, int | None]: + try: + value = json.loads(raw, object_pairs_hook=_unique_object) + if (type(value) is not dict or set(value) != {"version", "manifest", "schema", "digest", "prefix_count", "prefix_cursor"} + or type(value["version"]) is not int or value["version"] != 1 + or value["manifest"] != _hash(json.loads(_manifest_json(manifest))) or value["schema"] != schema + or not isinstance(value["digest"], str) or not _HEX.fullmatch(value["digest"]) + or not _integer(value["prefix_count"])): + raise ValueError("receipt invariant") + cursor = value["prefix_cursor"] + if ((cursor is not None and not _identifier(cursor)) or cursor != manifest.source.after_id + or (value["prefix_count"] == 0) != (cursor is None)): + raise ValueError("receipt prefix") + return value["digest"], value["prefix_count"], cursor + except (ValueError, TypeError, KeyError, RecursionError) as error: + raise TierSourceUntrusted("Malformed or incompatible tier pair receipt") from error + + +@dataclass(frozen=True) +class TierSourceProgress: + generation: str + processed: int + total: int + outputs: int + cursor: int | None + complete: bool + tracker: str + source_epoch: str + source_schema_signature: str + + +def read_tier_progress(conn: sqlite3.Connection, *, model_id: str, dimension: int, + targets: tuple[str, str]) -> TierSourceProgress | None: + """Read bounded receipt scalars, not source coverage or current completion. + + A caller may own one read snapshot for journal, selected marker and these + counters, then compare their exact target/tracker/epoch/schema identities. + This neither audits vectors nor checks current source freshness. The + manifest's complete flag certifies only its captured range. + """ + _identity(model_id, dimension, targets) + + def read() -> TierSourceProgress | None: + guarded = ("metadata", *targets) + marks = ",".join("?" for _ in guarded) + if conn.execute( + f"SELECT 1 FROM temp.sqlite_master WHERE name COLLATE NOCASE IN ({marks}) LIMIT 1", guarded, + ).fetchone(): + raise TierSourceUntrusted("TEMP shadows invalidate progress identity") + raw = _metadata(conn, manifest_key(targets)) + receipt = _metadata(conn, _receipt_key(targets)) + if raw is None and receipt is None: + return None + if raw is None or receipt is None: + raise TierSourceUntrusted("Progress requires its paired receipt") + manifest = _parse_manifest(raw, model_id, dimension, targets) + try: + value = json.loads(receipt, object_pairs_hook=_unique_object) + schema = value["schema"] + if type(schema) is not str or not _HEX.fullmatch(schema): + raise ValueError("receipt schema") + except (ValueError, TypeError, KeyError, RecursionError) as error: + raise TierSourceUntrusted("Malformed progress receipt") from error + _parse_receipt(receipt, manifest, schema) + return TierSourceProgress(manifest.generation, manifest.consumed, manifest.total, + manifest.outputs, manifest.cursor, manifest.complete, + manifest.source.tracker, manifest.source.revision.epoch, + manifest.source.schema_signature) + + if conn.in_transaction: + return read() + conn.execute("BEGIN") + try: + result = read() + except BaseException: + if conn.in_transaction: + conn.rollback() + if conn.in_transaction: + raise TierSourceUntrusted("Progress rollback is uncertain") + raise + conn.rollback() + if conn.in_transaction: + raise TierSourceUntrusted("Progress snapshot did not close") + return result + + +def _chain(digest: str, row_id: int, completion: bytes, separation: bytes) -> str: + return hashlib.sha256(bytes.fromhex(digest) + struct.pack(">qQQ", row_id, len(completion), len(separation)) + + completion + separation).hexdigest() + + +def _bounded_rows(cursor: sqlite3.Cursor) -> Iterator[tuple]: + try: + while rows := cursor.fetchmany(_PAGE_ROWS): + yield from rows + finally: + cursor.close() + + +def _audit_pair(conn: sqlite3.Connection, plan: TierSourcePlan) -> None: + """One snapshot, bounded resident rows; called only by plan/resume.""" + captured = plan.manifest.source + lower = " AND id > ?" if captured.after_id is not None else "" + args = (captured.high_id, captured.after_id) if lower else (captured.high_id,) + total, high = conn.execute( + f"SELECT COUNT(*), MAX(id) FROM main.messages WHERE id <= ?{lower}", args, + ).fetchone() + if (total, high) != (captured.total, captured.high_id): + raise TierSourceUntrusted("Captured source bounds do not match the manifest") + width = plan.manifest.dimension * 4 + readers = [_bounded_rows(conn.execute( + f"SELECT rowid, CASE WHEN typeof(embedding)='blob' AND length(embedding)=? THEN embedding END " + f"FROM main.{_quote(table)} ORDER BY rowid", (width,), + )) for table in plan.manifest.targets] + high = plan.manifest.cursor + source = _bounded_rows(conn.execute( + "SELECT id FROM main.messages WHERE id <= ? ORDER BY id", (high,), + )) + readers.append(source) + digest, count = _SEED, 0 + try: + while True: + left, right, original = (next(reader, None) for reader in readers) + if left is None and right is None and original is None: + break + if (left is None or right is None or original is None or left[0] != right[0] or left[0] != original[0] + or not _identifier(left[0]) or type(left[1]) is not bytes or type(right[1]) is not bytes + or len(left[1]) != width or len(right[1]) != width): + raise TierSourceUntrusted("Tier pair bytes or source coverage do not match the receipt") + digest = _chain(digest, left[0], left[1], right[1]) + count += 1 + finally: + for reader in readers: + reader.close() + if digest != plan.digest or count != plan.prefix_count + plan.manifest.outputs: + raise TierSourceUntrusted("Tier pair digest or consumed count changed") + + +def _fence(conn: sqlite3.Connection, plan: TierSourcePlan) -> None: + if (conn is not plan.connection or conn.total_changes != plan.changes + or conn.execute("PRAGMA main.data_version").fetchone()[0] != plan.data_version): + raise TierSourceUntrusted("Connection changed; explicitly replan and revalidate the tier pair") + + +@contextmanager +def _transaction(conn: sqlite3.Connection, *, write: bool = False) -> Iterator[None]: + if conn.in_transaction: + raise TierSourceUntrusted("Tier source work requires an owned, clean connection") + conn.execute("BEGIN IMMEDIATE" if write else "BEGIN") + try: + yield + conn.commit() + except BaseException: + conn.rollback() + raise + + +def _check(conn: sqlite3.Connection, plan: TierSourcePlan) -> None: + _fence(conn, plan) + if _guard_schema(conn, plan.manifest.targets) != plan.schema: + raise TierSourceUntrusted("Tier target schema changed") + if (_metadata(conn, manifest_key(plan.manifest.targets)) != plan.saved_manifest + or _metadata(conn, _receipt_key(plan.manifest.targets)) != plan.saved_receipt): + raise TierSourceUntrusted("Tier generation or receipt changed") + plan.manifest.source.check(conn) + + +def plan_tier_source(conn: sqlite3.Connection, *, model_id: str, dimension: int, + targets: tuple[str, str], force: bool = False) -> TierSourcePlan: + """Plan full, resume, delta or complete without changing persistent state. + + A full plan can only initialize an already empty inactive pair. Existing + unreceipted rows are refused, even with force. Snapshot consistency and + exclusive cooperating writer ownership are required throughout this call. + """ + _identity(model_id, dimension, targets) + version, changes = conn.execute("PRAGMA main.data_version").fetchone()[0], conn.total_changes + with _transaction(conn): + schema = _guard_schema(conn, targets) + saved = _metadata(conn, manifest_key(targets)) + receipt = _metadata(conn, _receipt_key(targets)) + if force or (saved is None and receipt is None): + if any(conn.execute(f"SELECT 1 FROM main.{_quote(table)} LIMIT 1").fetchone() for table in targets): + raise TierSourceUntrusted("Full initialization requires a separately owned empty inactive pair") + source = capture_source(conn) + manifest = replace(RebuildManifest.new(model_id, dimension, targets, source=source), + schema_version=conn.execute("PRAGMA main.schema_version").fetchone()[0]) + plan = TierSourcePlan("full", manifest, schema, _SEED, 0, None, conn, version, changes, saved, receipt) + else: + if saved is None or receipt is None: + raise TierSourceUntrusted("Both manifest and pair receipt are required for compatibility") + manifest = _parse_manifest(saved, model_id, dimension, targets) + if manifest.schema_version != conn.execute("PRAGMA main.schema_version").fetchone()[0]: + raise TierSourceUntrusted("Manifest schema generation changed") + manifest.source.check(conn) + digest, prefix, cursor = _parse_receipt(receipt, manifest, schema) + plan = TierSourcePlan("resume", manifest, schema, digest, prefix, cursor, conn, version, changes, saved, receipt) + _audit_pair(conn, plan) + if manifest.complete: + source = capture_source(conn, after_id=manifest.cursor) + if source.total: + updated = replace(RebuildManifest.new(model_id, dimension, targets, source=source), + schema_version=manifest.schema_version) + plan = replace(plan, action="delta", manifest=updated, + prefix_count=prefix + manifest.outputs, prefix_cursor=manifest.cursor) + else: + plan = replace(plan, action="complete") + _fence(conn, plan) + _fence(conn, plan) + return plan + + +def _persist(conn: sqlite3.Connection, plan: TierSourcePlan) -> TierSourcePlan: + saved, receipt = _manifest_json(plan.manifest), _receipt(plan) + for key, value in ((manifest_key(plan.manifest.targets), saved), (_receipt_key(plan.manifest.targets), receipt)): + conn.execute("INSERT OR REPLACE INTO main.metadata(key,value) VALUES (?,?)", (key, value)) + return replace(plan, saved_manifest=saved, saved_receipt=receipt, changes=conn.total_changes) + + +def initialize_tier_source(conn: sqlite3.Connection, plan: TierSourcePlan) -> TierSourcePlan: + """Persist a planned generation; never create or clear target tables.""" + with _transaction(conn, write=True): + _check(conn, plan) + if plan.action not in ("full", "delta"): + return plan + if plan.action == "full" and any( + conn.execute(f"SELECT 1 FROM main.{_quote(table)} LIMIT 1").fetchone() for table in plan.manifest.targets + ): + raise TierSourceUntrusted("Full target pair is no longer empty") + updated = _persist(conn, replace(plan, action="resume")) + return updated + + +def _read_rows(conn: sqlite3.Connection, plan: TierSourcePlan, size: int) -> tuple[tuple[SQLiteValue, ...], ...]: + source = plan.manifest.source + if source.high_id is None: + return () + lower = " AND id > ?" if plan.manifest.cursor is not None else "" + args = (source.high_id, plan.manifest.cursor, size) if lower else (source.high_id, size) + return tuple(tuple(row) for row in conn.execute( + f"SELECT {', '.join(_FIELDS)} FROM main.messages WHERE id <= ?{lower} ORDER BY id LIMIT ?", args, + )) + + +def _same_rows(left: tuple[tuple[SQLiteValue, ...], ...], right: tuple[tuple[SQLiteValue, ...], ...]) -> bool: + if len(left) != len(right): + return False + for current, pending in zip(left, right): + if len(current) != len(pending): + return False + for actual, captured in zip(current, pending): + if type(actual) is not type(captured): + return False + if isinstance(actual, float): + if struct.pack(">d", actual) != struct.pack(">d", captured): + return False + elif actual != captured: + return False + return True + + +def read_tier_page(conn: sqlite3.Connection, plan: TierSourcePlan, *, size: int = _PAGE_ROWS) -> TierSourcePage: + """Release the read snapshot before returning any texts to the encoder.""" + if not _integer(size, minimum=1): + raise ValueError("Page size must be a positive integer") + if plan.action not in ("resume", "complete"): + raise TierSourceUntrusted("Initialize the generation before reading") + with _transaction(conn): + _check(conn, plan) + rows = _read_rows(conn, plan, size) + if len(rows) != min(size, plan.manifest.total - plan.manifest.consumed): + raise TierSourceUntrusted("Source range no longer matches its captured count") + _fence(conn, plan) + return TierSourcePage(plan.manifest.generation, plan.manifest.cursor, plan.manifest.consumed, rows) + + +def publish_tier_prefix(conn: sqlite3.Connection, plan: TierSourcePlan, page: TierSourcePage, *, + completion: Sequence[bytes], separation: Sequence[bytes]) -> tuple[TierSourcePlan, TierSourcePage]: + """Atomically publish only an encoded page prefix and retain its pending suffix. + + The caller supplies already serialized vectors from the captured model. This + function checks byte widths, but does not infer model identity from vectors. + On failure the original immutable plan/page remain the retry checkpoint; + after database write failures replan because total_changes includes rollbacks. + """ + count = len(completion) + if (plan.action != "resume" or plan.manifest.complete or page.generation != plan.manifest.generation + or page.cursor != plan.manifest.cursor or page.consumed != plan.manifest.consumed + or count < 1 or count != len(separation) or count > len(page.rows)): + raise TierSourceUntrusted("Encoded prefix does not belong to the current pending page") + width = plan.manifest.dimension * 4 + if any(type(blob) is not bytes or len(blob) != width for vectors in (completion, separation) for blob in vectors): + raise ValueError("Both serialized vector widths must match the captured dimension") + digest = plan.digest + with _transaction(conn, write=True): + _check(conn, plan) + rows = _read_rows(conn, plan, len(page.rows)) + if not _same_rows(rows, page.rows): + raise TierSourceUntrusted("Pending page does not match the source under writer ownership") + for row, left, right in zip(rows[:count], completion, separation): + for table, vector in zip(plan.manifest.targets, (left, right)): + conn.execute(f"INSERT INTO main.{_quote(table)}(rowid,embedding) VALUES (?,?)", (row[0], vector)) + digest = _chain(digest, row[0], left, right) + manifest = plan.manifest.advance(rows[count - 1][0], count, count) + updated = _persist(conn, replace(plan, manifest=manifest, digest=digest)) + pending = TierSourcePage(manifest.generation, manifest.cursor, manifest.consumed, page.rows[count:]) + return updated, pending + + +def finish_tier_source(conn: sqlite3.Connection, plan: TierSourcePlan) -> TierSourceCompletion: + """Certify captured coverage only; no registry, model or serving activation.""" + if plan.action not in ("resume", "complete") or plan.manifest.consumed != plan.manifest.total: + raise TierSourceUntrusted("The captured source range still has pending inputs") + with _transaction(conn, write=True): + _check(conn, plan) + current = capture_source(conn) + covers_current = (current.total == plan.prefix_count + plan.manifest.outputs + and current.high_id == plan.manifest.cursor) + updated = _persist(conn, replace(plan, action="complete", manifest=replace(plan.manifest, complete=True))) + return TierSourceCompletion(updated, True, covers_current) + + +def require_current_completion_in_writer(conn: sqlite3.Connection, plan: TierSourcePlan) -> None: + """Read-only final certification inside an owned BEGIN IMMEDIATE. + + sqlite3 cannot expose transaction lock kind: the caller must hold the writer + and cooperative pair ownership. Unlike finish, this never persists or ends + a transaction. One bounded-memory full pair audit occurs at activation, + not after every page. Strict native table/model validation remains caller + responsibility; the adapter attests stored bytes and source coverage. + """ + if not conn.in_transaction: + raise TierSourceUntrusted("Current completion requires caller writer ownership") + if (plan.action != "complete" or not plan.manifest.complete + or plan.manifest.consumed != plan.manifest.total + or plan.saved_manifest is None or plan.saved_receipt is None): + raise TierSourceUntrusted("A finished paired generation is required") + _check(conn, plan) + parsed = _parse_manifest(plan.saved_manifest, plan.manifest.model, + plan.manifest.dimension, plan.manifest.targets) + if parsed != plan.manifest or _parse_receipt(plan.saved_receipt, parsed, plan.schema) != ( + plan.digest, plan.prefix_count, plan.prefix_cursor, + ): + raise TierSourceUntrusted("Completion does not match its persisted paired certificate") + current = capture_source(conn) + if (current.total != plan.prefix_count + plan.manifest.outputs + or current.high_id != plan.manifest.cursor): + raise TierSourceUntrusted("Captured pair does not cover the current source") + _audit_pair(conn, plan) diff --git a/truememory/tier_switch/state_machine.py b/truememory/tier_switch/state_machine.py index f195cf0d..03bb76a0 100644 --- a/truememory/tier_switch/state_machine.py +++ b/truememory/tier_switch/state_machine.py @@ -32,8 +32,16 @@ def __init__(self, start_batch: int, max_batch: int, ramp_step: int): self.state = self.PROBING self.good_streak = 0 self.batch_count = 0 - self.last_ramp_time = 0.0 - self.last_backoff_time = 0.0 + self.last_ramp_time = -float("inf") + self.last_backoff_time = -float("inf") + + @staticmethod + def all_required_channels_ok(readings: dict) -> bool: + """Unsupported channels are explicit; no evidence must not enable ramping.""" + required = [readings.get(channel, {}) for channel in + ("mps_level", "growth_rate", "thermal") + if readings.get(channel, {}).get("required", True)] + return bool(required) and all(reading.get("status") == "ok" for reading in required) def on_batch_complete(self): """Call after every batch. Increments batch counter.""" @@ -63,13 +71,16 @@ def safety_check(self, readings: dict) -> int: if readings.get(channel, {}).get("status") == "warning": return self._do_step_down(channel, readings[channel]) - self.good_streak += 1 + if self.all_required_channels_ok(readings): + self.good_streak += 1 + else: + self.good_streak = 0 return self.batch_size def should_ramp_check(self) -> bool: """Returns True if conditions are met to consider a ramp-up.""" if self.state == self.BACKOFF: - if time.time() - self.last_backoff_time >= self.BACKOFF_COOLDOWN: + if time.monotonic() - self.last_backoff_time >= self.BACKOFF_COOLDOWN: self.state = self.PROBING log.info("Backoff cooldown expired, re-entering PROBING") else: @@ -88,9 +99,9 @@ def should_ramp_check(self) -> bool: return False if self.good_streak < self.GOOD_WINDOWS_REQUIRED: return False - if time.time() - self.last_ramp_time < self.RAMP_COOLDOWN: + if time.monotonic() - self.last_ramp_time < self.RAMP_COOLDOWN: return False - if time.time() - self.last_backoff_time < self.BACKOFF_COOLDOWN: + if time.monotonic() - self.last_backoff_time < self.BACKOFF_COOLDOWN: return False return True @@ -105,14 +116,13 @@ def ramp_up(self, triple_sample_means: dict) -> int: Returns: Updated batch_size (may or may not have increased) """ - for channel in ("mps_level", "growth_rate", "thermal"): - if triple_sample_means.get(channel, {}).get("status") != "ok": - self.good_streak = 0 - return self.batch_size + if not self.all_required_channels_ok(triple_sample_means): + self.good_streak = 0 + return self.batch_size old = self.batch_size self.batch_size = min(self.max_batch, self.batch_size + self.ramp_step) - self.last_ramp_time = time.time() + self.last_ramp_time = time.monotonic() self.good_streak = 0 log.info("Ramp-up: %d → %d", old, self.batch_size) return self.batch_size @@ -134,7 +144,7 @@ def _do_backoff(self, channel: str, reading: dict) -> int: self.batch_size = max(1, self.batch_size // 2) self.state = self.BACKOFF self.good_streak = 0 - self.last_backoff_time = time.time() + self.last_backoff_time = time.monotonic() log.warning( "BACKOFF: %d → %d (CRITICAL on %s)", old, self.batch_size, channel ) diff --git a/truememory/tier_switch/throttler.py b/truememory/tier_switch/throttler.py index ded01cf6..e9033bfa 100644 --- a/truememory/tier_switch/throttler.py +++ b/truememory/tier_switch/throttler.py @@ -7,7 +7,11 @@ import gc import logging +import sys +import threading import time +from collections import deque +from typing import TYPE_CHECKING import psutil @@ -18,33 +22,38 @@ ) from truememory.tier_switch.state_machine import ThrottlerStateMachine +if TYPE_CHECKING: + from truememory.mps_utils import MPSMemoryBudget + log = logging.getLogger(__name__) _MACHINE_PROFILES = { - # (min_gb, max_gb): (ratio, start, max_batch, ramp_step) - (0, 12): (0.50, 1, 4, 1), - (12, 20): (0.50, 1, 8, 1), - (20, 30): (0.50, 1, 12, 2), - (30, 1024): (0.55, 1, 16, 2), + # (min_gb, max_gb): (start, max_batch, ramp_step). RAM sizes batches only. + (0, 12): (1, 4, 1), + (12, 20): (1, 8, 1), + (20, 30): (1, 12, 2), + (30, 1024): (1, 16, 2), } -def _get_profile(total_gb: float) -> tuple[float, int, int, int]: +def _get_profile(total_gb: float) -> tuple[int, int, int]: for (lo, hi), profile in _MACHINE_PROFILES.items(): if lo <= total_gb < hi: return profile - return (0.50, 1, 12, 2) + return (1, 12, 2) class DynamicThrottler: """Adaptive 3-channel throttler with state machine.""" + SAMPLE_SPACING = 10.0 + SAMPLE_MAX_AGE = 30.0 + def __init__(self, device: str = "cpu"): self.device = device self.total_gb = psutil.virtual_memory().total / (1024**3) - ratio, start, max_batch, ramp_step = _get_profile(self.total_gb) - self.mps_cap_gb = self.total_gb * ratio + start, max_batch, ramp_step = _get_profile(self.total_gb) self.state_machine = ThrottlerStateMachine( start_batch=start, @@ -52,59 +61,150 @@ def __init__(self, device: str = "cpu"): ramp_step=ramp_step, ) - self.growth_tracker = GrowthRateTracker(cap_gb=self.mps_cap_gb) + self.growth_tracker: GrowthRateTracker | None = None + self._growth_cap_gb: float | None = None + self._state_lock = threading.RLock() + self._sample_lock = threading.Lock() + self._samples: deque[tuple[float, dict]] = deque(maxlen=3) + self._last_sample_time: float | None = None + self._fault_generation = 0 self.items_processed = 0 - self.start_time = time.time() + self.start_time = time.monotonic() self.batch_times: list[float] = [] self.last_throttle_time = 0.0 # backward compat: worker sets this on OOM self._last_readings: dict = {} log.info( - "Throttler init: device=%s total_ram=%.0fGB mps_cap=%.1fGB " + "Throttler init: device=%s total_ram=%.0fGiB " "start=%d max=%d step=%d", - device, self.total_gb, self.mps_cap_gb, + device, self.total_gb, start, max_batch, ramp_step, ) + def _budget_snapshot(self) -> "MPSMemoryBudget | None": + # Reading telemetry must never initialize a local allocator for a proxy + # or race model setup. The loader publishes only after the setter works. + # A shared daemon can have CPU embedding and MPS reranking (or the + # reverse). A configured budget describes the process's MPS allocator, + # regardless of which device this admission's model currently uses. + from truememory.mps_utils import get_mps_memory_budget + return get_mps_memory_budget() + + @property + def adaptive_applicable(self) -> bool: + """Whether this process has an applicable MPS or thermal policy.""" + # Darwin remains applicable before model loading publishes its budget, + # including CPU fallback while the other model can still use MPS. + return self.device == "mps" or sys.platform == "darwin" or self._budget_snapshot() is not None + + @property + def mps_cap_gb(self) -> float | None: + with self._state_lock: + budget = self._budget_snapshot() + return budget.effective_bytes / (1024**3) if budget and budget.effective_bytes else None + @property def batch_size(self) -> int: - return self.state_machine.batch_size + with self._state_lock: + return self.state_machine.batch_size @batch_size.setter def batch_size(self, value: int): - self.state_machine.batch_size = value + with self._state_lock: + self.state_machine.batch_size = value def before_batch(self) -> tuple[int, dict]: - """Check sensors, update state machine, return (batch_size, metrics).""" - self.state_machine.on_batch_complete() - + """Use bounded observations; never wait for a three-sample ramp window.""" + with self._state_lock: + self.state_machine.on_batch_complete() + self._expire_samples(time.monotonic()) + self._sample_if_due() + + # Retain duty-cycle pacing, outside both locks. Ramping no longer adds + # two ten-second sleeps to an admission. Native sensor calls are separate. + paced_size = self.batch_size + sleep_time = 0.05 + paced_size * 0.02 + time.sleep(sleep_time) + with self._state_lock: + metrics = self._build_metrics() + # A concurrent backoff applies immediately; a concurrent increase + # cannot grant more work than this admission's pacing covered. + size = min(paced_size, self.state_machine.batch_size) + metrics["batch_size"] = size + return size, metrics + + def _expire_samples(self, now: float) -> None: + """Caller holds the state lock; old evidence cannot authorize a ramp.""" + while self._samples and now - self._samples[0][0] > self.SAMPLE_MAX_AGE: + self._samples.popleft() + if self._last_sample_time is not None and now - self._last_sample_time > self.SAMPLE_MAX_AGE: + self.state_machine.good_streak = 0 + + def _sample_due(self, now: float) -> bool: if self.state_machine.should_safety_check(): + return True + if self._last_sample_time is None: + return False + age = now - self._last_sample_time + # Collect fresh evidence before ramp eligibility: eligibility itself + # requires three good observations and cannot trigger their collection. + return age >= self.SAMPLE_SPACING + + def _sample_if_due(self) -> None: + # Only one caller samples. Other admissions keep the current bound; + # they never line up behind the sensor subprocess or create a monitor. + if not self._sample_lock.acquire(blocking=False): + return + try: + with self._state_lock: + if not self._sample_due(time.monotonic()): + return + generation = self._fault_generation + budget = self._budget_snapshot() readings = self._read_all_channels() - self._last_readings = readings - self.state_machine.safety_check(readings) - - if self.state_machine.should_ramp_check(): - means = self._triple_sample() - self.state_machine.ramp_up(means) - - sleep_time = 0.05 + self.state_machine.batch_size * 0.02 - time.sleep(sleep_time) - - metrics = self._build_metrics() - return self.state_machine.batch_size, metrics + now = time.monotonic() + with self._state_lock: + # An OOM during the probe invalidates its pre-fault evidence. + if generation != self._fault_generation: + return + if budget is not self._budget_snapshot(): + # The first MPS model may finish setup while a CPU-only + # thermal probe is pending. Its earlier unsupported MPS + # observations cannot authorize a ramp for the new state. + self._samples.clear() + self._last_readings = {} + self._last_sample_time = None + self.state_machine.good_streak = 0 + return + self._last_readings = readings + self._last_sample_time = now + self._expire_samples(now) + self.state_machine.safety_check(readings) + if not self.state_machine.all_required_channels_ok(readings): + self._samples.clear() + return + if not self._samples or now - self._samples[-1][0] >= self.SAMPLE_SPACING: + self._samples.append((now, readings)) + if len(self._samples) == 3 and self.state_machine.should_ramp_check(): + self.state_machine.ramp_up(self._compute_means([sample for _, sample in self._samples])) + self._samples.clear() + finally: + self._sample_lock.release() def after_batch(self, batch_items: int, batch_time: float): """Record batch completion for throughput tracking.""" - self.items_processed += batch_items - self.batch_times.append(batch_time) - if len(self.batch_times) > 20: - self.batch_times.pop(0) + with self._state_lock: + self.items_processed += batch_items + self.batch_times.append(batch_time) + if len(self.batch_times) > 20: + self.batch_times.pop(0) def get_throughput(self) -> float: """Items per second since start.""" - elapsed = time.time() - self.start_time - return self.items_processed / elapsed if elapsed > 0 else 0.0 + with self._state_lock: + elapsed = time.monotonic() - self.start_time + return self.items_processed / elapsed if elapsed > 0 else 0.0 def get_eta_seconds(self, remaining: int) -> float: """Estimated seconds to process remaining items.""" @@ -113,31 +213,56 @@ def get_eta_seconds(self, remaining: int) -> float: def should_flush_cache(self) -> bool: """Return True only on WARNING/BACKOFF — not during normal PROBING.""" - return self.state_machine.state in ( - ThrottlerStateMachine.STABLE, - ThrottlerStateMachine.BACKOFF, - ) + with self._state_lock: + return self.state_machine.state in ( + ThrottlerStateMachine.STABLE, + ThrottlerStateMachine.BACKOFF, + ) def on_oom(self): """Handle OOM by triggering BACKOFF in the state machine.""" - self.state_machine._do_backoff("oom", {"status": "critical"}) + with self._state_lock: + self._fault_generation += 1 + self._samples.clear() + self.state_machine._do_backoff("oom", {"status": "critical"}) def _read_all_channels(self) -> dict: - """Single quick reading of all 3 channels.""" - try: - mps = read_mps_memory(self.mps_cap_gb) - except Exception: - mps = {"used_gb": 0.0, "ratio": 0.0, "status": "ok"} - - try: - growth = self.growth_tracker.update(mps["used_gb"]) - except Exception: - growth = {"slope_gb_per_20s": 0.0, "slope_pct": 0.0, "status": "ok"} + """Observe applicable channels; unavailable data must not authorize ramping.""" + mps = {"used_gb": None, "ratio": None, "status": "unsupported", "required": False} + growth = {"slope_gb_per_20s": None, "slope_pct": None, + "status": "unsupported", "required": False} + with self._state_lock: + budget = self._budget_snapshot() + is_mps = self.device == "mps" or budget is not None + cap_gb = budget.effective_bytes / (1024**3) if budget and budget.effective_bytes else None + if cap_gb != self._growth_cap_gb: + self.growth_tracker = GrowthRateTracker(cap_gb=cap_gb) if cap_gb else None + self._growth_cap_gb = cap_gb + tracker = self.growth_tracker + if is_mps: + # Unlimited or unconfigured is not evidence of finite headroom. + mps = {"used_gb": None, "ratio": None, "status": "unknown"} + growth = {"slope_gb_per_20s": None, "slope_pct": None, "status": "unknown"} + if cap_gb is not None: + try: + mps = read_mps_memory(cap_gb) + except Exception: + pass + if mps.get("used_gb") is not None and tracker is not None: + try: + growth = tracker.update(mps["used_gb"]) + except Exception: + pass + elif tracker is not None: + # Do not calculate a slope across a gap with unknown memory. + with self._state_lock: + if self.growth_tracker is tracker: + self.growth_tracker = GrowthRateTracker(cap_gb=cap_gb) try: thermal = read_thermal_pressure() except Exception: - thermal = {"scheduler_limit": 100, "status": "ok"} + thermal = {"scheduler_limit": None, "status": "unknown"} return { "mps_level": mps, @@ -145,15 +270,6 @@ def _read_all_channels(self) -> dict: "thermal": thermal, } - def _triple_sample(self) -> dict: - """Take 3 readings 10s apart, return mean-based status.""" - samples = [] - for i in range(3): - if i > 0: - time.sleep(10) - samples.append(self._read_all_channels()) - return self._compute_means(samples) - def _compute_means(self, samples: list[dict]) -> dict: """Compute mean status across 3 samples. @@ -161,35 +277,59 @@ def _compute_means(self, samples: list[dict]) -> dict: """ result = {} for channel in ("mps_level", "growth_rate", "thermal"): - statuses = [s[channel]["status"] for s in samples] + readings = [sample.get(channel, {}) for sample in samples] + statuses = [reading.get("status", "unknown") for reading in readings] if "critical" in statuses: result[channel] = {"status": "critical"} elif "warning" in statuses: result[channel] = {"status": "warning"} - else: + elif all(status == "ok" for status in statuses): result[channel] = {"status": "ok"} + elif all(reading.get("required") is False and reading.get("status") == "unsupported" + for reading in readings): + result[channel] = {"status": "unsupported", "required": False} + else: + result[channel] = {"status": "unknown"} return result def _build_metrics(self) -> dict: """Build metrics dict for status reporting.""" readings = self._last_readings + age = None if self._last_sample_time is None else time.monotonic() - self._last_sample_time + budget = self._budget_snapshot() return { "batch_size": self.state_machine.batch_size, "state": self.state_machine.state, - "mps_used_gb": readings.get("mps_level", {}).get("used_gb", 0.0), - "mps_ratio": readings.get("mps_level", {}).get("ratio", 0.0), - "growth_slope_pct": readings.get("growth_rate", {}).get("slope_pct", 0.0), - "thermal_limit": readings.get("thermal", {}).get("scheduler_limit", 100), + "mps_used_gb": readings.get("mps_level", {}).get("used_gb"), + "mps_ratio": readings.get("mps_level", {}).get("ratio"), + "growth_slope_pct": readings.get("growth_rate", {}).get("slope_pct"), + "thermal_limit": readings.get("thermal", {}).get("scheduler_limit"), "good_streak": self.state_machine.good_streak, + "sensor_status": {channel: readings.get(channel, {}).get("status", "unknown") + for channel in ("mps_level", "growth_rate", "thermal")}, + "sample_age_seconds": age, + "sample_stale": age is None or age > self.SAMPLE_MAX_AGE, + "sample_pending": self._sample_lock.locked(), + "mps_budget": { + "scope": "process_mps_allocator", + "intended_bytes": budget.intended_bytes if budget else None, + "recommended_bytes": budget.recommended_bytes if budget else None, + "effective_bytes": budget.effective_bytes if budget else None, + "source": budget.source if budget else None, + "enforcement": budget.enforcement if budget else ( + "unknown" if self.device == "mps" else "not_applicable" + ), + }, } @staticmethod def flush_gpu_cache(): """Flush MPS/CUDA cache and run garbage collection.""" try: + from truememory.mps_utils import get_mps_memory_budget import torch - if torch.backends.mps.is_available(): + if get_mps_memory_budget() is not None and torch.backends.mps.is_available(): torch.mps.empty_cache() torch.mps.synchronize() elif torch.cuda.is_available(): diff --git a/truememory/tier_switch/worker.py b/truememory/tier_switch/worker.py index ea42d4ad..b219346c 100644 --- a/truememory/tier_switch/worker.py +++ b/truememory/tier_switch/worker.py @@ -7,13 +7,21 @@ """ import logging +import math import sqlite3 import time -from typing import Callable +from collections.abc import Iterator +from contextlib import contextmanager +from typing import TYPE_CHECKING, Callable, NamedTuple from truememory.tier_switch.cache import VectorCacheRegistry from truememory.tier_switch.throttler import DynamicThrottler +if TYPE_CHECKING: + from truememory.embedding_target import EmbeddingTarget + from truememory.tier_switch.source import TierSourcePlan + from truememory.vector_search import _PreparedEmbeddingLease + log = logging.getLogger(__name__) _HARD_TIMEOUT = 9000 # 2.5 hours @@ -21,6 +29,60 @@ StatusCallback = Callable[[int, int, dict], None] +class SourceBuildResult(NamedTuple): + status: str + plan: "TierSourcePlan" + processed: int + captured_range_complete: bool = False + current_source_complete: bool = False + activated: bool = False + + +class _SourceStopped(Exception): + pass + + +def _encode_source_prefix(lease: "_PreparedEmbeddingLease", rows: tuple, count: int, + admit: Callable[[], float]) -> tuple[list[bytes], list[bytes]]: + from truememory.vector_search import _build_sep_text, serialize_f32 + + # Keep arrays inside this frame so a failed attempt releases its traceback + # before the outer loop performs allocator cleanup or revalidates SQL. + texts = [row[1] for row in rows[:count]] + embeddings = lease.encode(texts, timeout=admit(), batch_size=32) + admit() + if len(embeddings) != count: + raise ValueError("Completion embedding count does not match the rebuild prefix") + completion = [serialize_f32(vector) for vector in embeddings] + del embeddings, texts + sep_texts = [_build_sep_text(row[2], row[3], row[4], row[1]) for row in rows[:count]] + embeddings = lease.encode(sep_texts, timeout=admit(), batch_size=32) + admit() + if len(embeddings) != count: + raise ValueError("Separation embedding count does not match the rebuild prefix") + separation = [serialize_f32(vector) for vector in embeddings] + del embeddings, sep_texts + admit() + return completion, separation + + +@contextmanager +def _rebuild_transaction(conn: sqlite3.Connection) -> Iterator[None]: + owned = not conn.in_transaction + conn.execute("BEGIN" if owned else "SAVEPOINT truememory_rebuild_batch") + try: + yield + conn.execute("COMMIT" if owned else "RELEASE SAVEPOINT truememory_rebuild_batch") + except BaseException: + if owned: + conn.rollback() + else: + # Do not release if rollback fails: that could publish partial work. + conn.execute("ROLLBACK TO SAVEPOINT truememory_rebuild_batch") + conn.execute("RELEASE SAVEPOINT truememory_rebuild_batch") + raise + + class RebuildWorker: """Batch embedding loop with throttling and progress tracking.""" @@ -45,6 +107,190 @@ def cancel(self): """Signal the worker to stop after the current batch.""" self._cancelled = True + def run_source( + self, target: "EmbeddingTarget", plan: "TierSourcePlan", *, + page_size: int = 64, timeout: float = _HARD_TIMEOUT, + ) -> SourceBuildResult: + """Build an initialized inactive pair without activating or clearing it. + + The caller owns maintenance, the clean connection and exclusive pair + writer, and has validated target schema and database identity. At most + one source page plus encoded prefix buffers is retained. Cancellation is + cooperative between calls: it cannot interrupt native/SQLite calls or + their internal waits. Deadlines also flow into the prepared lease. + """ + from truememory.embedding_target import EmbeddingTarget + from truememory.tier_switch.source import ( + TierSourcePlan, TierSourceUntrusted, finish_tier_source, plan_tier_source, + publish_tier_prefix, read_tier_page, + ) + from truememory.vector_search import prepare_embedding_target + + if not isinstance(target, EmbeddingTarget) or not isinstance(plan, TierSourcePlan): + raise TypeError("An explicit frozen target and initialized source plan are required") + if (plan.connection is not self.conn or plan.action not in ("resume", "complete") + or plan.manifest.model != target.model_id or plan.manifest.dimension != target.dimension + or plan.manifest.targets != target.tables or self.target_tier != target.tier + or self.target_group != target.tier_group): + raise ValueError("Worker, initialized source and prepared target identities must agree") + if type(page_size) is not int or page_size < 1: + raise ValueError("Source page size must be a positive integer") + if isinstance(timeout, bool) or not isinstance(timeout, (int, float)) or not math.isfinite(timeout) or timeout <= 0: + raise ValueError("Source timeout must be finite and positive") + if self.conn.in_transaction: + raise RuntimeError("RebuildWorker.run_source requires a clean owned connection") + expires = time.monotonic() + min(timeout, _HARD_TIMEOUT) + initial_consumed = plan.manifest.consumed + phase_ceiling = effective_ceiling = None + phase_failures = 0 + captured_complete = current_complete = False + publication_uncertain = False + + def admit() -> float: + if self.conn.in_transaction: + raise RuntimeError("Source worker connection did not return to a clean boundary") + if self._cancelled: + raise _SourceStopped("cancelled") + remaining = expires - time.monotonic() + if remaining <= 0: + raise _SourceStopped("timeout") + return remaining + + def result(status: str) -> SourceBuildResult: + if publication_uncertain: + raise TierSourceUntrusted("Failed publication requires durable checkpoint revalidation") + return SourceBuildResult(status, plan, plan.manifest.consumed - initial_consumed, + captured_complete, current_complete) + + try: + admit() + # A stale plan must fail before preparation, even for an empty range. + pending = read_tier_page(self.conn, plan, size=page_size) + admit() + try: + import torch + no_grad = torch.no_grad() + except ImportError: + from contextlib import nullcontext + no_grad = nullcontext() + with no_grad, prepare_embedding_target(target, timeout=admit()) as lease: + admit() + while pending.rows: + batch_size, metrics = self.throttler.before_batch() + admit() + if type(batch_size) is not int or batch_size < 1: + raise ValueError("Throttle admission must grant a positive integer prefix") + count = min(batch_size, len(pending.rows)) + if effective_ceiling is not None: + count = min(count, effective_ceiling) + started = time.monotonic() + oom = False + publishing = False + completion = separation = None + try: + completion, separation = _encode_source_prefix(lease, pending.rows, count, admit) + admit() + publishing = True + plan, pending = publish_tier_prefix( + self.conn, plan, pending, completion=completion, separation=separation, + ) + except _SourceStopped: + raise + except Exception as error: + # No DB queries, status updates or retries after uncertain + # rollback. The caller must discard this connection. + if (self.conn.in_transaction or (publishing and + (error.__context__ is not None or error.__cause__ is not None))): + raise + if not self._is_oom_error(error): + raise + publication_uncertain = publishing + oom = True + finally: + completion = separation = None + + if oom: + if phase_ceiling is None: + phase_ceiling = effective_ceiling = count + phase_failures = 1 + else: + effective_ceiling = min(effective_ceiling, count) + if count <= phase_ceiling // 2: + phase_ceiling = count + phase_failures = 1 + else: + phase_failures += 1 + exhausted = phase_ceiling == 1 and phase_failures == 2 + if phase_failures == 2 and not exhausted: + phase_ceiling //= 2 + effective_ceiling = min(effective_ceiling, phase_ceiling) + phase_failures = 0 + admit() + self.throttler.on_oom() + admit() + DynamicThrottler.flush_gpu_cache() + admit() + if exhausted and not publication_uncertain: + return result("oom_exhausted") + if self.conn.total_changes != plan.changes: + # Rolled-back SQL contributes to total_changes. One + # audit per failed attempt, bounded by OOM credits. + resumed = plan_tier_source( + self.conn, model_id=target.model_id, dimension=target.dimension, + targets=target.tables, + ) + admit() + if resumed.action != "resume" or resumed.manifest != plan.manifest: + raise TierSourceUntrusted("Failed publication no longer has the same source checkpoint") + plan = resumed + checked = read_tier_page(self.conn, plan, size=len(pending.rows)) + admit() + # The adapter checks exact SQLite types again under the + # writer. Retain the original immutable pending suffix. + from truememory.tier_switch.source import _same_rows + if checked.generation != pending.generation or checked.cursor != pending.cursor or not _same_rows(checked.rows, pending.rows): + raise TierSourceUntrusted("Failed publication no longer has the same pending source rows") + del checked + publication_uncertain = False + if exhausted: + return result("oom_exhausted") + continue + + # Capture the committed checkpoint before observing a + # cancellation that arrived during SQLite's writer wait. + phase_ceiling = effective_ceiling = None + phase_failures = 0 + admit() + self.throttler.after_batch(count, time.monotonic() - started) + admit() + flush = self.throttler.should_flush_cache() + admit() + if flush: + DynamicThrottler.flush_gpu_cache() + admit() + if self.status_callback is not None: + live = dict(metrics, batch_size=count, activated=False) + try: + self.status_callback(plan.manifest.consumed, plan.manifest.total, live) + except Exception: + pass + admit() + if not pending.rows: + pending = read_tier_page(self.conn, plan, size=page_size) + admit() + admit() + finished = finish_tier_source(self.conn, plan) + plan = finished.plan + captured_complete = finished.captured_range_complete + current_complete = finished.current_source_complete + admit() + return SourceBuildResult( + "complete", plan, plan.manifest.consumed - initial_consumed, + finished.captured_range_complete, finished.current_source_complete, + ) + except _SourceStopped as stopped: + return result(str(stopped)) + def run( self, messages: list[dict], @@ -72,6 +318,11 @@ def run( log.info("No messages to embed — nothing to do") return True, 0 + # Manager workers use dedicated connections. Initialization and status + # publication commit independently, so caller-owned work cannot join run(). + if self.conn.in_transaction: + raise RuntimeError("RebuildWorker.run requires a connection with no active transaction") + total = len(messages) log.info( "RebuildWorker starting: %d messages, group=%s, full=%s", @@ -87,11 +338,17 @@ def run( if is_full_rebuild: try: - self.conn.execute(f"DELETE FROM {vec_table}") - self.conn.execute(f"DELETE FROM {sep_table}") - self.conn.commit() - except sqlite3.OperationalError: - pass + with _rebuild_transaction(self.conn): + self.conn.execute(f"DELETE FROM {vec_table}") + self.conn.execute(f"DELETE FROM {sep_table}") + VectorCacheRegistry.update_progress( + self.conn, self.target_group, 0, 0, commit=False, + ) + except Exception as exc: + if self.conn.in_transaction: + raise + self._update_status("failed", 0, total, str(exc)) + return False, 0 try: import torch @@ -101,9 +358,11 @@ def run( no_grad = nullcontext() processed = 0 - last_id = 0 offset = 0 start_time = time.time() + phase_ceiling: int | None = None + effective_ceiling: int | None = None + phase_failures = 0 with no_grad: while offset < total: @@ -122,32 +381,73 @@ def run( return False, processed batch_size, metrics = self.throttler.before_batch() + if effective_ceiling is not None: + # Admission itself can consume the remaining recovery time. + if self._cancelled or time.time() - start_time > _HARD_TIMEOUT: + continue + batch_size = min(batch_size, effective_ceiling) batch = messages[offset : offset + batch_size] if not batch: break batch_start = time.time() + oom = False try: success = self._process_batch( batch, model, vec_table, sep_table, serialize_f32, _build_sep_text, + record_progress=lambda: VectorCacheRegistry.update_progress( + self.conn, self.target_group, batch[-1]["id"], + self._count_vectors(vec_table), commit=False, + ), ) except Exception as exc: + # run() owns a clean connection. An active transaction here + # means rollback did not restore that boundary. + if self.conn.in_transaction: + raise if self._is_oom_error(exc): - log.warning( - "OOM at batch_size=%d, triggering BACKOFF", - batch_size, + oom = True + else: + log.error( + "RebuildWorker error at %d/%d: %s", + processed, total, exc, ) - self.throttler.on_oom() - DynamicThrottler.flush_gpu_cache() + self._update_status("failed", processed, total, str(exc)) + return False, processed + + if oom: + # Leave the exception scope before cleanup releases native + # workspace retained by the failed forward's traceback. + failed_count = len(batch) + if phase_ceiling is None: + phase_ceiling = effective_ceiling = failed_count + phase_failures = 1 + else: + effective_ceiling = min(effective_ceiling, failed_count) + if failed_count <= phase_ceiling // 2: + phase_ceiling = failed_count + phase_failures = 1 + else: + phase_failures += 1 + exhausted = phase_ceiling == 1 and phase_failures == 2 + if phase_failures == 2 and not exhausted: + phase_ceiling //= 2 + effective_ceiling = min(effective_ceiling, phase_ceiling) + phase_failures = 0 + log.warning("OOM at effective batch_size=%d, triggering BACKOFF", failed_count) + self.throttler.on_oom() + DynamicThrottler.flush_gpu_cache() + if self._cancelled or time.time() - start_time > _HARD_TIMEOUT: continue - log.error( - "RebuildWorker error at %d/%d: %s", - processed, total, exc, - ) - self._update_status("failed", processed, total, str(exc)) - return False, processed + if exhausted: + self._update_status( + "failed", processed, total, + "Rebuild ran out of memory twice at batch size 1; retry after freeing memory.", + ) + return False, processed + continue if not success: self._update_status("failed", processed, total) @@ -157,17 +457,13 @@ def run( batch_count = len(batch) processed += batch_count offset += batch_count - last_id = batch[-1]["id"] + phase_ceiling = effective_ceiling = None + phase_failures = 0 self.throttler.after_batch(batch_count, batch_time) if self.throttler.should_flush_cache(): DynamicThrottler.flush_gpu_cache() - VectorCacheRegistry.update_progress( - self.conn, self.target_group, last_id, - self._count_vectors(vec_table), - ) - self._update_status("running", processed, total) self._update_status("complete", processed, total) @@ -185,21 +481,19 @@ def _process_batch( sep_table: str, serialize_f32, build_sep_text, + record_progress: Callable[[], None] | None = None, ) -> bool: - """Encode and insert a single batch of messages.""" + """Prepare both embeddings, then publish their pair and checkpoint atomically.""" + from truememory.mps_utils import encode_with_model_ownership + texts = [m["content"] for m in batch] ids = [m["id"] for m in batch] - embeddings = model.encode(texts, show_progress_bar=False) - - self.conn.executemany( - f"INSERT INTO {vec_table}(rowid, embedding) " - f"VALUES (?, ?)", - [ - (mid, serialize_f32(emb)) - for mid, emb in zip(ids, embeddings) - ], - ) + embeddings = encode_with_model_ownership(model, texts, show_progress_bar=False) + if len(embeddings) != len(ids): + raise ValueError("Completion embedding count does not match the rebuild batch") + completion_rows = [(mid, serialize_f32(emb)) for mid, emb in zip(ids, embeddings)] + del embeddings sep_texts = [ build_sep_text( @@ -210,30 +504,27 @@ def _process_batch( ) for m in batch ] - sep_embeddings = model.encode(sep_texts, show_progress_bar=False) - - self.conn.executemany( - f"INSERT INTO {sep_table}(rowid, embedding) " - f"VALUES (?, ?)", - [ - (mid, serialize_f32(emb)) - for mid, emb in zip(ids, sep_embeddings) - ], - ) - - self.conn.commit() - del embeddings, sep_embeddings + sep_embeddings = encode_with_model_ownership(model, sep_texts, show_progress_bar=False) + if len(sep_embeddings) != len(ids): + raise ValueError("Separation embedding count does not match the rebuild batch") + separation_rows = [(mid, serialize_f32(emb)) for mid, emb in zip(ids, sep_embeddings)] + del sep_embeddings + + with _rebuild_transaction(self.conn): + self.conn.executemany( + f"INSERT INTO {vec_table}(rowid, embedding) VALUES (?, ?)", completion_rows, + ) + self.conn.executemany( + f"INSERT INTO {sep_table}(rowid, embedding) VALUES (?, ?)", separation_rows, + ) + if record_progress is not None: + record_progress() return True def _count_vectors(self, vec_table: str) -> int: """Count rows in the vector table.""" - try: - row = self.conn.execute( - f"SELECT COUNT(*) FROM {vec_table}" - ).fetchone() - return row[0] if row else 0 - except sqlite3.OperationalError: - return 0 + row = self.conn.execute(f"SELECT COUNT(*) FROM {vec_table}").fetchone() + return row[0] if row else 0 def _update_status( self, @@ -258,22 +549,25 @@ def _update_status( if self.status_id: now = time.time() completed_at = now if status in ("complete", "failed", "cancelled") else None + owned_status = not self.conn.in_transaction try: - self.conn.execute( - "UPDATE rebuild_status SET " - "status=?, processed_messages=?, progress_pct=?, " - "eta_seconds=?, batch_size=?, throughput_ips=?, " - "ram_pct=?, last_heartbeat=?, completed_at=?, error=? " - "WHERE id=?", - ( - status, processed, pct, - eta, self.throttler.batch_size, throughput, - ram_pct, now, completed_at, error, - self.status_id, - ), - ) - self.conn.commit() + with _rebuild_transaction(self.conn): + self.conn.execute( + "UPDATE rebuild_status SET " + "status=?, processed_messages=?, progress_pct=?, " + "eta_seconds=?, batch_size=?, throughput_ips=?, " + "ram_pct=?, last_heartbeat=?, completed_at=?, error=? " + "WHERE id=?", + ( + status, processed, pct, + eta, self.throttler.batch_size, throughput, + ram_pct, now, completed_at, error, + self.status_id, + ), + ) except sqlite3.OperationalError: + if owned_status and self.conn.in_transaction: + raise pass if self.status_callback: diff --git a/truememory/tier_switch/writer.py b/truememory/tier_switch/writer.py new file mode 100644 index 00000000..d42e1481 --- /dev/null +++ b/truememory/tier_switch/writer.py @@ -0,0 +1,196 @@ +"""Selection fences for already-admitted foreground/source writers. + +No native model, serving lease, configuration, or selection is changed here. +An admitted runtime operation supplies its immutable selection; an unbound +direct caller captures a short database snapshot before preparing any output. +""" + +from __future__ import annotations + +import sqlite3 +import sys +import uuid +from collections.abc import Iterator +from contextlib import contextmanager +from dataclasses import dataclass, field, replace +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from truememory.tier_switch.activation import TierSelection, LegacyTierPolicy + + +class WriterSelectionChanged(RuntimeError): + """Retry the whole operation after serving selection reconciliation.""" + + +@dataclass(frozen=True) +class WriterCapture: + connection: sqlite3.Connection = field(repr=False) + selection: TierSelection | None + policy: LegacyTierPolicy | None = None + + def __post_init__(self) -> None: + _validate(self.selection) + if self.policy is not None: + from truememory.tier_switch.activation import LegacyTierPolicy + if type(self.policy) is not LegacyTierPolicy or self.selection is not None: + raise WriterSelectionChanged("Writer policy capture must be an immutable journal result") + + +def _authority(conn: sqlite3.Connection) -> tuple[TierSelection | None, LegacyTierPolicy | None]: + table = conn.execute( + "SELECT type FROM main.sqlite_master WHERE name='metadata' COLLATE NOCASE", + ).fetchone() + if table is None: + return None, None + if table[0] != "table": + raise WriterSelectionChanged("Selected metadata is not an ordinary table") + if conn.execute("SELECT 1 FROM main.metadata WHERE key IN ('tier_selected_v1','tier_activation_v1') LIMIT 1").fetchone() is None: + return None, None + from truememory.tier_switch.activation import TierActivationError, _guard_storage, _state + + try: + _guard_storage(conn) + state = _state(conn) + return state.selection, getattr(state, "legacy_policy", None) + except (TierActivationError, sqlite3.Error) as exc: + raise WriterSelectionChanged("Selected journal cannot be validated") from exc + + +def _selection(conn: sqlite3.Connection) -> TierSelection | None: + return _authority(conn)[0] + + +def _validate(selection: TierSelection | None) -> None: + if selection is not None: + from truememory.tier_switch.activation import TierSelection + + if type(selection) is not TierSelection: + raise WriterSelectionChanged("Writer capture must be an immutable selected descriptor") + + +def capture_writer_selection(conn: sqlite3.Connection) -> WriterCapture: + """Capture before native/source work; never substitute a newer admitted key. + + The optional runtime module is consulted only when already loaded. Its + current_operation accessor must raise for a mismatched active connection; + None means unbound, while operation.selection=None is an admitted legacy + selection. Caller transactions are read without ending or upgrading them. + """ + runtime = sys.modules.get("truememory.tier_switch.runtime") + if runtime is not None: + operation = runtime.current_operation(conn) + if operation is not None: + _validate(operation.selection) + return WriterCapture(conn, operation.selection, getattr(operation, "policy", None)) + owned = not conn.in_transaction + if owned: + conn.execute("BEGIN") + try: + return WriterCapture(conn, *_authority(conn)) + finally: + if owned: + # One cleanup attempt only, including failure after rollback. + conn.rollback() + if conn.in_transaction: + raise WriterSelectionChanged("Writer capture snapshot cleanup is uncertain") + + +def require_writer_selection( + conn: sqlite3.Connection, captured: WriterCapture | None, *, + model_id: str | None = None, dimension: int | None = None, + tables: tuple[str, str] | None = None, +) -> None: + """Read-only comparison under the caller's actual database writer. + + sqlite3 exposes transaction existence, not lock kind. Use writer_transaction + or another proven writer acquisition before this helper. It never loads, + recaptures an admitted selection, mutates metadata, commits, or rolls back. + Config acknowledgement alone does not change the captured embedding space. + """ + if not conn.in_transaction: + raise WriterSelectionChanged("Selected publication requires database writer ownership") + if captured is not None and (type(captured) is not WriterCapture or captured.connection is not conn): + raise WriterSelectionChanged("Writer capture belongs to a different accepted connection") + selected = captured.selection if captured is not None else None + current, current_policy = _authority(conn) + policy = captured.policy if captured is not None else None + if policy is None: + if current_policy is not None: + raise WriterSelectionChanged("A legacy policy appeared after writer admission") + elif current_policy is None or replace(current_policy, config_acknowledged=policy.config_acknowledged) != policy: + raise WriterSelectionChanged("Legacy policy generation changed; retry the operation") + if selected is None and policy is None: + if current is not None: + raise WriterSelectionChanged("A selected tier appeared after legacy admission") + return + if selected is not None and (current is None or replace(current, config_acknowledged=selected.config_acknowledged) != selected): + raise WriterSelectionChanged("Selected tier generation or descriptor changed; retry the operation") + if policy is not None and current is not None: + raise WriterSelectionChanged("A selected tier appeared after policy admission") + selected = selected if selected is not None else policy + if ((model_id is not None and model_id != selected.target.model_id) + or (dimension is not None and (type(dimension) is not int or dimension != selected.target.dimension)) + or (tables is not None and (type(tables) is not tuple or tables != selected.tables))): + raise WriterSelectionChanged("Prepared output does not match the captured selected pair") + if policy is None: + # Boolean SQL avoids copying unbounded registry/metadata values into Python. + row = conn.execute( + "SELECT vec_table=? AND sep_table=? AND model_name=? " + "AND typeof(embedding_dim)='integer' AND embedding_dim=? " + "FROM main.vector_cache_registry WHERE tier_group=?", + (*selected.tables, selected.target.model_id, selected.target.dimension, selected.target.tier_group), + ).fetchone() + if row is None or row[0] != 1: + raise WriterSelectionChanged("Selected vector registry identity changed") + for key, value in (("embed_model", selected.target.model_id), ("embed_dim", str(selected.target.dimension))): + row = conn.execute( + "SELECT typeof(value)='text' AND value=? FROM main.metadata WHERE key=?", (value, key), + ).fetchone() + if row is None or row[0] != 1: + raise WriterSelectionChanged("Selected embedder metadata changed") + + +@contextmanager +def writer_transaction( + conn: sqlite3.Connection, captured: WriterCapture | None, *, + model_id: str | None = None, dimension: int | None = None, + tables: tuple[str, str] | None = None, +) -> Iterator[None]: + """Acquire the writer before comparison; preserve borrowed outer work. + + Own BEGIN IMMEDIATE or borrow a SAVEPOINT and upgrade via a zero-row source + UPDATE, never a metadata write. A stale read snapshot may fail that upgrade + for retry. Only owned work is committed. Uncertain cleanup is not retried. + """ + owned = not conn.in_transaction + guard = "tier_writer_" + uuid.uuid4().hex + conn.execute("BEGIN IMMEDIATE" if owned else f"SAVEPOINT {guard}") + guard_release_attempted = False + try: + if not owned: + conn.execute("SAVEPOINT rebuild_source") + conn.execute("UPDATE messages SET id = id WHERE 0") + require_writer_selection(conn, captured, model_id=model_id, dimension=dimension, tables=tables) + yield + if owned: + conn.commit() + if conn.in_transaction: + raise WriterSelectionChanged("Writer transaction did not commit") + else: + conn.execute("RELEASE SAVEPOINT rebuild_source") + guard_release_attempted = True + conn.execute(f"RELEASE SAVEPOINT {guard}") + except BaseException: + if owned: + if conn.in_transaction: + conn.rollback() + if conn.in_transaction: + raise WriterSelectionChanged("Writer rollback is uncertain") + elif conn.in_transaction and not guard_release_attempted: + # Only our unique guard is eligible for cleanup. The compatibility + # inner name may now refer to a caller savepoint after RELEASE. + # A final guard-release error is ambiguous and permits no cleanup. + conn.execute(f"ROLLBACK TO SAVEPOINT {guard}") + conn.execute(f"RELEASE SAVEPOINT {guard}") + raise diff --git a/truememory/vector_search.py b/truememory/vector_search.py index f776af14..ccd35925 100644 --- a/truememory/vector_search.py +++ b/truememory/vector_search.py @@ -45,6 +45,10 @@ import struct import sqlite3 import threading +import time +from collections.abc import Callable, Iterator +from contextlib import closing, contextmanager +from dataclasses import replace from typing import TYPE_CHECKING import numpy as np @@ -53,7 +57,8 @@ from truememory.storage import _deserialize_metadata, select_message_cols if TYPE_CHECKING: - pass + from truememory.embedding_target import EmbeddingTarget + from truememory.tier_switch.writer import WriterCapture logger = logging.getLogger(__name__) @@ -142,6 +147,13 @@ def resolve_tier() -> str: should call this instead so that ``~/.truememory/config.json`` is honoured when the env var is absent. """ + import sys + runtime = sys.modules.get("truememory.tier_switch.runtime") + operation = runtime.current_runtime_operation() if runtime is not None else None + if operation is not None: + return operation.tier + if globals().get("_runtime_policy_tier") is not None: + return _runtime_policy_tier env = os.environ.get("TRUEMEMORY_EMBED_MODEL", "").strip().lower() if env: return env @@ -163,6 +175,9 @@ def resolve_tier() -> str: _model = None _embedding_dim: int = _MODEL_DIMS.get(EMBEDDING_MODEL, 256) _lock = threading.Lock() +_model_generation = 0 +_frozen_embedding_target = None +_runtime_policy_tier: str | None = None def _active_tier_group() -> str: @@ -172,6 +187,11 @@ def _active_tier_group() -> str: def _active_vec_table(conn: sqlite3.Connection) -> str: """Return the active vec_messages table name for the current tier.""" + import sys + runtime = sys.modules.get("truememory.tier_switch.runtime") + operation = runtime.current_operation(conn) if runtime is not None else None + if operation is not None: + return operation.tables[0] try: group = _active_tier_group() row = conn.execute( @@ -187,6 +207,11 @@ def _active_vec_table(conn: sqlite3.Connection) -> str: def _active_sep_table(conn: sqlite3.Connection) -> str: """Return the active vec_messages_sep table name for the current tier.""" + import sys + runtime = sys.modules.get("truememory.tier_switch.runtime") + operation = runtime.current_operation(conn) if runtime is not None else None + if operation is not None: + return operation.tables[1] try: group = _active_tier_group() row = conn.execute( @@ -205,9 +230,13 @@ def set_embedding_model(name: str) -> None: Accepts tier names ("base", "pro") or internal model names. """ - global EMBEDDING_MODEL, _model, _embedding_dim - with _lock: + global EMBEDDING_MODEL, _model, _embedding_dim, _model_generation, _frozen_embedding_target, _runtime_policy_tier + from truememory.tier_switch.serving import exclusive_activation + with exclusive_activation(), _lock: + _model_generation += 1 _model = None # Force reload + _frozen_embedding_target = None + _runtime_policy_tier = None EMBEDDING_MODEL = _resolve_model_name(name) _embedding_dim = get_embedding_dim(EMBEDDING_MODEL) @@ -223,8 +252,10 @@ def get_embedding_dim(name: str | None = None) -> int: def unload_model() -> None: """Release the embedding model from memory.""" - global _model - with _lock: + global _model, _model_generation + from truememory.tier_switch.serving import exclusive_activation + with exclusive_activation(), _lock: + _model_generation += 1 _model = None @@ -232,87 +263,442 @@ def get_model(): """Lazy-load the embedding model (singleton). When the shared model server is enabled (default), returns a proxy - that routes inference to the server process. Falls back to local - loading if the server is unavailable. + that routes inference to the server process, including after idle exit. + Local loading requires explicit TRUEMEMORY_NO_MODEL_SERVER=1. """ global _model, _embedding_dim if _model is not None: return _model # Fast path, no lock needed - with _lock: - if _model is not None: - return _model # Another thread loaded it + from truememory.tier_switch.runtime import require_model_load_allowed + require_model_load_allowed() + from contextlib import nullcontext + from truememory.tier_switch.runtime import current_runtime_operation + from truememory.tier_switch.serving import exclusive_activation + with exclusive_activation() if current_runtime_operation() is None else nullcontext(): + with _lock: + if _model is not None: + return _model # Another thread loaded it + + if _frozen_embedding_target is not None: + _model = _load_frozen_embedding_target(_frozen_embedding_target) + _embedding_dim = _frozen_embedding_target.dimension + return _model - from truememory.model_client import use_model_server, get_embedding_proxy - if use_model_server(): - try: - proxy = get_embedding_proxy(tier=EMBEDDING_MODEL) - _model = proxy + from truememory.model_client import use_model_server, get_embedding_proxy + if use_model_server(): + _model = get_embedding_proxy(tier=EMBEDDING_MODEL) return _model - except Exception: - logger.warning( - "Model server available but embedding proxy failed — " - "falling back to local model loading (high memory cost). " - "Check ~/.truememory/model_server.stderr for details." - ) - resolved = EMBEDDING_MODEL - if resolved == "model2vec": - from model2vec import StaticModel - _model = StaticModel.from_pretrained("minishlab/potion-base-8M", force_download=False) - _embedding_dim = 256 - elif resolved == "minilm": - from sentence_transformers import SentenceTransformer - _model = SentenceTransformer("all-MiniLM-L6-v2") - _embedding_dim = 384 - elif resolved == "bge-small": - from sentence_transformers import SentenceTransformer - _model = SentenceTransformer("BAAI/bge-small-en-v1.5") - _embedding_dim = 384 - elif resolved == "qwen3_256": - from sentence_transformers import SentenceTransformer - import sys as _sys - _mkwargs = {} - if _sys.platform == "darwin": - _mkwargs["attn_implementation"] = "eager" - _model = SentenceTransformer( - "Qwen/Qwen3-Embedding-0.6B", - truncate_dim=256, - model_kwargs=_mkwargs or None, - ) - _embedding_dim = 256 - elif resolved not in _MODEL_DIMS: - # Custom tier: load arbitrary SentenceTransformer model. - # Requires TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD=1 (enforced by - # tier_config.resolve_custom_tier at config time). - if os.environ.get("TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD", "").strip() != "1": - logger.warning( - "Custom model %r requested without " - "TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD=1 -- " - "falling back to model2vec.", - resolved, - ) + from truememory.mps_utils import ensure_mps_memory_budget, resolve_device + + resolved = EMBEDDING_MODEL + if resolved == "model2vec": from model2vec import StaticModel - _model = StaticModel.from_pretrained( - "minishlab/potion-base-8M", force_download=False - ) + _model = StaticModel.from_pretrained("minishlab/potion-base-8M", force_download=False) _embedding_dim = 256 - else: + elif resolved == "minilm": + device = resolve_device(None) + ensure_mps_memory_budget(device) + from sentence_transformers import SentenceTransformer + _model = SentenceTransformer("all-MiniLM-L6-v2", device=device) + _embedding_dim = 384 + elif resolved == "bge-small": + device = resolve_device(None) + ensure_mps_memory_budget(device) + from sentence_transformers import SentenceTransformer + _model = SentenceTransformer("BAAI/bge-small-en-v1.5", device=device) + _embedding_dim = 384 + elif resolved == "qwen3_256": + device = resolve_device(None) + ensure_mps_memory_budget(device) from sentence_transformers import SentenceTransformer - from truememory.tier_config import resolve_custom_tier - cfg = resolve_custom_tier() - custom_dim = cfg["embed_dim"] + import sys as _sys + _mkwargs = {} + if _sys.platform == "darwin": + _mkwargs["attn_implementation"] = "eager" _model = SentenceTransformer( - resolved, truncate_dim=custom_dim, - trust_remote_code=False, + "Qwen/Qwen3-Embedding-0.6B", + truncate_dim=256, + model_kwargs=_mkwargs or None, + device=device, ) - _embedding_dim = custom_dim - else: - from model2vec import StaticModel - _model = StaticModel.from_pretrained("minishlab/potion-base-8M", force_download=False) - _embedding_dim = 256 + _embedding_dim = 256 + elif resolved not in _MODEL_DIMS: + # Custom tier: load arbitrary SentenceTransformer model. + # Requires TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD=1 (enforced by + # tier_config.resolve_custom_tier at config time). + if os.environ.get("TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD", "").strip() != "1": + logger.warning( + "Custom model %r requested without " + "TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD=1 -- " + "falling back to model2vec.", + resolved, + ) + from model2vec import StaticModel + _model = StaticModel.from_pretrained( + "minishlab/potion-base-8M", force_download=False + ) + _embedding_dim = 256 + else: + from truememory.tier_config import resolve_custom_tier + cfg = resolve_custom_tier() + custom_dim = cfg["embed_dim"] + device = resolve_device(None) + ensure_mps_memory_budget(device) + from sentence_transformers import SentenceTransformer + _model = SentenceTransformer( + resolved, truncate_dim=custom_dim, + trust_remote_code=False, + device=device, + ) + _embedding_dim = custom_dim + else: + from model2vec import StaticModel + _model = StaticModel.from_pretrained("minishlab/potion-base-8M", force_download=False) + _embedding_dim = 256 return _model +_prepared_target_slot = threading.Lock() + + +def _load_frozen_embedding_target(target: EmbeddingTarget, *, timeout: float | None = None) -> object: + from truememory.embedding_target import TARGET_PROBE_TEXT, build_target_model, check_target_vectors + from truememory.model_client import EmbeddingProxy, PreparedEmbeddingProxy, use_model_server + from truememory.mps_utils import _own_model, resolve_device + + expires = _target_deadline(timeout) + target.check_configuration() + if use_model_server(): + if target.tier != "custom": + from truememory.model_client import CertifiedEmbeddingProxy + proxy = CertifiedEmbeddingProxy(target) + PreparedEmbeddingProxy(target).prepare(timeout=_target_remaining(expires)) + return proxy + + class FrozenProxy(EmbeddingProxy): + def __init__(self) -> None: + super().__init__(tier=target.model_id) + self._prepared = PreparedEmbeddingProxy(target) + + def encode(self, texts: str | list[str], timeout: float | None = None, **kwargs: object) -> np.ndarray: + texts = [texts] if isinstance(texts, str) else list(texts) + values = self._prepared.encode(texts, timeout=timeout, batch_size=kwargs.get("batch_size", 32)) + check_target_vectors(target, values, len(texts)) + return values + + proxy = FrozenProxy() + proxy._prepared.prepare(timeout=_target_remaining(expires)) + return proxy + if not _prepared_target_slot.acquire(blocking=False): + raise RuntimeError("Another prepared embedding target lease is active") + try: + encoder = build_target_model(target, resolve_device(None), check=lambda: _target_remaining(expires)) + with _own_model(encoder, remaining=lambda: _target_remaining(expires)): + _target_remaining(expires) + values = encoder.encode([TARGET_PROBE_TEXT], batch_size=1, show_progress_bar=False) + _target_remaining(expires) + check_target_vectors(target, np.asarray(values, dtype=np.float32), 1) + return encoder + finally: + _prepared_target_slot.release() + + +def apply_embedding_policy(target: EmbeddingTarget, *, certified: bool, deadline: float | None = None, + load: bool = False) -> None: + """Rebind same-space slots; construction is opt-in for ordinary admission.""" + global _model, EMBEDDING_MODEL, _embedding_dim, _model_generation + global _frozen_embedding_target, _runtime_policy_tier + from truememory.embedding_target import EmbeddingTarget + from truememory.model_client import CertifiedEmbeddingProxy, EmbeddingProxy + from truememory.tier_switch.serving import exclusive_activation + if type(target) is not EmbeddingTarget or target.tier not in {"base", "pro"}: + raise ValueError("Policy projection requires an explicit same-space public target") + if type(certified) is not bool or type(load) is not bool: + raise TypeError("Policy projection controls must be boolean") + with exclusive_activation(deadline=deadline): + with _target_state_lock(deadline): + if (EMBEDDING_MODEL, _embedding_dim) != (target.model_id, target.dimension): + _model = None + _model_generation += 1 + elif isinstance(_model, EmbeddingProxy): + _model = CertifiedEmbeddingProxy(target) + EMBEDDING_MODEL, _embedding_dim = target.model_id, target.dimension + _frozen_embedding_target = target if certified else None + _runtime_policy_tier = target.tier + needs_load = _model is None and load + if needs_load: + model = _load_frozen_embedding_target(target, timeout=_target_remaining(deadline)) + with _target_state_lock(deadline): + _model = model + + +def apply_frozen_embedding_target(target: EmbeddingTarget, *, timeout: float | None = None) -> None: + """Fill the existing active slot from exact prepared-target arguments.""" + global _model, _embedding_dim, EMBEDDING_MODEL, _model_generation, _frozen_embedding_target, _runtime_policy_tier + from truememory.embedding_target import EmbeddingTarget + from truememory.tier_switch.serving import exclusive_activation + if type(target) is not EmbeddingTarget: + raise TypeError("Expected an immutable EmbeddingTarget") + expires = _target_deadline(timeout) + target.check_configuration() + with exclusive_activation(deadline=expires): + _runtime_policy_tier = target.tier + with _target_state_lock(expires): + if (_frozen_embedding_target is not None and _frozen_embedding_target.identity == target.identity + and _model is not None): + _frozen_embedding_target = target + return + _model = None + _model_generation += 1 + _frozen_embedding_target = target + EMBEDDING_MODEL, _embedding_dim = target.model_id, target.dimension + encoder = _load_frozen_embedding_target(target, timeout=_target_remaining(expires)) + with _target_state_lock(expires): + _model = encoder + + +@contextmanager +def _target_state_lock(expires: float | None) -> Iterator[None]: + remaining = _target_remaining(expires) + if remaining is None: + _lock.acquire() + elif not _lock.acquire(timeout=min(remaining, threading.TIMEOUT_MAX)): + raise TimeoutError("Prepared embedding deadline exceeded") + try: + _target_remaining(expires) + yield + finally: + _lock.release() + + +class _PreparedEmbeddingLease: + """An encode-only handle; closing drains work before releasing residency.""" + + def __init__( + self, target: EmbeddingTarget, encoder: object, *, shared: bool, + release_slot: Callable[[], None] | None = None, + ) -> None: + self._target = target + self._encoder = encoder + self._shared = shared + self._release_slot = release_slot + self._state_lock = threading.Lock() + self._encode_lock = threading.Lock() + self._closed = False + self._released = False + + @property + def target(self) -> EmbeddingTarget: + return self._target + + def encode( + self, texts: str | list[str], *, timeout: float | None = None, batch_size: int = 32, + ) -> np.ndarray: + from truememory.embedding_target import TARGET_PROBE_TEXT, check_target_vectors + from truememory.mps_utils import _own_model + + expires = _target_deadline(timeout) + if type(batch_size) is not int or batch_size <= 0: + raise ValueError("batch_size must be a positive integer") + if isinstance(texts, str): + texts = [texts] + texts = list(texts) + with self._state_lock: + if self._closed: + raise RuntimeError("Prepared embedding target is closed") + remaining = _target_remaining(expires) + if remaining is None: + self._encode_lock.acquire() + elif not self._encode_lock.acquire(timeout=min(remaining, threading.TIMEOUT_MAX)): + raise TimeoutError("Prepared embedding deadline exceeded") + encoder = None + try: + with self._state_lock: + if self._closed: + raise RuntimeError("Prepared embedding target is closed") + encoder = self._encoder + remaining = _target_remaining(expires) + if self._shared: + return encoder.encode(texts, timeout=remaining, batch_size=batch_size) + # The local tier worker owns its retry policy. Preserve exact-model + # exclusion without adding an independent native recovery loop. + with _own_model(encoder, remaining=lambda: _target_remaining(expires)): + self.target.check_configuration() + _target_remaining(expires) + probe = not texts + values = encoder.encode( + [TARGET_PROBE_TEXT] if probe else texts, + batch_size=batch_size, show_progress_bar=False, + ) + _target_remaining(expires) + values = np.asarray(values, dtype=np.float32) + check_target_vectors(self.target, values, 1 if probe else len(texts)) + return values[:0] if probe else values + finally: + encoder = None + with self._state_lock: + closed = self._closed + if closed: + self._finish_close() + self._encode_lock.release() + + def _finish_close(self) -> None: + # Called only by the encode-lock owner, including an encode that drains + # after its closing thread was cancelled while waiting. + self._encoder = None + release_slot, self._release_slot = self._release_slot, None + if release_slot is not None: + release_slot() + with self._state_lock: + self._released = True + + def close(self) -> None: + with self._state_lock: + if self._released: + return + # Queued calls must see closure before the current call drains. + self._closed = True + try: + with self._encode_lock: + self._finish_close() + except BaseException: + if self._encode_lock.acquire(blocking=False): + try: + self._finish_close() + finally: + self._encode_lock.release() + raise + + +def _target_deadline(timeout: float | None) -> float | None: + if timeout is None: + return None + if isinstance(timeout, bool) or not isinstance(timeout, (int, float)) or not math.isfinite(timeout): + raise ValueError("Prepared embedding timeout must be finite") + if timeout <= 0: + raise TimeoutError("Prepared embedding deadline exceeded") + return time.monotonic() + timeout + + +def _target_remaining(expires: float | None) -> float | None: + if expires is None: + return None + remaining = expires - time.monotonic() + if remaining <= 0: + raise TimeoutError("Prepared embedding deadline exceeded") + return remaining + + +@contextmanager +def prepare_embedding_target( + target: EmbeddingTarget, *, timeout: float | None = None, +) -> Iterator[_PreparedEmbeddingLease]: + """Prepare outside SQLite transactions; never activate a tier or expose weights.""" + from truememory.embedding_target import EmbeddingTarget, TARGET_PROBE_TEXT, build_target_model + from truememory.model_client import PreparedEmbeddingProxy, use_model_server + from truememory.mps_utils import resolve_device + + if not isinstance(target, EmbeddingTarget): + raise TypeError("Expected an immutable EmbeddingTarget") + expires = _target_deadline(timeout) + target.check_configuration() + shared = use_model_server() + owns_slot = False + encoder = lease = None + try: + if shared: + encoder = PreparedEmbeddingProxy(target) + encoder.prepare(timeout=_target_remaining(expires)) + else: + if not _prepared_target_slot.acquire(blocking=False): + raise RuntimeError("Another prepared embedding target lease is active") + owns_slot = True + remaining = _target_remaining(expires) + if remaining is None: + _lock.acquire() + elif not _lock.acquire(timeout=min(remaining, threading.TIMEOUT_MAX)): + raise TimeoutError("Prepared embedding deadline exceeded") + try: + _target_remaining(expires) + # Legacy custom loaders may have fallen back despite their + # label. Only deterministic built-ins can borrow active weights. + if (target.model_id in {"model2vec", "qwen3_256"} + and EMBEDDING_MODEL == target.model_id and _embedding_dim == target.dimension): + encoder = _model + finally: + _lock.release() + if encoder is None: + device = resolve_device(None) + _target_remaining(expires) + encoder = build_target_model(target, device, check=lambda: _target_remaining(expires)) + _target_remaining(expires) + lease = _PreparedEmbeddingLease( + target, encoder, shared=shared, + release_slot=_prepared_target_slot.release if owns_slot else None, + ) + owns_slot = False + encoder = None + if not shared: + lease.encode([TARGET_PROBE_TEXT], timeout=_target_remaining(expires)) + yield lease + finally: + encoder = None + if lease is not None: + lease.close() + elif owns_slot: + _prepared_target_slot.release() + + +def init_prepared_target_tables(conn: sqlite3.Connection, target: EmbeddingTarget) -> tuple[str, str]: + """Initialize only the named main-schema pair; preserve a caller transaction.""" + from truememory.embedding_target import EmbeddingTarget + import sqlite_vec + + if not isinstance(target, EmbeddingTarget): + raise TypeError("Expected an immutable EmbeddingTarget") + conn.enable_load_extension(True) + try: + sqlite_vec.load(conn) + finally: + conn.enable_load_extension(False) + owned = not conn.in_transaction + conn.execute("BEGIN IMMEDIATE" if owned else "SAVEPOINT prepared_embedding_target") + try: + for name in target.tables: + if conn.execute("SELECT 1 FROM temp.sqlite_master WHERE name = ? COLLATE NOCASE", (name,)).fetchone(): + raise ValueError("A temporary object shadows the prepared target table") + existing = conn.execute( + "SELECT type, sql FROM main.sqlite_master WHERE name = ?", (name,), + ).fetchone() + if existing is not None: + declaration = ( + r"CREATE\s+VIRTUAL\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?" + + r'(?:"' + name + r'"|`' + name + r'`|\[' + name + r'\]|' + name + r')' + + r"\s+USING\s+vec0\s*\(\s*embedding\s+float\[" + + str(target.dimension) + r"\]\s+distance_metric\s*=\s*cosine\s*\)\s*" + ) + if (existing[0] != "table" or not isinstance(existing[1], str) + or re.fullmatch(declaration, existing[1], re.IGNORECASE) is None): + raise ValueError("Existing table does not match the prepared target schema") + else: + conn.execute( + f'CREATE VIRTUAL TABLE main."{name}" USING vec0({_vec0_column_decl(target.dimension)})' + ) + if owned: + conn.commit() + else: + conn.execute("RELEASE SAVEPOINT prepared_embedding_target") + except BaseException: + if owned: + conn.rollback() + else: + conn.execute("ROLLBACK TO SAVEPOINT prepared_embedding_target") + conn.execute("RELEASE SAVEPOINT prepared_embedding_target") + raise + return target.tables + + # --------------------------------------------------------------------------- # Serialization helpers # --------------------------------------------------------------------------- @@ -342,22 +728,73 @@ def _write_embedder_metadata_no_commit(conn: sqlite3.Connection) -> None: metadata in the same transaction as clearing the in-progress marker, so the table is never "trusted" under stale metadata (issue #647). """ - _ensure_metadata_table(conn) - now = datetime.datetime.now(datetime.timezone.utc).isoformat() - conn.execute( - "INSERT OR REPLACE INTO metadata(key, value, updated_at) VALUES (?, ?, ?)", - ("embed_model", EMBEDDING_MODEL, now), - ) - conn.execute( - "INSERT OR REPLACE INTO metadata(key, value, updated_at) VALUES (?, ?, ?)", - ("embed_dim", str(_embedding_dim), now), - ) + from truememory.tier_switch.runtime import current_operation + operation = current_operation(conn) + if operation is not None and (operation.selection is not None or operation.policy is not None): + from truememory.tier_switch.writer import capture_writer_selection, require_writer_selection + require_writer_selection(conn, capture_writer_selection(conn), model_id=EMBEDDING_MODEL, + dimension=_embedding_dim, tables=operation.tables) + return + from truememory.tier_switch.runtime import legacy_vector_mutation + with legacy_vector_mutation(conn): + _ensure_metadata_table(conn) + now = datetime.datetime.now(datetime.timezone.utc).isoformat() + conn.execute( + "INSERT OR REPLACE INTO metadata(key, value, updated_at) VALUES (?, ?, ?)", + ("embed_model", EMBEDDING_MODEL, now), + ) + conn.execute( + "INSERT OR REPLACE INTO metadata(key, value, updated_at) VALUES (?, ?, ?)", + ("embed_dim", str(_embedding_dim), now), + ) def _write_embedder_metadata(conn: sqlite3.Connection) -> None: """Record `(embed_model, embed_dim)` so later opens can detect drift.""" - _write_embedder_metadata_no_commit(conn) - conn.commit() + from truememory.tier_switch.runtime import legacy_vector_mutation + with legacy_vector_mutation(conn): + _write_embedder_metadata_no_commit(conn) + conn.commit() + + +def _write_foreground_embedder_metadata_no_commit( + conn: sqlite3.Connection, identity: tuple[int, str, int], *, selection: WriterCapture, +) -> None: + """Publish identity inside the caller's model fence and database writer. + + Only add/update/embed_single call this after validating their vector target. + Their writer_transaction owns BEGIN IMMEDIATE or a writer-upgraded savepoint; + in_transaction alone cannot establish that ownership. Maintenance continues + independently, while rebuilds and standalone metadata writes keep its owner. + """ + from truememory.tier_switch.runtime import current_operation + from truememory.tier_switch.writer import capture_writer_selection, require_writer_selection + + operation = current_operation(conn) + if operation is None or selection != capture_writer_selection(conn): + raise VectorPublicationChanged("Foreground metadata requires the admitted writer selection") + if identity != (_model_generation, EMBEDDING_MODEL, _embedding_dim): + raise VectorPublicationChanged("Foreground metadata model identity changed") + require_writer_selection(conn, selection, model_id=identity[1], dimension=identity[2], + tables=operation.tables) + if operation.selection is not None or operation.policy is not None: + return + if operation.key[:3] != ("legacy", identity[1], str(identity[2])): + raise VectorPublicationChanged("Foreground metadata disagrees with serving admission") + matches: list[int | None] = [] + for key, value in (("embed_model", identity[1]), ("embed_dim", str(identity[2]))): + row = conn.execute( + "SELECT typeof(value)='text' AND value=? FROM main.metadata WHERE key=?", (value, key), + ).fetchone() + matches.append(None if row is None else row[0]) + if matches not in ([None, None], [1, 1]): + raise VectorPublicationChanged("Foreground metadata is partial or uses another embedding identity") + now = datetime.datetime.now(datetime.timezone.utc).isoformat() + for key, value in (("embed_model", identity[1]), ("embed_dim", str(identity[2]))): + conn.execute( + "INSERT OR REPLACE INTO main.metadata(key, value, updated_at) VALUES (?, ?, ?)", + (key, value, now), + ) # --------------------------------------------------------------------------- @@ -490,42 +927,44 @@ def _check_embedder_compatibility(conn: sqlite3.Connection) -> None: so that explicit re-embed flows (truememory_configure dropping + rebuilding) aren't blocked by stale metadata from the previous embedder. """ - existing_dim = _detect_existing_vec_dim(conn) - if existing_dim is None: - return # fresh DB or dropped-for-re-embed — nothing to protect - - stored_model, _stored_dim = _read_embedder_metadata(conn) - current_dim = _embedding_dim - - if existing_dim != current_dim: - stored_hint = ( - f" (stored embed_model={stored_model!r})" - if stored_model - else " (no metadata — likely a legacy v0.3.0 DB)" - ) - raise TrueMemoryMigrationError( - f"Database has a {existing_dim}d vec_messages table but the " - f"current embedder produces {current_dim}d vectors{stored_hint}. " - f"This commonly happens when upgrading between tiers with " - f"different embedding dimensions (e.g. v0.3.0 Pro @ 1024d → " - f"v0.4.0 Pro @ 256d). " + _migration_hint() - ) + from truememory.tier_switch.runtime import legacy_vector_mutation + with legacy_vector_mutation(conn): + existing_dim = _detect_existing_vec_dim(conn) + if existing_dim is None: + return # fresh DB or dropped-for-re-embed — nothing to protect + + stored_model, _stored_dim = _read_embedder_metadata(conn) + current_dim = _embedding_dim + + if existing_dim != current_dim: + stored_hint = ( + f" (stored embed_model={stored_model!r})" + if stored_model + else " (no metadata — likely a legacy v0.3.0 DB)" + ) + raise TrueMemoryMigrationError( + f"Database has a {existing_dim}d vec_messages table but the " + f"current embedder produces {current_dim}d vectors{stored_hint}. " + f"This commonly happens when upgrading between tiers with " + f"different embedding dimensions (e.g. v0.3.0 Pro @ 1024d → " + f"v0.4.0 Pro @ 256d). " + _migration_hint() + ) - if stored_model is not None and stored_model != EMBEDDING_MODEL: - raise TrueMemoryMigrationError( - f"Database was built with embed_model={stored_model!r}; current " - f"is {EMBEDDING_MODEL!r}. Matching dims ({current_dim}d) would " - f"otherwise mask a silent vector-space mismatch. " + _migration_hint() - ) + if stored_model is not None and stored_model != EMBEDDING_MODEL: + raise TrueMemoryMigrationError( + f"Database was built with embed_model={stored_model!r}; current " + f"is {EMBEDDING_MODEL!r}. Matching dims ({current_dim}d) would " + f"otherwise mask a silent vector-space mismatch. " + _migration_hint() + ) - if stored_model is None: - logger.warning( - "vec_messages exists without embedder metadata (legacy v0.3.0-style " - "DB). Current embedder=%r at %dd. If you have switched embedding " - "models since ingestion, re-embed via truememory_configure() — " - "otherwise new vectors will carry the %r marker going forward.", - EMBEDDING_MODEL, current_dim, EMBEDDING_MODEL, - ) + if stored_model is None: + logger.warning( + "vec_messages exists without embedder metadata (legacy v0.3.0-style " + "DB). Current embedder=%r at %dd. If you have switched embedding " + "models since ingestion, re-embed via truememory_configure() — " + "otherwise new vectors will carry the %r marker going forward.", + EMBEDDING_MODEL, current_dim, EMBEDDING_MODEL, + ) def _check_rebuild_allowed(conn: sqlite3.Connection) -> None: @@ -646,41 +1085,58 @@ def init_vec_table( conn: An open SQLite connection (from :func:`truememory.storage.create_db`). tier_group: Optional explicit tier group name ("edge" or "basepro"). """ - import sqlite_vec + import sys + runtime = sys.modules.get("truememory.tier_switch.runtime") + operation = runtime.current_operation(conn) if runtime is not None else None + if operation is not None and operation.selection is not None: + from truememory.tier_switch.activation import _guard_pair + if tier_group is not None and tier_group != operation.selection.target.tier_group: + raise ValueError("Selected initialization requires the captured table group") + _guard_pair(conn, operation.selection.target) + return + if operation is not None and operation.policy is not None: + from truememory.tier_switch.activation import _guard_legacy_policy_pair + if tier_group is not None and operation.tables != (f"vec_messages_{tier_group}", f"vec_messages_sep_{tier_group}"): + raise ValueError("Policy initialization requires the captured table pair") + _guard_legacy_policy_pair(conn, operation.policy.target, operation.tables) + return + from truememory.tier_switch.runtime import legacy_vector_mutation + with legacy_vector_mutation(conn): + import sqlite_vec - conn.enable_load_extension(True) - sqlite_vec.load(conn) - conn.enable_load_extension(False) + conn.enable_load_extension(True) + sqlite_vec.load(conn) + conn.enable_load_extension(False) - _ensure_metadata_table(conn) + _ensure_metadata_table(conn) - if tier_group: - if tier_group not in _VALID_GROUPS: - raise ValueError( - f"Invalid tier_group: {tier_group!r}. " - f"Valid: {sorted(_VALID_GROUPS)}" - ) - vec_name = f"vec_messages_{tier_group}" - sep_name = f"vec_messages_sep_{tier_group}" - else: - _check_embedder_compatibility(conn) - vec_name = _active_vec_table(conn) - sep_name = _active_sep_table(conn) + if tier_group: + if tier_group not in _VALID_GROUPS: + raise ValueError( + f"Invalid tier_group: {tier_group!r}. " + f"Valid: {sorted(_VALID_GROUPS)}" + ) + vec_name = f"vec_messages_{tier_group}" + sep_name = f"vec_messages_sep_{tier_group}" + else: + _check_embedder_compatibility(conn) + vec_name = _active_vec_table(conn) + sep_name = _active_sep_table(conn) - dim = _embedding_dim - conn.execute( - f"CREATE VIRTUAL TABLE IF NOT EXISTS {vec_name} " - f"USING vec0({_vec0_column_decl(dim)})" - ) - conn.execute( - f"CREATE VIRTUAL TABLE IF NOT EXISTS {sep_name} " - f"USING vec0({_vec0_column_decl(dim)})" - ) - conn.commit() + dim = _embedding_dim + conn.execute( + f"CREATE VIRTUAL TABLE IF NOT EXISTS {vec_name} " + f"USING vec0({_vec0_column_decl(dim)})" + ) + conn.execute( + f"CREATE VIRTUAL TABLE IF NOT EXISTS {sep_name} " + f"USING vec0({_vec0_column_decl(dim)})" + ) + conn.commit() - # Upgrade any pre-#631 L2-default tables to cosine in place. Idempotent: - # a no-op once the active tables already declare distance_metric=cosine. - migrate_to_cosine_metric(conn) + # Upgrade any pre-#631 L2-default tables to cosine in place. Idempotent: + # a no-op once the active tables already declare distance_metric=cosine. + migrate_to_cosine_metric(conn) # --------------------------------------------------------------------------- @@ -724,12 +1180,239 @@ def _encode_with_mps_fallback(model, texts, **kwargs): """Number of embedding batches to accumulate before committing. Each embedding batch is ``_get_batch_size()`` messages (100 on CPU, 16 on -MPS). Every ``_BUILD_VECTORS_TXN_BATCH`` of those batches we commit the -transaction and release the DB write lock so other writers (MCP servers, -ingest pipeline) can proceed. +MPS). Serialize a finite group before acquiring an owned DB writer transaction; +publish the group and consumed-input checkpoint in one commit. """ +def _capture_rebuild_model(conn: sqlite3.Connection | None = None) -> tuple[object, tuple[int, str, int]]: + """Caller transactions can borrow a cached model, but cannot wait or load.""" + if conn is not None and conn.in_transaction: + if not _lock.acquire(blocking=False): + raise VectorPublicationChanged("Embedding model is busy; retry outside the caller transaction") + try: + if _model is None: + raise VectorPublicationChanged("Embedding model is not loaded; load it outside the caller transaction") + return _model, (_model_generation, EMBEDDING_MODEL, _embedding_dim) + finally: + _lock.release() + with _lock: + before = (_model_generation, EMBEDDING_MODEL) + model = get_model() + with _lock: + if before != (_model_generation, EMBEDDING_MODEL): + raise VectorPublicationChanged("Embedding model changed while loading rebuild model") + return model, (_model_generation, EMBEDDING_MODEL, _embedding_dim) + + +@contextmanager +def _rebuild_model_fence(identity: tuple[int, str, int]) -> Iterator[None]: + # A writer must never wait for a model load that needs this state lock. + if not _lock.acquire(blocking=False): + raise RuntimeError("Embedding model is busy; retry rebuild publication") + try: + if identity != (_model_generation, EMBEDDING_MODEL, _embedding_dim): + raise RuntimeError("Embedding model changed during rebuild") + yield + finally: + _lock.release() + + +class VectorPublicationChanged(RuntimeError): + """Precomputed vectors no longer match the active publication target.""" + + +@contextmanager +def _foreground_model_fence(identity: tuple[int, str, int], *, blocking: bool = False) -> Iterator[None]: + """Blocking admission is permitted only before an owned writer starts.""" + if not _lock.acquire(blocking=blocking): + raise VectorPublicationChanged("Embedding model is busy; retry vector publication") + try: + if identity != (_model_generation, EMBEDDING_MODEL, _embedding_dim): + raise VectorPublicationChanged("Embedding model changed; retry vector publication") + yield + finally: + _lock.release() + + +def _validate_foreground_vector_target( + conn: sqlite3.Connection, identity: tuple[int, str, int], message_id: int, + *, selection=None, +) -> tuple[str, str]: + """Validate while the caller owns both the database writer and model fence.""" + from truememory.rebuild_source import RebuildSourceChanged, load_manifest, manifest_key + + tables = (_active_vec_table(conn), _active_sep_table(conn)) + from truememory.tier_switch.writer import WriterSelectionChanged, require_writer_selection + try: + require_writer_selection(conn, selection, model_id=identity[1], dimension=identity[2], tables=tables) + except WriterSelectionChanged as exc: + raise VectorPublicationChanged(str(exc)) from exc + for table in tables: + manifest = load_manifest(conn, manifest_key((table,))) + if manifest is not None and (manifest.model != identity[1] or manifest.dimension != identity[2]): + raise VectorPublicationChanged("Vector target uses a different embedding model; reopen with the active tier") + if not _build_in_progress(conn, table): + continue + if manifest is None: + raise VectorPublicationChanged("Vector target is rebuilding without matching source identity") + if manifest.source is None: + # Explicit caller input has no database prefix certificate. + raise VectorPublicationChanged("Vector target is rebuilding explicit input; retry after completion") + high = manifest.source.high_id + if high is None or message_id > high: + continue + try: + manifest.source.check(conn) + except RebuildSourceChanged: + # A corrected source invalidates this generation. Publish the + # correction normally; the rebuild cannot later certify it. + continue + raise VectorPublicationChanged("Vector row belongs to an active rebuild; retry after completion") + return tables + + +@contextmanager +def _foreground_vector_publication( + conn: sqlite3.Connection, identity: tuple[int, str, int], message_id: int, + *, selection=None, +) -> Iterator[tuple[str, str]]: + from truememory.tier_switch.writer import WriterSelectionChanged, writer_transaction + + with _foreground_model_fence(identity, blocking=not conn.in_transaction): + try: + with writer_transaction(conn, selection, model_id=identity[1], dimension=identity[2]): + yield _validate_foreground_vector_target(conn, identity, message_id, selection=selection) + except WriterSelectionChanged as exc: + raise VectorPublicationChanged(str(exc)) from exc + + +def _build_streamed_vectors( + conn: sqlite3.Connection, messages: list[dict] | None, *, table_name: str | None, + txn_batch: int | None, separation: bool, +) -> int: + from truememory.maintenance import connection_database_path, maintenance_owner + from truememory.tier_switch.writer import capture_writer_selection, require_writer_selection + from truememory.rebuild_source import ( + RebuildManifest, RebuildSourceChanged, capture_source, input_fingerprint, + ensure_rebuild_tracking, load_manifest, manifest_key, rebuild_transaction, save_manifest, + ) + + if messages is not None and not messages: + return 0 + write_selection = capture_writer_selection(conn) + table = table_name or (_active_sep_table(conn) if separation else _active_vec_table(conn)) + publication_tables = None if write_selection.selection is None else ( + (write_selection.selection.tables[0], table) if separation else (table, write_selection.selection.tables[1]) + ) + quoted_table = '"' + table.replace('"', '""') + '"' + key = manifest_key((table,)) + digest = input_fingerprint(messages, separation=separation) if messages is not None else None + with maintenance_owner(connection_database_path(conn)): + source_empty = False + if messages is None: + ensure_rebuild_tracking(conn) + with closing(conn.execute("SELECT 1 FROM messages LIMIT 1")) as cursor: + source_empty = cursor.fetchone() is None + if source_empty: + identity = (_model_generation, EMBEDDING_MODEL, _embedding_dim) + with rebuild_transaction(conn, write=True, publication_fence=_rebuild_model_fence(identity)): + require_writer_selection(conn, write_selection, model_id=identity[1], dimension=identity[2], tables=publication_tables) + empty_source = capture_source(conn) + if empty_source.total == 0: + # An empty range needs no native model allocation. Recheck + # under the writer so an append cannot slip through clear. + conn.execute(f"DELETE FROM {quoted_table}") + _ensure_metadata_table(conn) + empty = RebuildManifest.new(identity[1], identity[2], (table,), source=empty_source) + empty = replace(empty, complete=True, + schema_version=conn.execute("PRAGMA schema_version").fetchone()[0]) + save_manifest(conn, key, empty) + _clear_build_in_progress(conn, table) + return 0 + model, identity = _capture_rebuild_model(conn) + with rebuild_transaction(conn, write=True, publication_fence=_rebuild_model_fence(identity)): + require_writer_selection(conn, write_selection, model_id=identity[1], dimension=identity[2], tables=publication_tables) + manifest = load_manifest(conn, key) + resumable = (manifest is not None and not manifest.complete + and manifest.model == identity[1] and manifest.dimension == identity[2] + and manifest.targets == (table,) and manifest.input_digest == digest + and manifest.schema_version == conn.execute("PRAGMA schema_version").fetchone()[0] + and (manifest.source is not None) == (messages is None)) + if resumable and manifest.source is not None: + try: + manifest.source.check(conn) + except RebuildSourceChanged: + resumable = False + if not resumable: + source = capture_source(conn) if messages is None else None + manifest = RebuildManifest.new(identity[1], identity[2], (table,), source=source, + input_digest=digest, total=len(messages) if messages is not None else 0) + conn.execute(f"DELETE FROM {quoted_table}") + _mark_build_in_progress(conn, table) + manifest = replace(manifest, schema_version=conn.execute("PRAGMA schema_version").fetchone()[0]) + save_manifest(conn, key, manifest) + + batch_size = _get_batch_size() + commit_every = max(1, txn_batch if txn_batch is not None else _BUILD_VECTORS_TXN_BATCH) + total = 0 + try: + import torch + no_grad = torch.no_grad() + except ImportError: + from contextlib import nullcontext + no_grad = nullcontext() + + with no_grad: + while manifest.consumed < manifest.total: + next_manifest = manifest + rows_to_insert = [] + for _ in range(commit_every): + if next_manifest.consumed >= next_manifest.total: + break + if manifest.source is not None: + batch = manifest.source.page(conn, next_manifest.cursor, batch_size, include_metadata=separation) + else: + batch = messages[next_manifest.consumed:next_manifest.consumed + batch_size] + if not batch: + raise RebuildSourceChanged("Rebuild source ended before its captured row count") + ids = [message["id"] for message in batch] + texts = ([_build_sep_text(message.get("sender", "?"), message.get("recipient", "?"), + message.get("timestamp", "?"), message["content"]) for message in batch] + if separation else [message["content"] for message in batch]) + embeddings = _encode_with_mps_fallback(model, texts, show_progress_bar=False) + if len(embeddings) != len(ids): + raise ValueError("Embedding count does not match the rebuild batch") + valid = 0 + for mid, embedding in zip(ids, embeddings): + normed = _normalize_for_cosine(embedding) + if normed is None: + logger.warning("Skipping undefined cosine embedding during rebuild") + continue + rows_to_insert.append((mid, serialize_f32(normed))) + valid += 1 + next_manifest = next_manifest.advance(ids[-1], len(batch), valid) + del embeddings, batch, texts, ids + _flush_mps_cache() + with rebuild_transaction(conn, write=True, publication_fence=_rebuild_model_fence(identity)): + require_writer_selection(conn, write_selection, model_id=identity[1], dimension=identity[2], tables=publication_tables) + manifest.check(conn, key) + if rows_to_insert: + conn.executemany(f"INSERT INTO {quoted_table}(rowid, embedding) VALUES (?, ?)", rows_to_insert) + save_manifest(conn, key, next_manifest) + total += next_manifest.outputs - manifest.outputs + manifest = next_manifest + del rows_to_insert + + with rebuild_transaction(conn, write=True, publication_fence=_rebuild_model_fence(identity)): + require_writer_selection(conn, write_selection, model_id=identity[1], dimension=identity[2], tables=publication_tables) + manifest.check(conn, key) + save_manifest(conn, key, replace(manifest, complete=True)) + _clear_build_in_progress(conn, table) + _write_embedder_metadata_no_commit(conn) + return total + + def build_vectors( conn: sqlite3.Connection, messages: list[dict] | None = None, @@ -744,14 +1427,13 @@ def build_vectors( ``messages`` table. Otherwise it uses the supplied list (each dict must have an ``"id"`` and ``"content"`` key). - Batch size adapts to the device: 100 on CPU (Edge tier), 16 on MPS - (Base/Pro tier) to keep GPU memory under ~4GB on 8GB machines. - MPS cache is flushed between batches to prevent memory accumulation. + Database inputs are read in bounded ID pages. A source revision and model + fence protect every publication. Resume uses consumed input, including + skipped invalid vectors, rather than the maximum stored vector ID. - The write transaction is committed every *txn_batch* embedding batches - (default ``_BUILD_VECTORS_TXN_BATCH`` = 100) so that the DB write lock - is not held for the entire re-embedding run. This lets other writers - (MCP servers, ingest pipeline) make progress between commits. + Serialize *txn_batch* embedding batches (default 100) before acquiring an + owned writer transaction. Caller-owned transactions remain caller-owned; + their existing locks cannot be released by this function. Args: conn: Open database connection with sqlite-vec already loaded @@ -765,108 +1447,19 @@ def build_vectors( Returns: Number of vectors inserted. """ - if messages is None: - rows = conn.execute( - "SELECT id, content FROM messages ORDER BY id" - ).fetchall() - messages = [{"id": row[0], "content": row[1]} for row in rows] - - if not messages: - return 0 - - tbl = table_name or _active_vec_table(conn) - - # Resume vs. fresh build (issue #647, M-21). - # - # If a prior run was interrupted it left an ``in_progress`` marker. The - # rows already written are valid (each is L2-normalized at write time), so - # we resume from ``max(rowid)`` instead of re-wiping — no data confusion, - # no redundant re-embedding. On a fresh build we wipe and set the marker in - # the SAME transaction, so a crash before completion never leaves an - # untracked partial/empty table that engine.open() would trust. - resume_after = 0 - if _build_in_progress(conn, tbl): - try: - resume_after = conn.execute( - f"SELECT MAX(rowid) FROM {tbl}" - ).fetchone()[0] or 0 - except sqlite3.OperationalError: - resume_after = 0 - if resume_after > 0: - messages = [m for m in messages if m["id"] > resume_after] - if not messages: - # Everything was already embedded before the interruption; just - # finalize (clear marker + write metadata) so the table is trusted. - _clear_build_in_progress(conn, tbl) - _write_embedder_metadata(conn) - return 0 - else: - conn.execute(f"DELETE FROM {tbl}") - _mark_build_in_progress(conn, tbl) - conn.commit() # DELETE + marker land atomically - - model = get_model() - batch_size = _get_batch_size() - commit_every = txn_batch if txn_batch is not None else _BUILD_VECTORS_TXN_BATCH - total = 0 - batches_since_commit = 0 - - try: - import torch - no_grad = torch.no_grad() - except ImportError: - from contextlib import nullcontext - no_grad = nullcontext() - - with no_grad: - for start in range(0, len(messages), batch_size): - batch = messages[start : start + batch_size] - texts = [m["content"] for m in batch] - ids = [m["id"] for m in batch] - - embeddings = _encode_with_mps_fallback(model, texts, show_progress_bar=False) - - rows_to_insert = [] - for mid, emb in zip(ids, embeddings): - normed = _normalize_for_cosine(emb) - if normed is None: - # Zero/NaN vector: undefined cosine — skip so it can't - # poison the cosine table with NULL distances (C2-8). - logger.warning( - "Skipping message %s: zero-norm/non-finite embedding " - "(undefined cosine)", mid, - ) - continue - rows_to_insert.append((mid, serialize_f32(normed))) - if rows_to_insert: - conn.executemany( - f"INSERT INTO {tbl}(rowid, embedding) VALUES (?, ?)", - rows_to_insert, - ) - - total += len(rows_to_insert) - batches_since_commit += 1 - del embeddings - _flush_mps_cache() - - if batches_since_commit >= commit_every: - conn.commit() - batches_since_commit = 0 + return _build_streamed_vectors(conn, messages, table_name=table_name, txn_batch=txn_batch, + separation=False) - # Final commit: clear the in-progress marker and stamp embedder metadata in - # the SAME transaction so the table is only ever "trusted" once it is fully - # built under the current embedder (issue #647: no NEW vectors under OLD - # metadata, no empty/partial table accepted as built). - _clear_build_in_progress(conn, tbl) - _write_embedder_metadata_no_commit(conn) - conn.commit() - return total # --------------------------------------------------------------------------- # Vector search # --------------------------------------------------------------------------- +from truememory.tier_switch.runtime import database_operation + + +@database_operation def search_vector( conn: sqlite3.Connection, query: str, @@ -959,6 +1552,7 @@ def search_vector( return results[:limit] +@database_operation def search_vector_raw( conn: sqlite3.Connection, query: str, @@ -1054,102 +1648,8 @@ def build_separation_vectors( Returns: Number of separation vectors inserted. """ - if messages is None: - rows = conn.execute( - "SELECT id, content, sender, recipient, timestamp FROM messages ORDER BY id" - ).fetchall() - messages = [ - {"id": r[0], "content": r[1], "sender": r[2], "recipient": r[3], "timestamp": r[4]} - for r in rows - ] - - if not messages: - return 0 - - tbl = table_name or _active_sep_table(conn) - - # Resume vs. fresh build — same durability contract as build_vectors - # (issue #647, M-21 + M-45). Marker + DELETE land atomically; batched - # commits below release the write lock during the run (#619 treatment). - resume_after = 0 - if _build_in_progress(conn, tbl): - try: - resume_after = conn.execute( - f"SELECT MAX(rowid) FROM {tbl}" - ).fetchone()[0] or 0 - except sqlite3.OperationalError: - resume_after = 0 - if resume_after > 0: - messages = [m for m in messages if m["id"] > resume_after] - if not messages: - _clear_build_in_progress(conn, tbl) - _write_embedder_metadata(conn) - return 0 - else: - conn.execute(f"DELETE FROM {tbl}") - _mark_build_in_progress(conn, tbl) - conn.commit() # DELETE + marker land atomically - - model = get_model() - batch_size = _get_batch_size() - commit_every = txn_batch if txn_batch is not None else _BUILD_VECTORS_TXN_BATCH - total = 0 - batches_since_commit = 0 - - try: - import torch - no_grad = torch.no_grad() - except ImportError: - from contextlib import nullcontext - no_grad = nullcontext() - - with no_grad: - for start in range(0, len(messages), batch_size): - batch = messages[start : start + batch_size] - - texts = [] - ids = [] - for m in batch: - sep_text = _build_sep_text( - m.get("sender", "?"), m.get("recipient", "?"), - m.get("timestamp", "?"), m["content"], - ) - texts.append(sep_text) - ids.append(m["id"]) - - embeddings = _encode_with_mps_fallback(model, texts, show_progress_bar=False) - - rows_to_insert = [] - for mid, emb in zip(ids, embeddings): - normed = _normalize_for_cosine(emb) - if normed is None: - logger.warning( - "Skipping separation vector for message %s: " - "zero-norm/non-finite embedding (undefined cosine)", mid, - ) - continue - rows_to_insert.append((mid, serialize_f32(normed))) - if rows_to_insert: - conn.executemany( - f"INSERT INTO {tbl}(rowid, embedding) VALUES (?, ?)", - rows_to_insert, - ) - - total += len(rows_to_insert) - batches_since_commit += 1 - del embeddings - _flush_mps_cache() - - if batches_since_commit >= commit_every: - conn.commit() - batches_since_commit = 0 - - # Final commit: clear the in-progress marker + stamp embedder metadata - # atomically (issue #647). - _clear_build_in_progress(conn, tbl) - _write_embedder_metadata_no_commit(conn) - conn.commit() - return total + return _build_streamed_vectors(conn, messages, table_name=table_name, txn_batch=txn_batch, + separation=True) def _build_sep_text(sender: str, recipient: str, timestamp: str, content: str) -> str: @@ -1159,6 +1659,7 @@ def _build_sep_text(sender: str, recipient: str, timestamp: str, content: str) - ) +@database_operation def embed_single(conn: sqlite3.Connection, message_id: int, content: str) -> None: """ Embed a single message and insert into ``vec_messages`` and ``vec_messages_sep``. @@ -1173,50 +1674,43 @@ def embed_single(conn: sqlite3.Connection, message_id: int, content: str) -> Non message_id: The ``messages.id`` of the row being embedded. content: The text to embed. """ - model = get_model() - embedding = _encode_with_mps_fallback(model, [content])[0] # shape (dim,) + from truememory.tier_switch.writer import capture_writer_selection + write_selection = capture_writer_selection(conn) + model, identity = _capture_rebuild_model(conn) + embedding = _encode_with_mps_fallback(model, [content])[0] normed = _normalize_for_cosine(embedding) if normed is None: - # Zero/NaN vector has undefined cosine — skip rather than poison the - # cosine table with NULL distances (C2-8). - logger.warning( - "Skipping message %d: zero-norm/non-finite embedding " - "(undefined cosine)", message_id, - ) + logger.warning("Skipping undefined cosine embedding during incremental publication") return - vec_tbl = _active_vec_table(conn) - conn.execute( - f"INSERT INTO {vec_tbl}(rowid, embedding) VALUES (?, ?)", - (message_id, serialize_f32(normed)), - ) - + completion = serialize_f32(normed) + separation = None try: row = conn.execute( - "SELECT sender, recipient, timestamp FROM messages WHERE id = ?", - (message_id,), + "SELECT sender, recipient, timestamp FROM messages WHERE id = ?", (message_id,), ).fetchone() if row: sep_text = _build_sep_text(row[0], row[1], row[2], content) sep_embedding = _encode_with_mps_fallback(model, [sep_text])[0] sep_normed = _normalize_for_cosine(sep_embedding) - if sep_normed is None: - logger.warning( - "Skipping separation vector for message %d: " - "zero-norm/non-finite embedding (undefined cosine)", - message_id, - ) - return - sep_tbl = _active_sep_table(conn) - conn.execute( - f"INSERT INTO {sep_tbl}(rowid, embedding) VALUES (?, ?)", - (message_id, serialize_f32(sep_normed)), - ) - except Exception: - logger.warning("Failed to create separation vector for message %d", message_id, exc_info=True) - _write_embedder_metadata(conn) - # Caller is responsible for committing + if sep_normed is not None: + separation = serialize_f32(sep_normed) + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) + logger.warning("Failed to prepare separation vector during incremental publication", exc_info=True) + with _foreground_vector_publication(conn, identity, message_id, selection=write_selection) as (vec_tbl, sep_tbl): + conn.execute(f"INSERT INTO {vec_tbl}(rowid, embedding) VALUES (?, ?)", (message_id, completion)) + if separation is not None: + try: + conn.execute(f"INSERT INTO {sep_tbl}(rowid, embedding) VALUES (?, ?)", (message_id, separation)) + except Exception as _serving_error: + from truememory.tier_switch.runtime import raise_if_serving_rejection + raise_if_serving_rejection(_serving_error) + logger.warning("Failed to create separation vector during incremental publication", exc_info=True) + _write_foreground_embedder_metadata_no_commit(conn, identity, selection=write_selection) +@database_operation def search_vector_separation( conn: sqlite3.Connection, query: str, @@ -1333,61 +1827,63 @@ def _rebuild_table_as_cosine(conn: sqlite3.Connection, table_name: str) -> int: Returns the number of vectors carried over. """ - dim = _detect_existing_vec_dim(conn, table_name) - if dim is None: - return 0 + from truememory.tier_switch.runtime import legacy_vector_mutation + with legacy_vector_mutation(conn): + dim = _detect_existing_vec_dim(conn, table_name) + if dim is None: + return 0 - stage = f"{table_name}_cos_stage" - stage_done = f"{stage}_done" - fmt = f"{dim}f" - - # Phase 1: copy normalized vectors into a durable plain staging table. - # D1-2 crash-safety: a separate "done" marker row is written in the SAME - # commit as the staged rows. An interrupted stage (crash mid-copy) leaves an - # empty/partial stage with NO done marker, so resume can tell it apart from a - # complete stage and re-stage from the still-intact original instead of - # swapping an empty stage over it and losing every vector. - conn.execute(f"DROP TABLE IF EXISTS {stage}") - conn.execute(f"DROP TABLE IF EXISTS {stage_done}") - conn.execute( - f"CREATE TABLE {stage} (rowid INTEGER PRIMARY KEY, embedding BLOB)" - ) - conn.execute(f"CREATE TABLE {stage_done} (done INTEGER PRIMARY KEY)") - rows = conn.execute( - f"SELECT rowid, embedding FROM {table_name}" - ).fetchall() - skipped = 0 - staged = 0 - for rowid, blob in rows: - if blob is None or len(blob) != dim * 4: - skipped += 1 - continue - normed = _normalize_for_cosine(struct.unpack(fmt, blob)) - if normed is None: - skipped += 1 - continue + stage = f"{table_name}_cos_stage" + stage_done = f"{stage}_done" + fmt = f"{dim}f" + + # Phase 1: copy normalized vectors into a durable plain staging table. + # D1-2 crash-safety: a separate "done" marker row is written in the SAME + # commit as the staged rows. An interrupted stage (crash mid-copy) leaves an + # empty/partial stage with NO done marker, so resume can tell it apart from a + # complete stage and re-stage from the still-intact original instead of + # swapping an empty stage over it and losing every vector. + conn.execute(f"DROP TABLE IF EXISTS {stage}") + conn.execute(f"DROP TABLE IF EXISTS {stage_done}") conn.execute( - f"INSERT INTO {stage}(rowid, embedding) VALUES (?, ?)", - (rowid, serialize_f32(normed)), + f"CREATE TABLE {stage} (rowid INTEGER PRIMARY KEY, embedding BLOB)" ) - staged += 1 - # Mark staging complete in the SAME transaction as the staged rows, so the - # marker is durable iff every row is. - conn.execute(f"INSERT INTO {stage_done}(done) VALUES (1)") - conn.commit() # staged rows + completeness marker durable before we touch the original - - # Phase 2: replace the vec0 table with a cosine one and refill it. - carried = _finish_cosine_swap(conn, table_name, dim) - - if skipped: - logger.warning( - "Cosine migration for %s dropped %d zero-norm/invalid vector(s)", - table_name, skipped, + conn.execute(f"CREATE TABLE {stage_done} (done INTEGER PRIMARY KEY)") + rows = conn.execute( + f"SELECT rowid, embedding FROM {table_name}" + ).fetchall() + skipped = 0 + staged = 0 + for rowid, blob in rows: + if blob is None or len(blob) != dim * 4: + skipped += 1 + continue + normed = _normalize_for_cosine(struct.unpack(fmt, blob)) + if normed is None: + skipped += 1 + continue + conn.execute( + f"INSERT INTO {stage}(rowid, embedding) VALUES (?, ?)", + (rowid, serialize_f32(normed)), + ) + staged += 1 + # Mark staging complete in the SAME transaction as the staged rows, so the + # marker is durable iff every row is. + conn.execute(f"INSERT INTO {stage_done}(done) VALUES (1)") + conn.commit() # staged rows + completeness marker durable before we touch the original + + # Phase 2: replace the vec0 table with a cosine one and refill it. + carried = _finish_cosine_swap(conn, table_name, dim) + + if skipped: + logger.warning( + "Cosine migration for %s dropped %d zero-norm/invalid vector(s)", + table_name, skipped, + ) + logger.info( + "Rebuilt %s as cosine (%d vectors carried over)", table_name, carried ) - logger.info( - "Rebuilt %s as cosine (%d vectors carried over)", table_name, carried - ) - return carried + return carried def _finish_cosine_swap( @@ -1398,24 +1894,26 @@ def _finish_cosine_swap( Assumes ``{table_name}_cos_stage`` exists and is populated. Idempotent / resumable: safe to call after a crash that left the staging table behind. """ - stage = f"{table_name}_cos_stage" - conn.execute(f"DROP TABLE IF EXISTS {table_name}") - conn.execute( - f"CREATE VIRTUAL TABLE {table_name} USING vec0({_vec0_column_decl(dim)})" - ) - carried = 0 - staged_rows = conn.execute( - f"SELECT rowid, embedding FROM {stage}" - ).fetchall() - if staged_rows: - conn.executemany( - f"INSERT INTO {table_name}(rowid, embedding) VALUES (?, ?)", - staged_rows, + from truememory.tier_switch.runtime import legacy_vector_mutation + with legacy_vector_mutation(conn): + stage = f"{table_name}_cos_stage" + conn.execute(f"DROP TABLE IF EXISTS {table_name}") + conn.execute( + f"CREATE VIRTUAL TABLE {table_name} USING vec0({_vec0_column_decl(dim)})" ) - carried = len(staged_rows) - conn.execute(f"DROP TABLE IF EXISTS {stage}") - conn.execute(f"DROP TABLE IF EXISTS {stage}_done") - return carried + carried = 0 + staged_rows = conn.execute( + f"SELECT rowid, embedding FROM {stage}" + ).fetchall() + if staged_rows: + conn.executemany( + f"INSERT INTO {table_name}(rowid, embedding) VALUES (?, ?)", + staged_rows, + ) + carried = len(staged_rows) + conn.execute(f"DROP TABLE IF EXISTS {stage}") + conn.execute(f"DROP TABLE IF EXISTS {stage}_done") + return carried def migrate_to_cosine_metric(conn: sqlite3.Connection) -> bool: @@ -1432,67 +1930,69 @@ def migrate_to_cosine_metric(conn: sqlite3.Connection) -> bool: """ # Ensure the extension is loaded (callers from engine.open already load it, # but init_vec_table is the common entry and loads it just above). - try: - import sqlite_vec - conn.enable_load_extension(True) - sqlite_vec.load(conn) - conn.enable_load_extension(False) - except Exception: - # If the extension cannot load, there are no vec0 tables to migrate. - return False + from truememory.tier_switch.runtime import legacy_vector_mutation + with legacy_vector_mutation(conn): + try: + import sqlite_vec + conn.enable_load_extension(True) + sqlite_vec.load(conn) + conn.enable_load_extension(False) + except Exception: + # If the extension cannot load, there are no vec0 tables to migrate. + return False - migrated = False - for table in _KNOWN_VEC_TABLES: - stage = f"{table}_cos_stage" - has_stage = conn.execute( - "SELECT 1 FROM sqlite_master WHERE name = ? AND type='table'", - (stage,), - ).fetchone() - if has_stage: - # A prior run was interrupted between staging and swap. Only finish - # the swap if the staging completed durably (the "done" marker is - # present); otherwise the stage is empty/partial and the ORIGINAL - # table is still intact, so drop the partial stage and re-stage from - # scratch below rather than swapping an empty stage over the original - # and losing every vector (D1-2). - stage_done = f"{stage}_done" - stage_complete = bool( - conn.execute( - "SELECT 1 FROM sqlite_master WHERE name = ? AND type='table'", - (stage_done,), - ).fetchone() - and conn.execute(f"SELECT 1 FROM {stage_done} LIMIT 1").fetchone() - ) - if stage_complete: - dim = ( - _detect_existing_vec_dim(conn, table) - or _detect_existing_vec_dim(conn, stage) - or _embedding_dim + migrated = False + for table in _KNOWN_VEC_TABLES: + stage = f"{table}_cos_stage" + has_stage = conn.execute( + "SELECT 1 FROM sqlite_master WHERE name = ? AND type='table'", + (stage,), + ).fetchone() + if has_stage: + # A prior run was interrupted between staging and swap. Only finish + # the swap if the staging completed durably (the "done" marker is + # present); otherwise the stage is empty/partial and the ORIGINAL + # table is still intact, so drop the partial stage and re-stage from + # scratch below rather than swapping an empty stage over the original + # and losing every vector (D1-2). + stage_done = f"{stage}_done" + stage_complete = bool( + conn.execute( + "SELECT 1 FROM sqlite_master WHERE name = ? AND type='table'", + (stage_done,), + ).fetchone() + and conn.execute(f"SELECT 1 FROM {stage_done} LIMIT 1").fetchone() ) - _finish_cosine_swap(conn, table, dim) + if stage_complete: + dim = ( + _detect_existing_vec_dim(conn, table) + or _detect_existing_vec_dim(conn, stage) + or _embedding_dim + ) + _finish_cosine_swap(conn, table, dim) + conn.commit() + migrated = True + continue + # Incomplete stage from an interrupted copy — discard it and fall + # through to a fresh rebuild from the intact original table. + conn.execute(f"DROP TABLE IF EXISTS {stage}") + conn.execute(f"DROP TABLE IF EXISTS {stage_done}") conn.commit() - migrated = True - continue - # Incomplete stage from an interrupted copy — discard it and fall - # through to a fresh rebuild from the intact original table. - conn.execute(f"DROP TABLE IF EXISTS {stage}") - conn.execute(f"DROP TABLE IF EXISTS {stage_done}") - conn.commit() - exists = conn.execute( - "SELECT 1 FROM sqlite_master WHERE name = ? AND type='table'", - (table,), - ).fetchone() - if not exists: - continue - if _table_uses_cosine(conn, table): - continue - _rebuild_table_as_cosine(conn, table) - migrated = True + exists = conn.execute( + "SELECT 1 FROM sqlite_master WHERE name = ? AND type='table'", + (table,), + ).fetchone() + if not exists: + continue + if _table_uses_cosine(conn, table): + continue + _rebuild_table_as_cosine(conn, table) + migrated = True - if migrated: - conn.commit() - return migrated + if migrated: + conn.commit() + return migrated # --------------------------------------------------------------------------- @@ -1512,82 +2012,84 @@ def migrate_legacy_vec_tables(conn: sqlite3.Connection) -> bool: Returns True if migration occurred, False if nothing to migrate. """ - from truememory.tier_switch.cache import VectorCacheRegistry - - has_old = conn.execute( - "SELECT name FROM sqlite_master WHERE name='vec_messages' AND type='table'" - ).fetchone() - if not has_old: - return False + from truememory.tier_switch.runtime import legacy_vector_mutation + with legacy_vector_mutation(conn): + from truememory.tier_switch.cache import VectorCacheRegistry - stored_model, _ = _read_embedder_metadata(conn) - group = _MODEL_TO_GROUP.get(stored_model, "edge") - if group not in _VALID_GROUPS: - group = "edge" - - new_vec = f"vec_messages_{group}" - new_sep = f"vec_messages_sep_{group}" - - count = conn.execute("SELECT COUNT(*) FROM vec_messages").fetchone()[0] - if count == 0: - has_tiered = conn.execute( - "SELECT name FROM sqlite_master WHERE name=? AND type='table'", - (new_vec,), + has_old = conn.execute( + "SELECT name FROM sqlite_master WHERE name='vec_messages' AND type='table'" ).fetchone() - if not has_tiered: + if not has_old: return False - conn.execute("DROP TABLE IF EXISTS vec_messages") - conn.execute("DROP TABLE IF EXISTS vec_messages_sep") - conn.commit() - return True - dim = _detect_existing_vec_dim(conn, "vec_messages") or 256 + stored_model, _ = _read_embedder_metadata(conn) + group = _MODEL_TO_GROUP.get(stored_model, "edge") + if group not in _VALID_GROUPS: + group = "edge" - import sqlite_vec + new_vec = f"vec_messages_{group}" + new_sep = f"vec_messages_sep_{group}" - conn.enable_load_extension(True) - sqlite_vec.load(conn) - conn.enable_load_extension(False) + count = conn.execute("SELECT COUNT(*) FROM vec_messages").fetchone()[0] + if count == 0: + has_tiered = conn.execute( + "SELECT name FROM sqlite_master WHERE name=? AND type='table'", + (new_vec,), + ).fetchone() + if not has_tiered: + return False + conn.execute("DROP TABLE IF EXISTS vec_messages") + conn.execute("DROP TABLE IF EXISTS vec_messages_sep") + conn.commit() + return True - conn.execute( - f"CREATE VIRTUAL TABLE IF NOT EXISTS {new_vec} " - f"USING vec0({_vec0_column_decl(dim)})" - ) - conn.execute( - f"CREATE VIRTUAL TABLE IF NOT EXISTS {new_sep} " - f"USING vec0({_vec0_column_decl(dim)})" - ) + dim = _detect_existing_vec_dim(conn, "vec_messages") or 256 - conn.execute(f"INSERT INTO {new_vec} SELECT * FROM vec_messages") - try: - conn.execute(f"INSERT INTO {new_sep} SELECT * FROM vec_messages_sep") - except sqlite3.OperationalError: - pass + import sqlite_vec - conn.execute("DROP TABLE vec_messages") - try: - conn.execute("DROP TABLE vec_messages_sep") - except sqlite3.OperationalError: - pass + conn.enable_load_extension(True) + sqlite_vec.load(conn) + conn.enable_load_extension(False) - max_id = conn.execute( - f"SELECT MAX(rowid) FROM {new_vec}" - ).fetchone()[0] or 0 - - model_map = {"edge": "potion-base-8M", "basepro": "Qwen3-Embedding-0.6B"} - VectorCacheRegistry.set( - conn, - group, - vec_table=new_vec, - sep_table=new_sep, - last_embedded_id=max_id, - vector_count=count, - model_name=model_map.get(group, "potion-base-8M"), - embedding_dim=dim, - ) + conn.execute( + f"CREATE VIRTUAL TABLE IF NOT EXISTS {new_vec} " + f"USING vec0({_vec0_column_decl(dim)})" + ) + conn.execute( + f"CREATE VIRTUAL TABLE IF NOT EXISTS {new_sep} " + f"USING vec0({_vec0_column_decl(dim)})" + ) - logger.info( - "Migrated %d vectors from vec_messages → %s (group=%s)", - count, new_vec, group, - ) - return True + conn.execute(f"INSERT INTO {new_vec} SELECT * FROM vec_messages") + try: + conn.execute(f"INSERT INTO {new_sep} SELECT * FROM vec_messages_sep") + except sqlite3.OperationalError: + pass + + conn.execute("DROP TABLE vec_messages") + try: + conn.execute("DROP TABLE vec_messages_sep") + except sqlite3.OperationalError: + pass + + max_id = conn.execute( + f"SELECT MAX(rowid) FROM {new_vec}" + ).fetchone()[0] or 0 + + model_map = {"edge": "potion-base-8M", "basepro": "Qwen3-Embedding-0.6B"} + VectorCacheRegistry.set( + conn, + group, + vec_table=new_vec, + sep_table=new_sep, + last_embedded_id=max_id, + vector_count=count, + model_name=model_map.get(group, "potion-base-8M"), + embedding_dim=dim, + ) + + logger.info( + "Migrated %d vectors from vec_messages → %s (group=%s)", + count, new_vec, group, + ) + return True From 8402d0b180752fed9499444873b67fb82e371ea6 Mon Sep 17 00:00:00 2001 From: buildingjoshbetter <236675935+buildingjoshbetter@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:26:07 -0500 Subject: [PATCH 02/16] fix: Canonicalize synthetic Hermes fixture paths --- tests/test-hermes-native-bridge-765.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test-hermes-native-bridge-765.py b/tests/test-hermes-native-bridge-765.py index d67cd039..bddf753a 100644 --- a/tests/test-hermes-native-bridge-765.py +++ b/tests/test-hermes-native-bridge-765.py @@ -74,6 +74,7 @@ def setUpClass(cls) -> None: cls.fixture_root = Path(explicit) if explicit else Path(tempfile.mkdtemp(prefix="hermes765-")) if explicit: cls.fixture_root.mkdir(parents=True, exist_ok=False) + cls.fixture_root = cls.fixture_root.resolve(strict=True) def setUp(self) -> None: self.case = self.fixture_root / self._testMethodName From f624aed4b4fac4daa31da415120e6c7aa52a1dbd Mon Sep 17 00:00:00 2001 From: buildingjoshbetter <236675935+buildingjoshbetter@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:31:01 -0500 Subject: [PATCH 03/16] fix: Preserve test assertions while satisfying hosted lint --- tests/test-hermes-native-bridge-765.py | 3 +- tests/test-ingestion-completion-768.py | 1 - tests/test-profile-freshness-748.py | 267 +++++++++++++++++------ tests/test-style-getter-freshness-748.py | 144 ++++++++---- truememory/engine.py | 2 +- 5 files changed, 308 insertions(+), 109 deletions(-) diff --git a/tests/test-hermes-native-bridge-765.py b/tests/test-hermes-native-bridge-765.py index bddf753a..5734e3ee 100644 --- a/tests/test-hermes-native-bridge-765.py +++ b/tests/test-hermes-native-bridge-765.py @@ -341,7 +341,8 @@ def outcome(*args: object, **kwargs: object) -> object: if not missing_api: extractor.extract_facts_outcome = outcome pipeline = types.ModuleType("truememory.ingest.pipeline") - original = lambda *_args, **_kwargs: fact_list + def original(*_args: object, **_kwargs: object) -> list[object]: + return fact_list pipeline.extract_facts = original pipeline.extract_facts_simple = lambda _text: fact_list config = object() diff --git a/tests/test-ingestion-completion-768.py b/tests/test-ingestion-completion-768.py index 6c5fd50e..a2681e95 100644 --- a/tests/test-ingestion-completion-768.py +++ b/tests/test-ingestion-completion-768.py @@ -5,7 +5,6 @@ import contextlib import datetime import enum -import hashlib import importlib.util import json import logging diff --git a/tests/test-profile-freshness-748.py b/tests/test-profile-freshness-748.py index a899dde8..1d19b2fb 100644 --- a/tests/test-profile-freshness-748.py +++ b/tests/test-profile-freshness-748.py @@ -1,8 +1,8 @@ """Actual profile token and incremental deferral controls; synthetic in-memory SQLite.""" + from __future__ import annotations import hashlib -import json import runpy import sqlite3 import unittest @@ -10,14 +10,20 @@ LOAD = runpy.run_path(str(Path(__file__).with_name("test-source-invalidation-748.py"))) P, _STYLE = LOAD["_load_modules"]() -STORAGE = runpy.run_path(str(Path(__file__).with_name("test-l0-style-accumulator-747.py")))["STORAGE"] +STORAGE = runpy.run_path( + str(Path(__file__).with_name("test-l0-style-accumulator-747.py")) +)["STORAGE"] class ProfileFreshnessTests(unittest.TestCase): def db(self, *, certified: bool = True) -> sqlite3.Connection: - conn = sqlite3.connect(":memory:", cached_statements=0); self.addCleanup(conn.close) - conn.executescript(STORAGE._SCHEMA_SQL); STORAGE._initialize_maintenance_tracking(conn) - conn.execute("INSERT INTO messages(content,sender,recipient,timestamp) VALUES('synthetic code original','Synthetic','Other','2020-01-01')") + conn = sqlite3.connect(":memory:", cached_statements=0) + self.addCleanup(conn.close) + conn.executescript(STORAGE._SCHEMA_SQL) + STORAGE._initialize_maintenance_tracking(conn) + conn.execute( + "INSERT INTO messages(content,sender,recipient,timestamp) VALUES('synthetic code original','Synthetic','Other','2020-01-01')" + ) conn.commit() if certified: P.build_entity_profiles(conn) @@ -26,152 +32,277 @@ def db(self, *, certified: bool = True) -> sqlite3.Connection: def rows(self, conn: sqlite3.Connection) -> list[tuple]: return conn.execute("SELECT * FROM entity_profiles ORDER BY entity").fetchall() - def append(self, conn: sqlite3.Connection, *, content: str = "synthetic code append", sender: str = "Synthetic", recipient: str = "Other", directive: int = 0) -> None: - conn.execute("INSERT INTO messages(content,sender,recipient,timestamp,directive) VALUES(?,?,?,'2020-01-02',?)", (content,sender,recipient,directive)); conn.commit() + def append( + self, + conn: sqlite3.Connection, + *, + content: str = "synthetic code append", + sender: str = "Synthetic", + recipient: str = "Other", + directive: int = 0, + ) -> None: + conn.execute( + "INSERT INTO messages(content,sender,recipient,timestamp,directive) VALUES(?,?,?,'2020-01-02',?)", + (content, sender, recipient, directive), + ) + conn.commit() def test_source_proof_matches_actual_storage_contract(self) -> None: - self.assertEqual(P._PROFILE_SOURCE_TRIGGER_SHA256, - {name: hashlib.sha256(" ".join(sql.split()).encode()).hexdigest() - for name, sql in STORAGE._maintenance_trigger_definitions().items()}) + self.assertEqual( + P._PROFILE_SOURCE_TRIGGER_SHA256, + { + name: hashlib.sha256(" ".join(sql.split()).encode()).hexdigest() + for name, sql in STORAGE._maintenance_trigger_definitions().items() + }, + ) def test_bulk_certifies_exact_current_generation(self) -> None: - conn = self.db(); profile = P.get_entity_profile(conn, "SYNTHETIC") + conn = self.db() + profile = P.get_entity_profile(conn, "SYNTHETIC") self.assertEqual(profile["message_count"], 1) source, previous = P._profile_read_state(conn) self.assertEqual(source, previous) self.assertFalse(conn.in_transaction) def test_every_correction_hides_retained_rows_and_defers_increment(self) -> None: - for column, value in {"content":"synthetic changed","sender":"Changed","recipient":"Changed", - "timestamp":"2021-01-01","directive":1,"metadata":'{"synthetic":true}'}.items(): + for column, value in { + "content": "synthetic changed", + "sender": "Changed", + "recipient": "Changed", + "timestamp": "2021-01-01", + "directive": 1, + "metadata": '{"synthetic":true}', + }.items(): with self.subTest(column=column): - conn = self.db(); before = self.rows(conn) - conn.execute(f"UPDATE messages SET {column}=?", (value,)); conn.commit() + conn = self.db() + before = self.rows(conn) + conn.execute(f"UPDATE messages SET {column}=?", (value,)) + conn.commit() self.assertIsNone(P.get_entity_profile(conn, "Synthetic")) with self.assertRaises(P.ProfileRebuildRequired): - P.update_entity_profile_incremental(conn, "Synthetic", "synthetic partial", "Other") + P.update_entity_profile_incremental( + conn, "Synthetic", "synthetic partial", "Other" + ) self.assertEqual(self.rows(conn), before) self.assertFalse(conn.in_transaction) - def test_exact_ordinary_append_extends_certificate_without_formula_change(self) -> None: - conn = self.db(); self.append(conn) + def test_exact_ordinary_append_extends_certificate_without_formula_change( + self, + ) -> None: + conn = self.db() + self.append(conn) self.assertEqual(P.get_entity_profile(conn, "Synthetic")["message_count"], 1) - P.update_entity_profile_incremental(conn, "SYNTHETIC", "synthetic code append", "OTHER") + P.update_entity_profile_incremental( + conn, "SYNTHETIC", "synthetic code append", "OTHER" + ) self.assertEqual(P.get_entity_profile(conn, "Synthetic")["message_count"], 2) - source, previous = P._profile_read_state(conn); self.assertEqual(source, previous) + source, previous = P._profile_read_state(conn) + self.assertEqual(source, previous) def test_multiple_appends_defer_without_partial_generation(self) -> None: - conn = self.db(); before = self.rows(conn); self.append(conn); self.append(conn) + conn = self.db() + before = self.rows(conn) + self.append(conn) + self.append(conn) with self.assertRaises(P.ProfileRebuildRequired): - P.update_entity_profile_incremental(conn, "Synthetic", "synthetic code append", "Other") + P.update_entity_profile_incremental( + conn, "Synthetic", "synthetic code append", "Other" + ) self.assertEqual(self.rows(conn), before) def test_mismatched_content_sender_recipient_and_directive_refuse(self) -> None: - for kwargs in ({"content":"different"},{"sender":"Different"},{"recipient":"Different"},{"directive":1}): + for kwargs in ( + {"content": "different"}, + {"sender": "Different"}, + {"recipient": "Different"}, + {"directive": 1}, + ): with self.subTest(kwargs=kwargs): - conn = self.db(); before = self.rows(conn); self.append(conn, **kwargs) + conn = self.db() + before = self.rows(conn) + self.append(conn, **kwargs) with self.assertRaises(P.ProfileRebuildRequired): - P.update_entity_profile_incremental(conn, "Synthetic", "synthetic code append", "Other") + P.update_entity_profile_incremental( + conn, "Synthetic", "synthetic code append", "Other" + ) self.assertEqual(self.rows(conn), before) def test_source_free_direct_call_cannot_extend_certified_generation(self) -> None: - conn = self.db(); before = self.rows(conn) + conn = self.db() + before = self.rows(conn) with self.assertRaises(P.ProfileRebuildRequired): - P.update_entity_profile_incremental(conn, "Synthetic", "synthetic fabricated", "Other") + P.update_entity_profile_incremental( + conn, "Synthetic", "synthetic fabricated", "Other" + ) self.assertEqual(self.rows(conn), before) def test_direct_output_mutation_and_missing_token_preserve_enrollment(self) -> None: - for sql in ("UPDATE entity_profiles SET message_count=999", "DELETE FROM metadata WHERE key='entity_profile_source_v1'", - "UPDATE metadata SET value='{}' WHERE key='entity_profile_source_v1'"): + for sql in ( + "UPDATE entity_profiles SET message_count=999", + "DELETE FROM metadata WHERE key='entity_profile_source_v1'", + "UPDATE metadata SET value='{}' WHERE key='entity_profile_source_v1'", + ): with self.subTest(sql=sql): - conn = self.db(); conn.execute(sql); conn.commit(); before = self.rows(conn) + conn = self.db() + conn.execute(sql) + conn.commit() + before = self.rows(conn) self.assertIsNone(P.get_entity_profile(conn, "Synthetic")) with self.assertRaises(P.ProfileRebuildRequired): - P.update_entity_profile_incremental(conn, "Synthetic", "synthetic append", "Other") + P.update_entity_profile_incremental( + conn, "Synthetic", "synthetic append", "Other" + ) self.assertEqual(self.rows(conn), before) def test_epoch_reset_and_tracking_loss_do_not_revive_old_rows(self) -> None: - for sql in ("UPDATE maintenance_source_state SET epoch='synthetic reset'", "DROP TABLE maintenance_source_state"): + for sql in ( + "UPDATE maintenance_source_state SET epoch='synthetic reset'", + "DROP TABLE maintenance_source_state", + ): with self.subTest(sql=sql): - conn = self.db(); conn.execute(sql); conn.commit(); before = self.rows(conn) + conn = self.db() + conn.execute(sql) + conn.commit() + before = self.rows(conn) self.assertIsNone(P.get_entity_profile(conn, "Synthetic")) self.assertEqual(self.rows(conn), before) - def test_borrowed_corrective_repair_and_rollback_restore_output_token_source(self) -> None: - conn = self.db(); before = self.rows(conn); token = conn.execute("SELECT value FROM metadata WHERE key=?", (P._PROFILE_TOKEN_KEY,)).fetchone() + def test_borrowed_corrective_repair_and_rollback_restore_output_token_source( + self, + ) -> None: + conn = self.db() + before = self.rows(conn) + token = conn.execute( + "SELECT value FROM metadata WHERE key=?", (P._PROFILE_TOKEN_KEY,) + ).fetchone() conn.execute("UPDATE messages SET content='synthetic corrected code'") - self.assertIsNone(P.get_entity_profile(conn, "Synthetic")); self.assertTrue(conn.in_transaction) + self.assertIsNone(P.get_entity_profile(conn, "Synthetic")) + self.assertTrue(conn.in_transaction) P.build_entity_profiles(conn) - self.assertIsNotNone(P.get_entity_profile(conn, "Synthetic")); self.assertTrue(conn.in_transaction) - conn.rollback(); self.assertEqual(self.rows(conn), before) - self.assertEqual(conn.execute("SELECT value FROM metadata WHERE key=?", (P._PROFILE_TOKEN_KEY,)).fetchone(), token) + self.assertIsNotNone(P.get_entity_profile(conn, "Synthetic")) + self.assertTrue(conn.in_transaction) + conn.rollback() + self.assertEqual(self.rows(conn), before) + self.assertEqual( + conn.execute( + "SELECT value FROM metadata WHERE key=?", (P._PROFILE_TOKEN_KEY,) + ).fetchone(), + token, + ) def test_borrowed_append_rollback_restores_certificate_and_rows(self) -> None: - conn = self.db(); before = self.rows(conn) - conn.execute("INSERT INTO messages(content,sender,recipient) VALUES('synthetic append','Synthetic','Other')") - P.update_entity_profile_incremental(conn, "Synthetic", "synthetic append", "Other") - self.assertEqual(P.get_entity_profile(conn, "Synthetic")["message_count"], 2); self.assertTrue(conn.in_transaction) - conn.rollback(); self.assertEqual(self.rows(conn), before) - source, previous = P._profile_read_state(conn); self.assertEqual(source, previous) + conn = self.db() + before = self.rows(conn) + conn.execute( + "INSERT INTO messages(content,sender,recipient) VALUES('synthetic append','Synthetic','Other')" + ) + P.update_entity_profile_incremental( + conn, "Synthetic", "synthetic append", "Other" + ) + self.assertEqual(P.get_entity_profile(conn, "Synthetic")["message_count"], 2) + self.assertTrue(conn.in_transaction) + conn.rollback() + self.assertEqual(self.rows(conn), before) + source, previous = P._profile_read_state(conn) + self.assertEqual(source, previous) def test_known_nonappend_history_without_certificate_is_unavailable(self) -> None: conn = self.db(certified=False) - conn.execute("INSERT INTO entity_profiles(entity,message_count) VALUES('synthetic',99)") - conn.execute("UPDATE messages SET content='synthetic correction'"); conn.commit() + conn.execute( + "INSERT INTO entity_profiles(entity,message_count) VALUES('synthetic',99)" + ) + conn.execute("UPDATE messages SET content='synthetic correction'") + conn.commit() self.assertIsNone(P.get_entity_profile(conn, "Synthetic")) with self.assertRaises(P.ProfileRebuildRequired): P.update_entity_profile_incremental(conn, "Synthetic", "synthetic partial") - P.build_entity_profiles(conn); self.assertEqual(P.get_entity_profile(conn, "Synthetic")["message_count"], 1) + P.build_entity_profiles(conn) + self.assertEqual(P.get_entity_profile(conn, "Synthetic")["message_count"], 1) def test_legacy_bulk_and_source_free_helpers_remain_compatible(self) -> None: - conn = LOAD["_db"](":memory:"); self.addCleanup(conn.close); conn.commit() - P.update_entity_profile_incremental(conn, "Synthetic", "synthetic first", "Other") - P.update_entity_profile_incremental(conn, "SYNTHETIC", "synthetic second", "OTHER") + conn = LOAD["_db"](":memory:") + self.addCleanup(conn.close) + conn.commit() + P.update_entity_profile_incremental( + conn, "Synthetic", "synthetic first", "Other" + ) + P.update_entity_profile_incremental( + conn, "SYNTHETIC", "synthetic second", "OTHER" + ) self.assertEqual(P.get_entity_profile(conn, "Synthetic")["message_count"], 2) self.assertEqual(P.build_entity_profiles(conn), {}) self.assertIsNone(P.get_entity_profile(conn, "Synthetic")) canonical = self.db(certified=False) - P.update_entity_profile_incremental(canonical, "Direct", "synthetic source-free") + P.update_entity_profile_incremental( + canonical, "Direct", "synthetic source-free" + ) self.assertEqual(P.get_entity_profile(canonical, "Direct")["message_count"], 1) def test_read_is_bounded_and_never_reads_messages(self) -> None: - conn = self.db(); expected = P.get_entity_profile(conn, "Synthetic") + conn = self.db() + expected = P.get_entity_profile(conn, "Synthetic") + def authorize(action: int, table: str | None, *_args: object) -> int: - if action == sqlite3.SQLITE_READ and table == 'messages':return sqlite3.SQLITE_DENY - if action in (sqlite3.SQLITE_INSERT,sqlite3.SQLITE_UPDATE,sqlite3.SQLITE_DELETE):return sqlite3.SQLITE_DENY + if action == sqlite3.SQLITE_READ and table == "messages": + return sqlite3.SQLITE_DENY + if action in ( + sqlite3.SQLITE_INSERT, + sqlite3.SQLITE_UPDATE, + sqlite3.SQLITE_DELETE, + ): + return sqlite3.SQLITE_DENY return sqlite3.SQLITE_OK + conn.set_authorizer(authorize) - try:self.assertEqual(P.get_entity_profile(conn, "Synthetic"), expected) - finally:conn.set_authorizer(None) + try: + self.assertEqual(P.get_entity_profile(conn, "Synthetic"), expected) + finally: + conn.set_authorizer(None) self.assertFalse(conn.in_transaction) def test_missing_tracking_or_output_trigger_is_unavailable(self) -> None: for name in (*P._PROFILE_OUTPUT_TRIGGERS, *P._PROFILE_SOURCE_TRIGGER_SHA256): with self.subTest(name=name): - conn = self.db(); conn.execute(f"DROP TRIGGER {name}"); conn.commit() + conn = self.db() + conn.execute(f"DROP TRIGGER {name}") + conn.commit() self.assertIsNone(P.get_entity_profile(conn, "Synthetic")) self.assertTrue(self.rows(conn)) def test_duplicate_keys_in_source_token_cannot_certify_rows(self) -> None: - conn = self.db(); before = self.rows(conn) - value = conn.execute("SELECT value FROM metadata WHERE key=?", (P._PROFILE_TOKEN_KEY,)).fetchone()[0] + conn = self.db() + before = self.rows(conn) + value = conn.execute( + "SELECT value FROM metadata WHERE key=?", (P._PROFILE_TOKEN_KEY,) + ).fetchone()[0] duplicate = '{"schema_version":9,' + value[1:] - conn.execute("UPDATE metadata SET value=? WHERE key=?", (duplicate, P._PROFILE_TOKEN_KEY)); conn.commit() + conn.execute( + "UPDATE metadata SET value=? WHERE key=?", (duplicate, P._PROFILE_TOKEN_KEY) + ) + conn.commit() self.assertIsNone(P.get_entity_profile(conn, "Synthetic")) with self.assertRaises(P.ProfileRebuildRequired): - P.update_entity_profile_incremental(conn, "Synthetic", "synthetic append", "Other") + P.update_entity_profile_incremental( + conn, "Synthetic", "synthetic append", "Other" + ) self.assertEqual(self.rows(conn), before) def test_invalid_utf8_source_epoch_or_token_is_controlled_unavailable(self) -> None: - for sql in ("UPDATE maintenance_source_state SET epoch=CAST(X'eda080' AS TEXT)", - "UPDATE metadata SET value=CAST(X'eda080' AS TEXT) WHERE key='entity_profile_source_v1'"): + for sql in ( + "UPDATE maintenance_source_state SET epoch=CAST(X'eda080' AS TEXT)", + "UPDATE metadata SET value=CAST(X'eda080' AS TEXT) WHERE key='entity_profile_source_v1'", + ): with self.subTest(sql=sql): - conn = self.db(); before = self.rows(conn) - conn.text_factory = lambda value: value.decode('utf-8', 'surrogatepass') - conn.execute(sql); conn.commit() + conn = self.db() + before = self.rows(conn) + conn.text_factory = lambda value: value.decode("utf-8", "surrogatepass") + conn.execute(sql) + conn.commit() self.assertIsNone(P.get_entity_profile(conn, "Synthetic")) with self.assertRaises(P.ProfileRebuildRequired): - P.update_entity_profile_incremental(conn, "Synthetic", "synthetic append", "Other") + P.update_entity_profile_incremental( + conn, "Synthetic", "synthetic append", "Other" + ) self.assertEqual(self.rows(conn), before) diff --git a/tests/test-style-getter-freshness-748.py b/tests/test-style-getter-freshness-748.py index 15f5ec43..09c7d181 100644 --- a/tests/test-style-getter-freshness-748.py +++ b/tests/test-style-getter-freshness-748.py @@ -1,4 +1,5 @@ """Bounded tracked style getter controls with actual source and in-memory SQLite.""" + from __future__ import annotations import hashlib @@ -18,7 +19,9 @@ def db(self, *, certified: bool = True) -> sqlite3.Connection: self.addCleanup(conn.close) conn.executescript(STORAGE._SCHEMA_SQL) STORAGE._initialize_maintenance_tracking(conn) - conn.execute("INSERT INTO messages(content,sender,recipient,timestamp) VALUES('synthetic original style','Synthetic','Other','2020-01-01')") + conn.execute( + "INSERT INTO messages(content,sender,recipient,timestamp) VALUES('synthetic original style','Synthetic','Other','2020-01-01')" + ) conn.commit() if certified: result = MAINTENANCE.run_style_maintenance(conn) @@ -33,8 +36,10 @@ def rows(self, conn: sqlite3.Connection) -> list[tuple]: def test_proof_fingerprints_match_actual_storage_contract(self) -> None: definitions = STORAGE._maintenance_trigger_definitions() definitions.update(STORAGE._STYLE_OUTPUT_TRIGGERS) - hashes = {name: hashlib.sha256(" ".join(sql.split()).encode()).hexdigest() - for name, sql in definitions.items()} + hashes = { + name: hashlib.sha256(" ".join(sql.split()).encode()).hexdigest() + for name, sql in definitions.items() + } self.assertEqual(STYLE._STYLE_READ_TRIGGER_SHA256, hashes) def test_current_generation_and_case_normalization_remain_exact(self) -> None: @@ -45,65 +50,110 @@ def test_current_generation_and_case_normalization_remain_exact(self) -> None: self.assertFalse(conn.in_transaction) def test_every_source_correction_hides_old_rows_without_erasing(self) -> None: - edits = {"content": "synthetic changed", "sender": "Changed", "recipient": "Changed", - "timestamp": "2021-01-01", "category": "changed", "modality": "changed", - "directive": 1, "metadata": '{"synthetic":true}'} + edits = { + "content": "synthetic changed", + "sender": "Changed", + "recipient": "Changed", + "timestamp": "2021-01-01", + "category": "changed", + "modality": "changed", + "directive": 1, + "metadata": '{"synthetic":true}', + } for column, value in edits.items(): with self.subTest(column=column): - conn = self.db(); before = self.rows(conn) - conn.execute(f"UPDATE messages SET {column}=?", (value,)); conn.commit() - self.assertEqual(MAINTENANCE.observe_style(conn).health["freshness"], "correction_pending") + conn = self.db() + before = self.rows(conn) + conn.execute(f"UPDATE messages SET {column}=?", (value,)) + conn.commit() + self.assertEqual( + MAINTENANCE.observe_style(conn).health["freshness"], + "correction_pending", + ) self.assertIsNone(STYLE.get_entity_style_vector(conn, "Synthetic")) self.assertEqual(self.rows(conn), before) self.assertFalse(conn.in_transaction) def test_append_pending_prior_valid_generation_remains_readable(self) -> None: - conn = self.db(); expected = STYLE.get_entity_style_vector(conn, "Synthetic") - conn.execute("INSERT INTO messages(content,sender) VALUES('synthetic appended','Synthetic')"); conn.commit() - self.assertEqual(MAINTENANCE.observe_style(conn).health["freshness"], "append_pending") + conn = self.db() + expected = STYLE.get_entity_style_vector(conn, "Synthetic") + conn.execute( + "INSERT INTO messages(content,sender) VALUES('synthetic appended','Synthetic')" + ) + conn.commit() + self.assertEqual( + MAINTENANCE.observe_style(conn).health["freshness"], "append_pending" + ) self.assertEqual(STYLE.get_entity_style_vector(conn, "Synthetic"), expected) def test_successful_repair_restores_current_exact_generation(self) -> None: conn = self.db() - conn.execute("UPDATE messages SET content='synthetic corrected',sender='Changed'"); conn.commit() + conn.execute( + "UPDATE messages SET content='synthetic corrected',sender='Changed'" + ) + conn.commit() self.assertIsNone(STYLE.get_entity_style_vector(conn, "Synthetic")) self.assertEqual(MAINTENANCE.run_style_maintenance(conn).outcome, "success") self.assertIsNone(STYLE.get_entity_style_vector(conn, "Synthetic")) - self.assertEqual(STYLE.get_entity_style_vector(conn, "CHANGED"), json.loads(self.rows(conn)[0][1])) + self.assertEqual( + STYLE.get_entity_style_vector(conn, "CHANGED"), + json.loads(self.rows(conn)[0][1]), + ) def test_direct_artifact_mutation_invalidates_the_generation(self) -> None: - conn = self.db(); conn.execute("UPDATE entity_style_vectors SET vector='[123]'"); conn.commit() + conn = self.db() + conn.execute("UPDATE entity_style_vectors SET vector='[123]'") + conn.commit() before = self.rows(conn) self.assertIsNone(STYLE.get_entity_style_vector(conn, "Synthetic")) self.assertEqual(self.rows(conn), before) def test_epoch_reset_and_missing_checkpoint_do_not_revive_old_rows(self) -> None: - for statement in ("UPDATE maintenance_source_state SET epoch='synthetic changed epoch'", - "DELETE FROM maintenance_layers WHERE layer='style_vectors'", - "DROP TABLE maintenance_layers"): + for statement in ( + "UPDATE maintenance_source_state SET epoch='synthetic changed epoch'", + "DELETE FROM maintenance_layers WHERE layer='style_vectors'", + "DROP TABLE maintenance_layers", + ): with self.subTest(statement=statement): - conn = self.db(); before = self.rows(conn); conn.execute(statement); conn.commit() + conn = self.db() + before = self.rows(conn) + conn.execute(statement) + conn.commit() self.assertIsNone(STYLE.get_entity_style_vector(conn, "Synthetic")) self.assertEqual(self.rows(conn), before) - def test_missing_or_replaced_tracking_and_output_triggers_are_unavailable(self) -> None: + def test_missing_or_replaced_tracking_and_output_triggers_are_unavailable( + self, + ) -> None: for name in STYLE._STYLE_READ_TRIGGER_SHA256: with self.subTest(name=name): - conn = self.db(); before = self.rows(conn) - conn.execute(f"DROP TRIGGER {name}"); conn.commit() + conn = self.db() + before = self.rows(conn) + conn.execute(f"DROP TRIGGER {name}") + conn.commit() self.assertIsNone(STYLE.get_entity_style_vector(conn, "Synthetic")) self.assertEqual(self.rows(conn), before) def test_untracked_legacy_and_direct_builder_payloads_remain_exact(self) -> None: conn = self.db(certified=False) - self.assertEqual(STYLE.get_entity_style_vector(conn, "Synthetic"), json.loads(self.rows(conn)[0][1])) - legacy = sqlite3.connect(":memory:"); self.addCleanup(legacy.close) - legacy.execute("CREATE TABLE entity_style_vectors(entity TEXT PRIMARY KEY,vector TEXT)") - legacy.execute("INSERT INTO entity_style_vectors VALUES('synthetic','[1]')"); legacy.commit() + self.assertEqual( + STYLE.get_entity_style_vector(conn, "Synthetic"), + json.loads(self.rows(conn)[0][1]), + ) + legacy = sqlite3.connect(":memory:") + self.addCleanup(legacy.close) + legacy.execute( + "CREATE TABLE entity_style_vectors(entity TEXT PRIMARY KEY,vector TEXT)" + ) + legacy.execute("INSERT INTO entity_style_vectors VALUES('synthetic','[1]')") + legacy.commit() self.assertEqual(STYLE.get_entity_style_vector(legacy, "SYNTHETIC"), [1]) - def test_borrowed_source_change_is_hidden_and_caller_rollback_preserved(self) -> None: - conn = self.db(); expected = STYLE.get_entity_style_vector(conn, "Synthetic") + def test_borrowed_source_change_is_hidden_and_caller_rollback_preserved( + self, + ) -> None: + conn = self.db() + expected = STYLE.get_entity_style_vector(conn, "Synthetic") conn.execute("UPDATE messages SET content='synthetic pending caller change'") self.assertIsNone(STYLE.get_entity_style_vector(conn, "Synthetic")) self.assertTrue(conn.in_transaction) @@ -111,15 +161,25 @@ def test_borrowed_source_change_is_hidden_and_caller_rollback_preserved(self) -> self.assertEqual(STYLE.get_entity_style_vector(conn, "Synthetic"), expected) def test_bounded_inspection_never_reads_source_or_changes_schema(self) -> None: - conn = self.db(); expected = STYLE.get_entity_style_vector(conn, "Synthetic"); seen = [] + conn = self.db() + expected = STYLE.get_entity_style_vector(conn, "Synthetic") + seen = [] + def authorize(action: int, table: str | None, *_args: object) -> int: seen.append((action, table)) if action == sqlite3.SQLITE_READ and table == "messages": return sqlite3.SQLITE_DENY - if action in (sqlite3.SQLITE_INSERT, sqlite3.SQLITE_UPDATE, sqlite3.SQLITE_DELETE, - sqlite3.SQLITE_CREATE_TABLE, sqlite3.SQLITE_CREATE_TRIGGER, sqlite3.SQLITE_DROP_TABLE): + if action in ( + sqlite3.SQLITE_INSERT, + sqlite3.SQLITE_UPDATE, + sqlite3.SQLITE_DELETE, + sqlite3.SQLITE_CREATE_TABLE, + sqlite3.SQLITE_CREATE_TRIGGER, + sqlite3.SQLITE_DROP_TABLE, + ): return sqlite3.SQLITE_DENY return sqlite3.SQLITE_OK + conn.set_authorizer(authorize) try: self.assertEqual(STYLE.get_entity_style_vector(conn, "Synthetic"), expected) @@ -129,18 +189,26 @@ def authorize(action: int, table: str | None, *_args: object) -> int: self.assertFalse(conn.in_transaction) def test_shadowed_tables_and_unknown_output_triggers_are_unavailable(self) -> None: - for statement in ("CREATE TEMP TABLE messages(id INTEGER)", - "CREATE TRIGGER synthetic_foreign AFTER UPDATE ON entity_style_vectors BEGIN SELECT 1; END"): + for statement in ( + "CREATE TEMP TABLE messages(id INTEGER)", + "CREATE TRIGGER synthetic_foreign AFTER UPDATE ON entity_style_vectors BEGIN SELECT 1; END", + ): with self.subTest(statement=statement): - conn = self.db(); conn.execute(statement); conn.commit() + conn = self.db() + conn.execute(statement) + conn.commit() self.assertIsNone(STYLE.get_entity_style_vector(conn, "Synthetic")) def test_incompatible_dependency_and_hash_never_claim_current(self) -> None: - for statement in ("UPDATE metadata SET value='1' WHERE key='style_vec_hash_version'", - "UPDATE maintenance_layers SET successful_dependency='{}' WHERE layer='style_vectors'", - "UPDATE maintenance_layers SET successful_coverage='unverified' WHERE layer='style_vectors'"): + for statement in ( + "UPDATE metadata SET value='1' WHERE key='style_vec_hash_version'", + "UPDATE maintenance_layers SET successful_dependency='{}' WHERE layer='style_vectors'", + "UPDATE maintenance_layers SET successful_coverage='unverified' WHERE layer='style_vectors'", + ): with self.subTest(statement=statement): - conn = self.db(); conn.execute(statement); conn.commit() + conn = self.db() + conn.execute(statement) + conn.commit() self.assertIsNone(STYLE.get_entity_style_vector(conn, "Synthetic")) diff --git a/truememory/engine.py b/truememory/engine.py index f1143034..60c1012a 100644 --- a/truememory/engine.py +++ b/truememory/engine.py @@ -173,7 +173,7 @@ def get_vectors_load_error() -> str | None: _HAS_TEMPORAL = False try: from truememory.temporal import ( - detect_temporal_intent, search_temporal, detect_episodes, detect_landmark_events, + detect_temporal_intent as detect_temporal_intent, search_temporal, detect_episodes, detect_landmark_events, resolve_temporal_intent, attach_temporal_resolution, _source_intervals, _temporal_row_matches, _exclusive_upper_bound, ) From a66d185f84e08f86c39d5cc0cd24303bfe71b8a8 Mon Sep 17 00:00:00 2001 From: buildingjoshbetter <236675935+buildingjoshbetter@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:36:16 -0500 Subject: [PATCH 04/16] fix: Preserve platform file identities and fixture encodings --- .gitattributes | 1 + .github/workflows/ci.yml | 3 + .../windows-owned-file-diagnostics-v1.py | 83 +++++++++++++++++++ tests/test-claude-stop-fallback-722.py | 15 ++-- tests/test-ingestion-completion-768.py | 8 +- tests/test-landmark-resolution-755.py | 12 +-- tests/test-openclaw-native-bridge-764.py | 4 +- tests/test-source-update-fence-748.py | 4 +- truememory/ingest/hooks/hermes.py | 9 +- 9 files changed, 118 insertions(+), 21 deletions(-) create mode 100644 .gitattributes create mode 100644 tests/native/windows-owned-file-diagnostics-v1.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..d9bd16b0 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +*.py text eol=lf diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 762659bc..9bab1729 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,6 +52,9 @@ jobs: cache-dependency-path: pyproject.toml - name: Install dependencies run: pip install -e ".[all,dev]" + - name: Trace synthetic Windows file ownership + if: runner.os == 'Windows' + run: python tests/native/windows-owned-file-diagnostics-v1.py - name: Run tests (gate — deterministic, no network) # Issue #426: exclude `network`-marked tests (real HuggingFace model # downloads) from the gate so a transient HfHubHTTPError can't fail CI diff --git a/tests/native/windows-owned-file-diagnostics-v1.py b/tests/native/windows-owned-file-diagnostics-v1.py new file mode 100644 index 00000000..d243b2d0 --- /dev/null +++ b/tests/native/windows-owned-file-diagnostics-v1.py @@ -0,0 +1,83 @@ +"""Trace actual documented Windows ownership calls on retained synthetic files.""" +from __future__ import annotations + +import ctypes +import hashlib +import inspect +import json +import os +from pathlib import Path +import tempfile +import uuid + + +def main() -> None: + if os.name != "nt": + print(json.dumps({"platform": os.name, "executed": False})) + return + import msvcrt + from truememory.ingest import completion + + directory = Path(tempfile.gettempdir()) / ("tm-windows-owned-diagnostic-" + uuid.uuid4().hex) + directory.mkdir(mode=0o700) + source = directory / "synthetic-source.jsonl" + source.write_text('{"type":"user","message":{"content":"synthetic diagnostic"}}\n', encoding="utf-8") + calls: list[dict[str, object]] = [] + original = ctypes.WinDLL + + class FunctionTrace: + def __init__(self, name: str, function: object) -> None: + object.__setattr__(self, "name", name) + object.__setattr__(self, "function", function) + + def __getattr__(self, name: str) -> object: + return getattr(self.function, name) + + def __setattr__(self, name: str, value: object) -> None: + setattr(self.function, name, value) + + def __call__(self, *args: object) -> object: + result = self.function(*args) + row: dict[str, object] = {"api": self.name, "result": result if isinstance(result, (int, bool)) else "pointer", "last_error": ctypes.get_last_error()} + if self.name == "GetTokenInformation": + row["information_class"] = args[1] + row["buffer_bytes"] = args[3] + row["needed_bytes"] = ctypes.cast(args[4], ctypes.POINTER(ctypes.c_ulong)).contents.value + if self.name == "GetSecurityInfo": + row["security_information"] = args[2] + calls.append(row) + return result + + class LibraryTrace: + def __init__(self, library: object) -> None: + self.library = library + self.functions: dict[str, FunctionTrace] = {} + + def __getattr__(self, name: str) -> FunctionTrace: + if name not in self.functions: + self.functions[name] = FunctionTrace(name, getattr(self.library, name)) + return self.functions[name] + + def traced_library(*args: object, **kwargs: object) -> LibraryTrace: + return LibraryTrace(original(*args, **kwargs)) + + cases: list[dict[str, object]] = [] + for private, flags in ((False, os.O_RDONLY), (True, os.O_RDWR)): + fd = os.open(source, flags) + try: + before = len(calls) + ctypes.WinDLL = traced_library + try: + accepted = completion._windows_security(msvcrt.get_osfhandle(fd), private=private) + finally: + ctypes.WinDLL = original + info = os.fstat(fd) + path_info = source.lstat() + cases.append({"private": private, "accepted": accepted, "calls": calls[before:], "fstat": [info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns, info.st_ctime_ns], "lstat": [path_info.st_dev, path_info.st_ino, path_info.st_size, path_info.st_mtime_ns, path_info.st_ctime_ns]}) + finally: + os.close(fd) + print(json.dumps({"executed": True, "synthetic_only": True, "source_sha256": hashlib.sha256(inspect.getsource(completion._windows_security).encode()).hexdigest(), "cases": cases}, sort_keys=True)) + + +if __name__ == "__main__": + main() diff --git a/tests/test-claude-stop-fallback-722.py b/tests/test-claude-stop-fallback-722.py index 575d65a7..9397a59f 100644 --- a/tests/test-claude-stop-fallback-722.py +++ b/tests/test-claude-stop-fallback-722.py @@ -18,7 +18,7 @@ def adapter_source() -> dict: source = ROOT / "truememory/hooks/adapters/claude.py" - tree = ast.parse(source.read_text()) + tree = ast.parse(source.read_text(encoding="utf-8")) nodes = [node for node in tree.body if isinstance(node, (ast.FunctionDef, ast.ClassDef)) or isinstance(node, ast.Assign) and any(isinstance(target, ast.Name) and target.id == "_HOOK_MODULES" for target in node.targets)] namespace = {"__file__": str(source), "json": json, "os": os, "sys": sys, "Path": Path, "CLIAdapter": object} @@ -55,7 +55,10 @@ def test_exact_module_and_script_ownership(self) -> None: directory = ROOT / "truememory/ingest/hooks" module = "truememory.ingest.hooks.stop" self.assertEqual(owned("python3 -m " + module, directory, "python3"), module) - self.assertEqual(owned("python3 " + shlex.quote(str(directory / "stop.py")), directory, "python3"), module) + import subprocess + parts = ["python3", str(directory / "stop.py")] + command = subprocess.list2cmdline(parts) if sys.platform == "win32" else " ".join(shlex.quote(part) for part in parts) + self.assertEqual(owned(command, directory, "python3"), module) for command in ("echo truememory", "echo -m " + module, "python3 -m " + module + ".foreign", "python3 -m " + module + " && echo x", "python3 /foreign/truememory/stop.py", "python3 -m foreign --name truememory"): @@ -63,7 +66,7 @@ def test_exact_module_and_script_ownership(self) -> None: def test_recursion_guards_do_not_read_source_or_mark_completion(self) -> None: source = ROOT / "truememory/ingest/hooks/stop.py" - node = next(node for node in ast.parse(source.read_text()).body if isinstance(node, ast.FunctionDef) and node.name == "main") + node = next(node for node in ast.parse(source.read_text(encoding="utf-8")).body if isinstance(node, ast.FunctionDef) and node.name == "main") calls = [] namespace = {"_parse_args": lambda: types.SimpleNamespace(host="claude", user="", db=""), "os": os, "sys": sys, "json": json, "Path": lambda *args: calls.append("source")} @@ -101,9 +104,9 @@ def test_install_preserves_foreign_and_viable_stop_and_is_idempotent(self) -> No patch.dict(os.environ, {}, clear=True): adapter = adapter_type() adapter.install_hooks("python3", "synthetic-account", "/synthetic/db") - first = json.loads(target.read_text()) + first = json.loads(target.read_text(encoding="utf-8")) adapter.install_hooks("python3", "synthetic-account", "/synthetic/db") - second = json.loads(target.read_text()) + second = json.loads(target.read_text(encoding="utf-8")) self.assertEqual(first, second) self.assertEqual(second["other_setting"], "retained") commands = [hook for group in second["hooks"]["Stop"] for hook in group["hooks"]] @@ -122,7 +125,7 @@ def test_foreign_substring_does_not_suppress_owned_event(self) -> None: with patch.object(ADAPTER["ClaudeAdapter"], "config_path", new=property(lambda self: target)), \ patch.dict(os.environ, {}, clear=True): ADAPTER["ClaudeAdapter"]().install_hooks("python3") - settings = json.loads(target.read_text()) + settings = json.loads(target.read_text(encoding="utf-8")) self.assertEqual(settings["hooks"]["Stop"], [foreign]) self.assertIn(foreign, settings["hooks"]["SessionEnd"]) self.assertEqual(len(settings["hooks"]["SessionEnd"]), 2) diff --git a/tests/test-ingestion-completion-768.py b/tests/test-ingestion-completion-768.py index a2681e95..9c3bd4e5 100644 --- a/tests/test-ingestion-completion-768.py +++ b/tests/test-ingestion-completion-768.py @@ -229,7 +229,7 @@ def test_legacy_conversion_requires_exact_scope_retains_pointer_and_avoids_live_ def test_automatic_maintenance_failure_never_falls_back_to_legacy_cleanup(self) -> None: source = ROOT / "truememory/ingest/hooks/session_start.py" - node = next(node for node in ast.parse(source.read_text()).body + node = next(node for node in ast.parse(source.read_text(encoding="utf-8")).body if isinstance(node, ast.FunctionDef) and node.name == "_drain_backlog") namespace = {"os": os, "log": logging.getLogger(__name__)} exec(compile(ast.fix_missing_locations(ast.Module(body=[node], type_ignores=[])), str(source), "exec"), namespace) @@ -348,7 +348,7 @@ class InertPipelineCompletionControls(unittest.TestCase): def setUp(self) -> None: source = ROOT / "truememory/ingest/pipeline.py" names = {"IngestionResult", "_invalidate_recall_after_batch", "IngestionPipeline"} - nodes = [node for node in ast.parse(source.read_text()).body if isinstance(node, (ast.ClassDef, ast.FunctionDef)) and node.name in names] + nodes = [node for node in ast.parse(source.read_text(encoding="utf-8")).body if isinstance(node, (ast.ClassDef, ast.FunctionDef)) and node.name in names] action = enum.Enum("DedupAction", ["ADD", "UPDATE", "SKIP"]) self.namespace = {"dataclass": dataclass, "field": field, "contextlib": contextlib, "Iterator": object, "time": time, "datetime": datetime, "sqlite3": sqlite3, "Path": Path, "os": os, @@ -524,7 +524,7 @@ def test_five_failed_attempts_retain_capture_outcomes_and_no_receipt(self) -> No self.assertFalse(COMPLETION.record_receipt(capture, result(extractor="partial"))) with patch.object(COMPLETION.time, "time", return_value=10000): self.assertEqual(COMPLETION.reserve_inline_attempt(capture), 0) - state = json.loads((capture.directory / "state.json").read_text())["captures"][capture.key] + state = json.loads((capture.directory / "state.json").read_text(encoding="utf-8"))["captures"][capture.key] self.assertEqual(state["status"], "exhausted") self.assertEqual(len(state["outcomes"]), 5) self.assertEqual(capture.path.read_bytes(), self.data) @@ -540,7 +540,7 @@ def test_os_claim_is_exclusive_and_exception_does_not_complete(self) -> None: self.assertFalse(second) raise RuntimeError("synthetic commit ambiguity") self.assertFalse(COMPLETION.has_receipt(capture)) - state = json.loads((capture.directory / "state.json").read_text())["captures"][capture.key] + state = json.loads((capture.directory / "state.json").read_text(encoding="utf-8"))["captures"][capture.key] self.assertEqual(state["outcomes"][0]["category"], "worker_unproven") self.assertEqual(capture.path.read_bytes(), self.data) diff --git a/tests/test-landmark-resolution-755.py b/tests/test-landmark-resolution-755.py index 81a9056c..031bb880 100644 --- a/tests/test-landmark-resolution-755.py +++ b/tests/test-landmark-resolution-755.py @@ -27,7 +27,7 @@ def load_functions(name: str, selected: set[str], namespace: dict) -> types.Modu path = ROOT / "truememory" / (name + ".py") module = types.ModuleType("synthetic_landmark755_" + name) module.__dict__.update(namespace) - tree = ast.parse(path.read_text(), filename=str(path)) + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) body = [] for node in tree.body: if isinstance(node, ast.FunctionDef) and node.name in selected: @@ -43,11 +43,11 @@ def load_functions(name: str, selected: set[str], namespace: dict) -> types.Modu STORAGE = types.ModuleType("synthetic_landmark755_storage") -exec(compile((ROOT / "truememory/storage.py").read_text(), "storage.py", "exec"), STORAGE.__dict__) +exec(compile((ROOT / "truememory/storage.py").read_text(encoding="utf-8"), "storage.py", "exec"), STORAGE.__dict__) TEMPORAL = types.ModuleType("synthetic_landmark755_temporal") TEMPORAL._row_to_dict = STORAGE._row_to_dict TEMPORAL.select_message_cols = STORAGE.select_message_cols -tree = ast.parse((ROOT / "truememory/temporal.py").read_text()) +tree = ast.parse((ROOT / "truememory/temporal.py").read_text(encoding="utf-8")) tree.body = [node for node in tree.body if not ( isinstance(node, ast.ImportFrom) and node.module == "truememory.storage" )] @@ -507,7 +507,7 @@ def test_actual_engine_rescope_rejects_disjoint_gap_and_unknown_coverage(self) - class BoundaryControls(unittest.TestCase): def test_original_parser_and_landmark_writer_ast_unchanged(self) -> None: - current = (ROOT / "truememory/temporal.py").read_text() + current = (ROOT / "truememory/temporal.py").read_text(encoding="utf-8") baseline_hashes = { "detect_temporal_intent": "2a752baf88d85db0eb61b91c087284c4e02dd3d4b89dece12bc5bf9e04ded287", "detect_landmark_events": "d3bbd201a06107765f0b80448f5cec8d2dde800d54f20e84cd442c137b91fe9c", @@ -566,7 +566,7 @@ def search_operation(**kwargs): self.assertEqual(len(json.loads(function("after Demo Day | before Launch", include_temporal_resolution=True))["temporal_resolutions"]), 2) def test_tools_count_and_empty_query_unchanged_by_default(self) -> None: - source = (ROOT / "truememory/mcp_server.py").read_text() + source = (ROOT / "truememory/mcp_server.py").read_text(encoding="utf-8") tree = ast.parse(source) tools = [node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name.startswith("truememory_")] self.assertEqual(len(tools), 11) @@ -575,7 +575,7 @@ def test_tools_count_and_empty_query_unchanged_by_default(self) -> None: self.assertEqual(json.loads(functions.truememory_search_deep("", include_temporal_resolution=True))["results"], []) def test_engine_source_all_consumers_and_post_clean_diagnostics(self) -> None: - tree = ast.parse((ROOT / "truememory/engine.py").read_text()) + tree = ast.parse((ROOT / "truememory/engine.py").read_text(encoding="utf-8")) engine = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == "TrueMemoryEngine") methods = {node.name: node for node in engine.body if isinstance(node, ast.FunctionDef)} search_calls = [node for node in ast.walk(methods["search"]) if isinstance(node, ast.Call)] diff --git a/tests/test-openclaw-native-bridge-764.py b/tests/test-openclaw-native-bridge-764.py index 8ef6dc61..581f1604 100644 --- a/tests/test-openclaw-native-bridge-764.py +++ b/tests/test-openclaw-native-bridge-764.py @@ -136,7 +136,7 @@ def test_wrapper_has_no_capture_lifecycle_or_shared_main(self) -> None: def test_installer_serializes_user_db_and_interpreter_without_execution(self) -> None: written: dict[str, str] = {} namespace = actual_source(ADAPTER, {"CLIAdapter": object, - "atomic_write_text": lambda p, t: written.update({str(p): t})}) + "atomic_write_text": lambda p, t: written.update({Path(p).as_posix(): t})}) namespace["_PLUGINS_DIR"] = Path("/synthetic/.openclaw/extensions") config_path = Path("/synthetic/.openclaw/openclaw.json") namespace["_CONFIG_PATH"] = config_path @@ -161,7 +161,7 @@ def test_installer_serializes_user_db_and_interpreter_without_execution(self) -> def permission_install(self, seed: object, *, raw: bool = False) -> dict[str, str]: written: dict[str, str] = {} namespace = actual_source(ADAPTER, {"CLIAdapter": object, - "atomic_write_text": lambda p, t: written.update({str(p): t})}) + "atomic_write_text": lambda p, t: written.update({Path(p).as_posix(): t})}) config_path = Path("/synthetic/.openclaw/openclaw.json") namespace["_CONFIG_PATH"] = config_path namespace["_PLUGINS_DIR"] = Path("/synthetic/.openclaw/extensions") diff --git a/tests/test-source-update-fence-748.py b/tests/test-source-update-fence-748.py index 325c6f15..a71920bf 100644 --- a/tests/test-source-update-fence-748.py +++ b/tests/test-source-update-fence-748.py @@ -22,7 +22,7 @@ def functions(relative: str, names: set[str], namespace: dict[str, object]) -> None: path = ROOT / relative nodes = [ast.ImportFrom(module="__future__", names=[ast.alias(name="annotations")], level=0)] - nodes.extend(node for node in ast.parse(path.read_text()).body + nodes.extend(node for node in ast.parse(path.read_text(encoding="utf-8")).body if isinstance(node, ast.FunctionDef) and node.name in names) if {node.name for node in nodes if isinstance(node, ast.FunctionDef)} != names: raise AssertionError("Missing actual source functions") @@ -105,7 +105,7 @@ def publication(conn: sqlite3.Connection, identity: object, memory_id: int, writer.__name__: writer, vector.__name__: vector, runtime.__name__: runtime}) self.modules.start() path = ROOT / "truememory/engine.py" - original = next(node for node in ast.parse(path.read_text()).body + original = next(node for node in ast.parse(path.read_text(encoding="utf-8")).body if isinstance(node, ast.ClassDef) and node.name == "TrueMemoryEngine") methods = [copy.deepcopy(node) for node in original.body if isinstance(node, ast.FunctionDef) and node.name in {"update", "get"}] diff --git a/truememory/ingest/hooks/hermes.py b/truememory/ingest/hooks/hermes.py index 80ac53d2..d996e944 100644 --- a/truememory/ingest/hooks/hermes.py +++ b/truememory/ingest/hooks/hermes.py @@ -158,7 +158,14 @@ def _read_owned(path: Path, limit: int = MAX_STATE_BYTES) -> dict[str, object]: before = os.fstat(stream.fileno()) raw = stream.read(limit + 1) after = os.fstat(stream.fileno()) - final = path.lstat() + if os.name == "nt": + final_fd = _open_owned(path, os.O_RDONLY) + try: + final = os.fstat(final_fd) + finally: + os.close(final_fd) + else: + final = path.lstat() _check_stat(final) def version(value: os.stat_result) -> tuple[int, int, int, int, int]: return (value.st_dev, value.st_ino, value.st_size, value.st_mtime_ns, value.st_ctime_ns) From ac3c447be0307d8f7025dc9b80729851bf89b755 Mon Sep 17 00:00:00 2001 From: buildingjoshbetter <236675935+buildingjoshbetter@users.noreply.github.com> Date: Mon, 12 Oct 2026 19:14:51 -0500 Subject: [PATCH 05/16] test: Measure Windows default owner and explicit file ownership --- .github/workflows/ci.yml | 2 +- .../windows-owned-file-diagnostics-v2.py | 318 ++++++++++++++++++ 2 files changed, 319 insertions(+), 1 deletion(-) create mode 100644 tests/native/windows-owned-file-diagnostics-v2.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9bab1729..2e4b5ef9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,7 +54,7 @@ jobs: run: pip install -e ".[all,dev]" - name: Trace synthetic Windows file ownership if: runner.os == 'Windows' - run: python tests/native/windows-owned-file-diagnostics-v1.py + run: python tests/native/windows-owned-file-diagnostics-v2.py - name: Run tests (gate — deterministic, no network) # Issue #426: exclude `network`-marked tests (real HuggingFace model # downloads) from the gate so a transient HfHubHTTPError can't fail CI diff --git a/tests/native/windows-owned-file-diagnostics-v2.py b/tests/native/windows-owned-file-diagnostics-v2.py new file mode 100644 index 00000000..49bba748 --- /dev/null +++ b/tests/native/windows-owned-file-diagnostics-v2.py @@ -0,0 +1,318 @@ +"""Observe default ownership and owner-only assignment on one retained fixture.""" +from __future__ import annotations + +import ctypes +import hashlib +import inspect +import json +import os +from pathlib import Path +import stat +import tempfile +import uuid + +MAX_BUFFER = 65536 +MAX_OUTPUT = 65536 +MAX_CALLS = 256 +SOURCE_BYTES = b'{"type":"user","message":{"content":"synthetic diagnostic"}}\n' + + +class DiagnosticError(ValueError): + pass + + +def identity(value: os.stat_result) -> list[int]: + return [value.st_dev, value.st_ino, value.st_size, value.st_mtime_ns, value.st_ctime_ns] + + +def main() -> None: + if os.name != "nt": + print(json.dumps({"platform": os.name, "executed": False})) + return + import msvcrt + from ctypes import wintypes + from truememory.ingest import completion + + directory = Path(tempfile.gettempdir()).resolve() / ("tm-windows-owned-diagnostic-v2-" + uuid.uuid4().hex) + directory.mkdir(mode=0o700) + source = directory / "synthetic-source.jsonl" + fd = os.open(source, os.O_CREAT | os.O_EXCL | os.O_RDWR | os.O_BINARY, 0o600) + calls: list[dict[str, object]] = [] + cases: list[dict[str, object]] = [] + comparisons: list[dict[str, object]] = [] + snapshots: list[dict[str, object]] = [] + original = ctypes.WinDLL + phase = "initial" + trace_bytes = 0 + trace_refusals: list[dict[str, str]] = [] + owner_handle: int | None = None + token = wintypes.HANDLE() + descriptor = ctypes.c_void_p() + kernel: object | None = None + buffers: list[object] = [] + outcome = "observed" + failure: str | None = None + + class FunctionTrace: + def __init__(self, name: str, function: object) -> None: + object.__setattr__(self, "name", name) + object.__setattr__(self, "function", function) + + def __getattr__(self, name: str) -> object: + return getattr(self.function, name) + + def __setattr__(self, name: str, value: object) -> None: + setattr(self.function, name, value) + + def __call__(self, *args: object) -> object: + nonlocal trace_bytes + if (len(calls) >= MAX_CALLS - 8 or trace_bytes > 45056) and self.name not in {"CloseHandle", "LocalFree"}: + if len(trace_refusals) < 8: + trace_refusals.append({"api": self.name, "phase": phase}) + raise DiagnosticError("trace_bound_before_native_call") + result = self.function(*args) + error = ctypes.get_last_error() + if self.name in {"GetSecurityInfo", "SetSecurityInfo"}: + displayed = int(result) + success = result == 0 + semantics = "DWORD_return_is_authoritative_last_error_unspecified" + elif self.name in {"CreateFileW", "GetCurrentProcess"}: + success = result is not None and result != ctypes.c_void_p(-1).value + if self.name == "GetCurrentProcess": + success = result is not None + displayed = bool(success) + semantics = "stale_or_unspecified_on_success" if success else "last_error_on_failure" + elif self.name == "LocalFree": + success = not result + displayed = bool(success) + semantics = "stale_or_unspecified_on_success" if success else "last_error_on_failure" + else: + displayed = bool(result) + success = bool(result) + semantics = "stale_or_unspecified_on_success" if success else "last_error_on_failure" + if self.name in {"EqualSid", "IsWellKnownSid"}: + semantics = "last_error_not_authoritative_for_boolean_match_result" + row: dict[str, object] = {"api": self.name, "phase": phase, "result": displayed, + "last_error": error, "last_error_semantics": semantics, + "last_error_may_be_stale": success or self.name in { + "GetSecurityInfo", "SetSecurityInfo", "EqualSid", "IsWellKnownSid"}} + if self.name == "GetTokenInformation": + row.update(information_class=int(args[1]), buffer_bytes=int(args[3]), + needed_bytes=ctypes.cast(args[4], ctypes.POINTER(wintypes.DWORD)).contents.value) + elif self.name in {"GetSecurityInfo", "SetSecurityInfo"}: + row["security_information"] = int(args[2]) + elif self.name == "IsWellKnownSid": + row["well_known_type"] = int(args[1]) + elif self.name == "CreateFileW": + row.update(desired_access=int(args[1]), share_mode=int(args[2]), + creation_disposition=int(args[4]), flags=int(args[5])) + size = len(json.dumps(row, sort_keys=True).encode("utf-8")) + if size > 1024: + raise DiagnosticError("trace_bound") + trace_bytes += size + calls.append(row) + return result + + class LibraryTrace: + def __init__(self, library: object) -> None: + self.library = library + self.functions: dict[str, FunctionTrace] = {} + + def __getattr__(self, name: str) -> FunctionTrace: + if name not in self.functions: + self.functions[name] = FunctionTrace(name, getattr(self.library, name)) + return self.functions[name] + + def traced_library(*args: object, **kwargs: object) -> LibraryTrace: + return LibraryTrace(original(*args, **kwargs)) + + def snapshot(label: str) -> dict[str, object]: + if source.parent != directory or source.name != "synthetic-source.jsonl": + raise DiagnosticError("fixture_path_changed") + if directory.is_symlink() or source.is_symlink() or source.resolve() != source: + raise DiagnosticError("fixture_link") + info, path_info = os.fstat(fd), source.lstat() + if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1 or identity(info) != identity(path_info): + raise DiagnosticError("fixture_identity_changed") + if getattr(info, "st_file_attributes", 0) & 0x400 or getattr(path_info, "st_file_attributes", 0) & 0x400: + raise DiagnosticError("fixture_reparse_point") + os.lseek(fd, 0, os.SEEK_SET) + data = os.read(fd, MAX_BUFFER + 1) + if data != SOURCE_BYTES or info.st_size != len(SOURCE_BYTES): + raise DiagnosticError("fixture_bytes_changed") + row = {"phase": label, "fstat": identity(info), "lstat": identity(path_info), + "byte_count": len(data), "bytes_hex": data.hex(), "sha256": hashlib.sha256(data).hexdigest()} + snapshots.append(row) + return row + + def completion_cases(label: str) -> None: + nonlocal phase + for private, flags in ((False, os.O_RDONLY), (True, os.O_RDWR)): + phase = label + ("_private" if private else "_public") + opened = os.open(source, flags | os.O_BINARY) + try: + if identity(os.fstat(opened)) != identity(os.fstat(fd)): + raise DiagnosticError("completion_descriptor_identity") + start = len(calls) + ctypes.WinDLL = traced_library + try: + accepted = completion._windows_security(msvcrt.get_osfhandle(opened), private=private) + finally: + ctypes.WinDLL = original + cases.append({"phase": label, "private": private, "accepted": accepted, + "call_start": start, "call_end": len(calls), + "fstat": identity(os.fstat(opened)), "lstat": identity(source.lstat())}) + finally: + os.close(opened) + + class FileTime(ctypes.Structure): + _fields_ = [("low", wintypes.DWORD), ("high", wintypes.DWORD)] + + class FileInformation(ctypes.Structure): + _fields_ = [("attributes", wintypes.DWORD), ("creation", FileTime), ("access", FileTime), + ("write", FileTime), ("volume", wintypes.DWORD), ("size_high", wintypes.DWORD), + ("size_low", wintypes.DWORD), ("links", wintypes.DWORD), + ("index_high", wintypes.DWORD), ("index_low", wintypes.DWORD)] + + def native_file_identity(handle: int) -> tuple[int, int, int]: + info = FileInformation() + if not kernel.GetFileInformationByHandle(handle, ctypes.byref(info)): + raise DiagnosticError("native_file_identity_unavailable") + if info.attributes & 0x400 or info.attributes & 0x10 or info.links != 1: + raise DiagnosticError("native_file_not_single_regular_no_reparse") + return (info.volume, info.index_high, info.index_low) + + try: + remaining = SOURCE_BYTES + while remaining: + written = os.write(fd, remaining) + if written <= 0: + raise DiagnosticError("fixture_write_no_progress") + remaining = remaining[written:] + os.fsync(fd) + snapshot("before") + completion_cases("before_owner_assignment") + phase = "owner_diagnostic" + security = traced_library("advapi32", use_last_error=True) + kernel = traced_library("kernel32", use_last_error=True) + pointer = ctypes.c_void_p + security.GetSecurityInfo.argtypes = [wintypes.HANDLE, wintypes.DWORD, wintypes.DWORD, + ctypes.POINTER(pointer), pointer, pointer, pointer, ctypes.POINTER(pointer)] + security.GetSecurityInfo.restype = wintypes.DWORD + security.OpenProcessToken.argtypes = [wintypes.HANDLE, wintypes.DWORD, ctypes.POINTER(wintypes.HANDLE)] + security.OpenProcessToken.restype = wintypes.BOOL + security.GetTokenInformation.argtypes = [wintypes.HANDLE, wintypes.DWORD, pointer, + wintypes.DWORD, ctypes.POINTER(wintypes.DWORD)] + security.GetTokenInformation.restype = wintypes.BOOL + security.EqualSid.argtypes = [pointer, pointer] + security.EqualSid.restype = wintypes.BOOL + security.IsWellKnownSid.argtypes = [pointer, wintypes.DWORD] + security.IsWellKnownSid.restype = wintypes.BOOL + security.IsValidSid.argtypes = [pointer] + security.IsValidSid.restype = wintypes.BOOL + security.SetSecurityInfo.argtypes = [wintypes.HANDLE, wintypes.DWORD, wintypes.DWORD, + pointer, pointer, pointer, pointer] + security.SetSecurityInfo.restype = wintypes.DWORD + kernel.GetCurrentProcess.restype = wintypes.HANDLE + kernel.CloseHandle.argtypes = [wintypes.HANDLE] + kernel.CloseHandle.restype = wintypes.BOOL + kernel.LocalFree.argtypes = [pointer] + kernel.LocalFree.restype = pointer + kernel.CreateFileW.argtypes = [wintypes.LPCWSTR, wintypes.DWORD, wintypes.DWORD, pointer, + wintypes.DWORD, wintypes.DWORD, wintypes.HANDLE] + kernel.CreateFileW.restype = wintypes.HANDLE + kernel.GetFileInformationByHandle.argtypes = [wintypes.HANDLE, ctypes.POINTER(FileInformation)] + kernel.GetFileInformationByHandle.restype = wintypes.BOOL + if not security.OpenProcessToken(kernel.GetCurrentProcess(), 8, ctypes.byref(token)): + raise DiagnosticError("token_query_open_failed") + + def token_sid(information_class: int) -> ctypes.c_void_p: + needed = wintypes.DWORD() + ctypes.set_last_error(0) + queried = security.GetTokenInformation(token, information_class, None, 0, ctypes.byref(needed)) + if queried or ctypes.get_last_error() != 122 or not ctypes.sizeof(pointer) <= needed.value <= MAX_BUFFER: + raise DiagnosticError("token_size_query_failed_or_bound") + buffer = ctypes.create_string_buffer(needed.value) + if not security.GetTokenInformation(token, information_class, buffer, len(buffer), ctypes.byref(needed)): + raise DiagnosticError("token_information_failed") + if needed.value > len(buffer): + raise DiagnosticError("token_information_buffer_changed") + sid = pointer.from_buffer(buffer) + if not sid or not ctypes.addressof(buffer) <= sid.value < ctypes.addressof(buffer) + len(buffer): + raise DiagnosticError("token_sid_outside_owned_buffer") + if not security.IsValidSid(sid): + raise DiagnosticError("token_sid_invalid") + buffers.append(buffer) + return sid + + user_sid, token_owner_sid = token_sid(1), token_sid(4) + + def owner_comparison(handle: int, label: str) -> None: + nonlocal descriptor + owner = pointer() + descriptor = pointer() + try: + if security.GetSecurityInfo(handle, 1, 1, ctypes.byref(owner), None, None, None, ctypes.byref(descriptor)): + raise DiagnosticError("owner_query_failed") + if not owner or not security.IsValidSid(owner): + raise DiagnosticError("owner_sid_invalid") + comparisons.append({"phase": label, + "owner_is_builtin_administrators": bool(security.IsWellKnownSid(owner, 26)), + "owner_is_local_system": bool(security.IsWellKnownSid(owner, 22)), + "owner_equals_token_user": bool(security.EqualSid(owner, user_sid)), + "owner_equals_token_owner": bool(security.EqualSid(owner, token_owner_sid)), + "token_owner_equals_token_user": bool(security.EqualSid(token_owner_sid, user_sid))}) + finally: + if descriptor: + if kernel.LocalFree(descriptor): + raise DiagnosticError("security_descriptor_free_failed") + descriptor = pointer() + + owner_comparison(msvcrt.get_osfhandle(fd), "before_owner_assignment") + snapshot("before_CreateFileW") + phase = "owner_assignment" + owner_handle = kernel.CreateFileW(str(source), 0x00020000 | 0x00080000, 1 | 2, + None, 3, 0x80 | 0x00200000, None) + if owner_handle is None or owner_handle == ctypes.c_void_p(-1).value: + owner_handle = None + raise DiagnosticError("owner_assignment_handle_denied") + if native_file_identity(owner_handle) != native_file_identity(msvcrt.get_osfhandle(fd)): + raise DiagnosticError("owner_assignment_wrong_file") + snapshot("before_SetSecurityInfo") + if security.SetSecurityInfo(owner_handle, 1, 1, user_sid, None, None, None): + outcome = "owner_assignment_returned_error" + snapshot("after_SetSecurityInfo") + owner_comparison(owner_handle, "after_owner_assignment") + completion_cases("after_owner_assignment") + snapshot("after") + except (OSError, ValueError, AttributeError) as error: + outcome = "diagnostic_incomplete" + failure = type(error).__name__ + (":" + str(error) if isinstance(error, DiagnosticError) else "") + finally: + ctypes.WinDLL = original + phase = "cleanup" + if kernel is not None: + if descriptor: + kernel.LocalFree(descriptor) + if owner_handle is not None: + kernel.CloseHandle(owner_handle) + if token: + kernel.CloseHandle(token) + os.close(fd) + output = {"schema": "truememory.windows.owned_file.diagnostic.v2", "executed": True, + "synthetic_only": True, "retained_directory_basename": directory.name, + "retained_file_basename": source.name, "source_bytes_hex": SOURCE_BYTES.hex(), + "source_sha256": hashlib.sha256(inspect.getsource(completion._windows_security).encode()).hexdigest(), + "outcome": outcome, "failure": failure, "cases": cases, "comparisons": comparisons, + "snapshots": snapshots, "calls": calls, "trace_refusals": trace_refusals, + "acceptance_claimed": False, + "token_or_privilege_or_DACL_or_group_changes": False, + "only_requested_security_information": "OWNER_SECURITY_INFORMATION"} + data = json.dumps(output, sort_keys=True, allow_nan=False) + if len(data.encode("utf-8")) > MAX_OUTPUT: + raise DiagnosticError("output_bound") + print(data) + + +if __name__ == "__main__": + main() From 38620d958810ebdafa6f8ecc8a68be4092bb3f0f Mon Sep 17 00:00:00 2001 From: buildingjoshbetter <236675935+buildingjoshbetter@users.noreply.github.com> Date: Mon, 12 Oct 2026 19:18:07 -0500 Subject: [PATCH 06/16] feat: Integrate accepted memory accounting and bounded launch controls --- pyproject.toml | 4 + tests/native/mac-native-acceptance-v1.py | 874 +++ tests/native/mac-native-acceptance-v2.py | 885 +++ tests/native/mac-native-acceptance-v4.py | 895 +++ tests/native/mac-native-acceptance-v6.py | 966 +++ tests/native/mac-native-acceptance-v7.py | 966 +++ tests/native/mac-native-bootstrap-v1.py | 378 ++ tests/native/mac-native-bootstrap-v2.py | 378 ++ tests/native/mac-native-bootstrap-v4.py | 557 ++ tests/native/mac-native-bootstrap-v6.py | 918 +++ tests/native/mac-native-bootstrap-v7.py | 918 +++ .../mac-native-package-acceptance-v3.py | 1053 ++++ .../mac-native-package-acceptance-v4.py | 1053 ++++ .../native/mac-native-package-bootstrap-v3.py | 415 ++ .../native/mac-native-package-bootstrap-v4.py | 431 ++ ...st-clustered-search-materialization-754.py | 9 + tests/test-linux-preexec-enforcement-297.py | 1042 ++++ tests/test-linux-startup-correlation-297.py | 452 ++ tests/test-linux-startup-owner-297.py | 424 ++ .../test-linux-startup-transport-owner-297.py | 500 ++ tests/test-mac-native-acceptance-v1.py | 341 ++ tests/test-mac-native-acceptance-v2.py | 525 ++ tests/test-mac-native-acceptance-v4.py | 1273 ++++ tests/test-mac-native-acceptance-v6.py | 1987 ++++++ tests/test-mac-native-acceptance-v7.py | 2246 +++++++ .../test-mac-native-package-acceptance-v3.py | 859 +++ .../test-mac-native-package-acceptance-v4.py | 1098 ++++ tests/test-memory-enforcement-native-297.py | 131 + ...test-source-capture-admission-native-v1.py | 292 + tests/test-tier-streamed-worker-756.py | 1 + tests/test-whole-process-memory-budget-297.py | 812 +++ tests/test_issue_577_model_server_oom.py | 7 +- truememory/__init__.py | 1 + truememory/clustering.py | 18 +- truememory/hybrid.py | 17 +- truememory/ingest/completion.py | 46 +- truememory/ingest/encoding_gate.py | 29 +- truememory/memory_bootstrap.py | 133 + truememory/memory_budget.py | 983 +++ truememory/model_client.py | 233 +- truememory/model_server.py | 549 +- truememory/reranker.py | 57 +- truememory/tier_switch/worker.py | 36 +- truememory/vector_search.py | 293 +- truememory_memory_bootstrap.py | 5367 +++++++++++++++++ 45 files changed, 30134 insertions(+), 318 deletions(-) create mode 100644 tests/native/mac-native-acceptance-v1.py create mode 100644 tests/native/mac-native-acceptance-v2.py create mode 100644 tests/native/mac-native-acceptance-v4.py create mode 100644 tests/native/mac-native-acceptance-v6.py create mode 100644 tests/native/mac-native-acceptance-v7.py create mode 100644 tests/native/mac-native-bootstrap-v1.py create mode 100644 tests/native/mac-native-bootstrap-v2.py create mode 100644 tests/native/mac-native-bootstrap-v4.py create mode 100644 tests/native/mac-native-bootstrap-v6.py create mode 100644 tests/native/mac-native-bootstrap-v7.py create mode 100644 tests/native/mac-native-package-acceptance-v3.py create mode 100644 tests/native/mac-native-package-acceptance-v4.py create mode 100644 tests/native/mac-native-package-bootstrap-v3.py create mode 100644 tests/native/mac-native-package-bootstrap-v4.py create mode 100644 tests/test-linux-preexec-enforcement-297.py create mode 100644 tests/test-linux-startup-correlation-297.py create mode 100644 tests/test-linux-startup-owner-297.py create mode 100644 tests/test-linux-startup-transport-owner-297.py create mode 100644 tests/test-mac-native-acceptance-v1.py create mode 100644 tests/test-mac-native-acceptance-v2.py create mode 100644 tests/test-mac-native-acceptance-v4.py create mode 100644 tests/test-mac-native-acceptance-v6.py create mode 100644 tests/test-mac-native-acceptance-v7.py create mode 100644 tests/test-mac-native-package-acceptance-v3.py create mode 100644 tests/test-mac-native-package-acceptance-v4.py create mode 100644 tests/test-memory-enforcement-native-297.py create mode 100644 tests/test-source-capture-admission-native-v1.py create mode 100644 tests/test-whole-process-memory-budget-297.py create mode 100644 truememory/memory_bootstrap.py create mode 100644 truememory/memory_budget.py create mode 100644 truememory_memory_bootstrap.py diff --git a/pyproject.toml b/pyproject.toml index f9be3037..9d9f2dc8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,7 @@ all = ["truememory[agentic,clustering]"] truememory-mcp = "truememory.mcp_server:main" truememory-ingest = "truememory.ingest.cli:main" truememory-model-server = "truememory.model_server:main" +truememory-model-server-capped = "truememory_memory_bootstrap:main" [tool.setuptools.package-data] truememory = ["assets/*.icns"] @@ -99,6 +100,9 @@ select = ["E4", "E7", "E9", "F"] [tool.hatch.build.targets.wheel] packages = ["truememory"] +[tool.hatch.build.targets.wheel.force-include] +"truememory_memory_bootstrap.py" = "truememory_memory_bootstrap.py" + # CLAUDE_TEMPLATE.md lives inside the package at truememory/ingest/CLAUDE_TEMPLATE.md # so it's auto-included in the wheel — no force-include needed. diff --git a/tests/native/mac-native-acceptance-v1.py b/tests/native/mac-native-acceptance-v1.py new file mode 100644 index 00000000..f9862e69 --- /dev/null +++ b/tests/native/mac-native-acceptance-v1.py @@ -0,0 +1,874 @@ +"""ROOT-run Stage A supervisor. Importing this module never starts native work. + +This instrumented source-module harness proves tiny filesystem/lock behavior. +It does not certify installed packages, host dispatch, models or a physical cap. +""" +from __future__ import annotations + +import argparse +import hashlib +import importlib.abc +import importlib.machinery +import importlib.util +import json +import math +import os +import re +import select +import selectors +import signal +import stat +import subprocess +import sys +import time +from dataclasses import asdict, dataclass +from pathlib import Path +from types import ModuleType + +RUN_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/native-mac-v1/run-0001") +SOURCE_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/worktrees/mac-native-acceptance-v1") +RUNTIME_PACKET = Path("/Users/j/dev/truememory-maintenance-v1/.work/reports/mac-native-runtime-pins-v1.json") +RUNTIME_PACKET_SHA = "8db57e3f700aebb3adeb383c719a4bc5e2525917f4c017c853ee4694be1ee1e4" +PROOF_SCOPE = "instrumented_unmodified_source_native_filesystem_lock_capture" +SOURCE_FILES = ( + "truememory/_platform.py", "truememory/ingest/hooks/_shared.py", + "truememory/ingest/completion.py", "truememory/ingest/transcript.py", + "truememory/ingest/provenance.py", +) +CASES = frozenset({"capture-a1", "capture-a2", "capture-revision", "negative-a3", "invalid-state", "flock-a4", "parser-a5"}) +LIMITS = {"wall_seconds": 5, "termination_seconds": 1, "cpu_soft": 2, "cpu_hard": 3, + "rss_abort_bytes": 67108864, "core_bytes": 0, "nofile": 64, + "fsize_bytes": 1048576, "input_bytes": 65536, "output_bytes": 262144, + "events": 32, "frame_bytes": 2048, "ack_ms": 100, "threads": 1} +FREE_MIN = 268435456 +MAX_INT = (1 << 63) - 1 +_SHA = re.compile(r"[0-9a-f]{64}\Z") +_PENDING = re.compile(r"state-[0-9a-f]{32}\.pending\Z") +_announcement_bytes = 0 + + +class SafetyError(ValueError): + pass + + +def integer(value: object, *, minimum: int = 0, maximum: int = MAX_INT) -> int: + if type(value) is not int or not minimum <= value <= maximum: + raise SafetyError("invalid_integer") + return value + + +def exact_keys(value: object, keys: set[str]) -> dict: + if type(value) is not dict or set(value) != keys: + raise SafetyError("invalid_fields") + return value + + +def encoded(value: object) -> bytes: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8") + + +def digest(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def hash_text(value: object) -> str: + if type(value) is not str or _SHA.fullmatch(value) is None: + raise SafetyError("invalid_hash") + return value + + +def canonical_path(value: object, root: Path) -> Path: + if type(value) is not str or not value or "\0" in value or len(value.encode("utf-8")) > 1024: + raise SafetyError("invalid_path") + path = Path(value) + if not path.is_absolute() or ".." in path.parts or str(path) != value: + raise SafetyError("noncanonical_path") + if path != root and not path.is_relative_to(root): + raise SafetyError("path_escape") + return path + + +def owned_path(path: Path, *, directory: bool = False) -> os.stat_result: + if path.resolve() != path or any(item.is_symlink() for item in (path, *path.parents)): + raise SafetyError("path_link") + info = path.lstat() + if info.st_uid != os.getuid() or info.st_mode & 0o077: + raise SafetyError("path_not_private_owned") + if directory: + if not stat.S_ISDIR(info.st_mode): + raise SafetyError("path_not_directory") + elif not stat.S_ISREG(info.st_mode) or info.st_nlink != 1: + raise SafetyError("path_not_regular_single_link") + return info + + +def read_bounded(path: Path, maximum: int, *, private: bool = True) -> bytes: + if private: + owned_path(path) + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + try: + before = os.fstat(fd) + if not stat.S_ISREG(before.st_mode) or before.st_size > maximum: + raise SafetyError("read_limit_or_type") + if private and (before.st_uid != os.getuid() or before.st_mode & 0o077 or before.st_nlink != 1): + raise SafetyError("read_not_private") + chunks: list[bytes] = [] + remaining = maximum + 1 + while remaining: + chunk = os.read(fd, min(remaining, 65536)) + if not chunk: + break + chunks.append(chunk) + remaining -= len(chunk) + after = os.fstat(fd) + data = b"".join(chunks) + if file_identity(before) != file_identity(after) or len(data) > maximum or len(data) != after.st_size: + raise SafetyError("read_changed_or_limit") + return data + finally: + os.close(fd) + + +def file_identity(info: os.stat_result) -> list[int]: + return [info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns, info.st_ctime_ns] + + +@dataclass(frozen=True) +class Sensor: + monotonic_ns: int + pressure: int + free_bytes: int + swap_used_bytes: int + + +def valid_sensor(value: object) -> Sensor: + row = exact_keys(value, {"monotonic_ns", "pressure", "free_bytes", "swap_used_bytes"}) + integer(row["monotonic_ns"], minimum=1) + integer(row["pressure"], minimum=1, maximum=4) + integer(row["free_bytes"]) + integer(row["swap_used_bytes"]) + if row["pressure"] not in (1, 2, 4): + raise SafetyError("unknown_pressure_abi") + return Sensor(**row) + + +def admission(samples: list[Sensor], now_ns: int) -> None: + integer(now_ns, minimum=1) + if len(samples) < 3: + raise SafetyError("sensor_count") + previous = None + for sample in samples: + valid_sensor(asdict(sample)) + if sample.pressure != 1 or sample.free_bytes < FREE_MIN: + raise SafetyError("pressure_or_headroom") + if previous is not None and (sample.monotonic_ns <= previous.monotonic_ns or sample.swap_used_bytes > previous.swap_used_bytes): + raise SafetyError("unordered_or_growing_swap") + previous = sample + if samples[-1].monotonic_ns - samples[0].monotonic_ns < 2_000_000_000: + raise SafetyError("sensor_window") + if not 0 <= now_ns - samples[-1].monotonic_ns <= 500_000_000: + raise SafetyError("sensor_stale") + + +@dataclass(frozen=True) +class Identity: + pid: int + uid: int + birth_ns: int + pgid: int + + +def same_owned_identity(expected: Identity, actual: Identity, worker_uid: int) -> bool: + try: + for row in (expected, actual): + integer(row.pid, minimum=1) + integer(row.uid) + integer(row.birth_ns, minimum=1) + integer(row.pgid, minimum=1) + integer(worker_uid) + except SafetyError: + return False + return expected == actual and actual.uid == worker_uid and actual.pgid == actual.pid + + +def account_directory(case: dict) -> Path: + key = digest(encoded(["native-acceptance-v1", case["database"]])) + return RUN_ROOT / "completion" / key + + +def capture_metadata(case: dict) -> dict: + return {"version": 1, "user_id": "native-acceptance-v1", "database": case["database"], + "host": "claude", "session_id": case["session"], "source": case["source"], + "source_suffix": ".jsonl", "file_identity": case["source_identity"], + "byte_count": case["source_bytes"], "sha256": case["source_sha256"]} + + +def validate_case(value: object) -> dict: + row = exact_keys(value, {"id", "source", "source_sha256", "source_bytes", "source_identity", "session", "database", "previous"}) + if type(row["id"]) is not str or row["id"] not in CASES: + raise SafetyError("unsupported_case") + base = RUN_ROOT / "home/.claude/projects/native-acceptance-v1" + filename = {"capture-revision": "capture-a2", "invalid-state": "negative-a3"}.get(row["id"], row["id"]) + if row["id"] != "flock-a4": + if canonical_path(row["source"], RUN_ROOT) != base / (filename + ".jsonl"): + raise SafetyError("unexpected_fixture") + hash_text(row["source_sha256"]) + integer(row["source_bytes"], minimum=1, maximum=4096) + if type(row["source_identity"]) is not list or len(row["source_identity"]) != 5: + raise SafetyError("fixture_identity") + for number in row["source_identity"]: + integer(number) + if row["source_identity"][2] != row["source_bytes"]: + raise SafetyError("fixture_size_identity") + elif any(row[name] is not None for name in ("source", "source_sha256", "source_bytes", "source_identity")): + raise SafetyError("unexpected_lock_fixture") + if row["session"] != "synthetic-native-" + filename: + raise SafetyError("unexpected_session") + database_case = "capture-a2" if row["id"] == "capture-revision" else "negative-a3" if row["id"] == "invalid-state" else row["id"] + if row["database"] != str(RUN_ROOT / (database_case + "-v1.db")): + raise SafetyError("unexpected_database_locator") + if row["id"] == "capture-revision": + old = exact_keys(row["previous"], {"key", "sha256", "byte_count"}) + hash_text(old["key"]) + hash_text(old["sha256"]) + integer(old["byte_count"], minimum=1, maximum=4096) + if old["key"] == digest(encoded(capture_metadata(row))): + raise SafetyError("revision_not_new_evidence") + elif row["previous"] is not None: + raise SafetyError("unexpected_previous") + return row + + +def validate_manifest(value: object) -> dict: + row = exact_keys(value, {"version", "stage", "synthetic", "root_execution_go", "run_root", "home", "source_root", "source_commit", "sources", "supervisor_sha256", "bootstrap_sha256", "limits", "cases"}) + if type(row["version"]) is not int or row["version"] != 1 or row["stage"] != "A" or row["synthetic"] is not True or row["root_execution_go"] is not True: + raise SafetyError("manifest_authority_or_version") + if row["run_root"] != str(RUN_ROOT) or row["home"] != str(RUN_ROOT / "home") or row["source_root"] != str(SOURCE_ROOT): + raise SafetyError("unexpected_root") + if type(row["source_commit"]) is not str or re.fullmatch(r"[0-9a-f]{40}", row["source_commit"]) is None: + raise SafetyError("source_commit") + sources = exact_keys(row["sources"], set(SOURCE_FILES)) + for value in sources.values(): + hash_text(value) + hash_text(row["supervisor_sha256"]) + hash_text(row["bootstrap_sha256"]) + if type(row["limits"]) is not dict or row["limits"] != LIMITS or any(type(v) is not int for v in row["limits"].values()): + raise SafetyError("limit_change") + if type(row["cases"]) is not list or not 1 <= len(row["cases"]) <= len(CASES): + raise SafetyError("case_count") + cases = [validate_case(case) for case in row["cases"]] + if len({case["id"] for case in cases}) != len(cases): + raise SafetyError("duplicate_case") + return row + + +def validate_ready(value: object, pid: int) -> dict: + row = exact_keys(value, {"type", "version", "pid", "limits", "audit_installed"}) + integer(pid, minimum=1) + if row["type"] != "ready" or type(row["version"]) is not int or row["version"] != 1 or type(row["pid"]) is not int or row["pid"] != pid or row["audit_installed"] is not True: + raise SafetyError("limits_ready_mismatch") + expected = {"cpu": [2, 3], "core": [0, 0], "nofile": [64, 64], "fsize": [1048576, 1048576]} + limits = exact_keys(row["limits"], set(expected)) + for name, pair in limits.items(): + if type(pair) is not list or len(pair) != 2 or any(type(number) is not int for number in pair) or pair != expected[name]: + raise SafetyError("limits_ready_mismatch") + return row + + +def validate_result(value: object, case: dict, sources: dict) -> dict: + row = exact_keys(value, {"type", "version", "case", "passed", "proof_scope", "import_mode", "sources", "details"}) + if row["type"] != "result" or type(row["version"]) is not int or row["version"] != 1 or row["case"] != case["id"] or row["passed"] is not True or row["proof_scope"] != PROOF_SCOPE or row["import_mode"] != "isolated_source_module" or row["sources"] != sources or type(row["details"]) is not dict: + raise SafetyError("result_scope_mismatch") + return row + + +def evidence_paths(manifest_path: Path) -> tuple[Path, str]: + if manifest_path == RUN_ROOT / "manifest-v1.json": + return RUN_ROOT / "results-v1.json", "v1" + if manifest_path == RUN_ROOT / "manifest-v2.json": + return RUN_ROOT / "results-v2.json", "v2" + raise SafetyError("unexpected_manifest_path") + + +class MutationPolicy: + def __init__(self, case: dict) -> None: + self.case = case + self.account = account_directory(case) + self.directory_paths = {self.account, self.account.parent, *(self.account / name for name in ("captures", "receipts", "logs"))} + self.fixed = {self.account / "state.lock": "Completion account lock", self.account / "state.json": "Atomic completion state"} + if case["id"] not in {"flock-a4", "parser-a5"}: + self.fixed[self.account / "captures" / (digest(encoded(capture_metadata(case))) + ".jsonl")] = "Immutable synthetic source capture" + if case["id"] in {"negative-a3", "parser-a5"}: + self.directory_paths = set() + self.fixed = {} + elif case["id"] == "invalid-state": + self.fixed = {self.account / "state.lock": "Completion account lock"} + elif case["id"] == "flock-a4": + self.directory_paths = set() + self.fixed = {RUN_ROOT / "locks/contention-v1.lock": "Native lock contention witness"} + self.pending: set[Path] = set() + self.sequence = 0 + + def decide(self, value: object) -> dict: + row = exact_keys(value, {"type", "version", "seq", "event", "path", "source", "flags"}) + if row["type"] != "mutation" or type(row["version"]) is not int or row["version"] != 1: + raise SafetyError("mutation_shape") + seq = integer(row["seq"], minimum=1, maximum=LIMITS["events"]) + if seq != self.sequence + 1: + raise SafetyError("mutation_sequence") + path = canonical_path(row["path"], RUN_ROOT) + flags = integer(row["flags"], maximum=(1 << 31) - 1) + purpose = self.fixed.get(path) + if row["event"] == "os.mkdir": + if row["source"] is not None or path not in self.directory_paths or flags != 0o700: + raise SafetyError("unplanned_directory") + purpose = "Private completion directory" + elif row["event"] == "open": + if row["source"] is not None or flags & os.O_TRUNC: + raise SafetyError("unplanned_open") + access = flags & os.O_ACCMODE + extras = flags & ~(os.O_ACCMODE | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW | getattr(os, "O_CLOEXEC", 0)) + if extras or access not in (os.O_WRONLY, os.O_RDWR) or not flags & os.O_CREAT: + raise SafetyError("unplanned_open_flags") + if path.parent == self.account and _PENDING.fullmatch(path.name): + if self.case["id"] not in {"capture-a1", "capture-a2", "capture-revision"} or access != os.O_WRONLY or not flags & os.O_EXCL or len(self.pending | {path}) > 1: + raise SafetyError("pending_slot_limit") + self.pending.add(path) + purpose = "Atomic completion-state temporary; failed evidence retained" + elif path not in self.fixed or path.name == "state.json": + raise SafetyError("unplanned_file") + elif path.name == "state.lock" or path == RUN_ROOT / "locks/contention-v1.lock": + if access != os.O_RDWR or flags & os.O_EXCL: + raise SafetyError("lock_flags") + elif access != os.O_WRONLY or not flags & os.O_EXCL: + raise SafetyError("capture_flags") + elif row["event"] == "os.rename": + source = canonical_path(row["source"], RUN_ROOT) + if flags != 0 or path != self.account / "state.json" or source not in self.pending: + raise SafetyError("unplanned_replace") + purpose = "Publish current-run completion state from its approved temporary" + else: + raise SafetyError("unsupported_mutation") + self.sequence = seq + return {"type": "grant", "version": 1, "seq": seq, "path": str(path), "purpose": purpose} + + +def mutation_target(grant: dict, request: dict) -> None: + path = Path(grant["path"]) + owned_path(path.parent, directory=True) + if path.exists() or path.is_symlink(): + owned_path(path, directory=request["event"] == "os.mkdir") + if request["event"] == "os.rename": + owned_path(Path(request["source"])) + + +class PressureReader: + def __init__(self) -> None: + import ctypes + self.ctypes = ctypes + self.library = ctypes.CDLL("/usr/lib/libSystem.B.dylib", use_errno=True) + self.read = self.library.sysctlbyname + self.read.argtypes = [ctypes.c_char_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.c_void_p, ctypes.c_size_t] + self.read.restype = ctypes.c_int + + def pressure(self) -> int: + c = self.ctypes + output = c.c_uint32() + size = c.c_size_t(c.sizeof(output)) + if self.read(b"kern.memorystatus_vm_pressure_level", c.byref(output), c.byref(size), None, 0) != 0 or size.value != 4: + raise SafetyError("pressure_unavailable") + if output.value not in (1, 2, 4): + raise SafetyError("pressure_abi_unknown") + return output.value + + +class PinnedPythonLoader(importlib.abc.Loader): + def __init__(self, path: Path, checksum: str) -> None: + self.path, self.checksum = path, checksum + + def create_module(self, spec: object) -> None: + return None + + def exec_module(self, module: ModuleType) -> None: + data = read_bounded(self.path, 262144, private=False) + if digest(data) != self.checksum: + raise SafetyError("dependency_hash_changed") + exec(compile(data, str(self.path), "exec", dont_inherit=True), module.__dict__) + + +class PinnedPsutilFinder(importlib.abc.MetaPathFinder): + def __init__(self, packet: dict) -> None: + self.root = Path(packet["dependency_root"]) + self.files = packet["dependency_files"] + + def find_spec(self, fullname: str, path: object = None, target: object = None) -> object: + if fullname != "psutil" and not fullname.startswith("psutil."): + return None + relative = fullname.split(".")[1:] + candidate = self.root.joinpath(*relative) if relative else self.root + source = candidate / "__init__.py" if not relative else candidate.with_suffix(".py") + entry = self.files.get(str(source)) + if entry is not None: + return importlib.util.spec_from_file_location(fullname, source, loader=PinnedPythonLoader(source, entry["sha256"]), submodule_search_locations=[str(self.root)] if not relative else None) + for suffix in importlib.machinery.EXTENSION_SUFFIXES: + extension = Path(str(candidate) + suffix) + entry = self.files.get(str(extension)) + if entry is not None: + if digest(read_bounded(extension, 262144, private=False)) != entry["sha256"]: + raise SafetyError("dependency_extension_changed") + return importlib.util.spec_from_file_location(fullname, extension) + raise SafetyError("unpinned_dependency_module") + + +def pinned_runtime() -> ModuleType: + raw = read_bounded(RUNTIME_PACKET, 65536, private=False) + if digest(raw) != RUNTIME_PACKET_SHA: + raise SafetyError("runtime_packet_changed") + packet = json.loads(raw) + if Path(sys.executable).resolve() != Path(packet["python_realpath"]) or sys.version_info[:3] != (3, 12, 12): + raise SafetyError("interpreter_mismatch") + if digest(read_bounded(Path(packet["python_realpath"]), 1048576, private=False)) != packet["python_executable_sha256"]: + raise SafetyError("interpreter_hash_changed") + if "psutil" in sys.modules: + raise SafetyError("dependency_preloaded") + for name, entry in packet["dependency_files"].items(): + if digest(read_bounded(Path(name), 262144, private=False)) != entry["sha256"]: + raise SafetyError("dependency_hash_changed") + sys.meta_path.insert(0, PinnedPsutilFinder(packet)) + import psutil + if psutil.__version__ != packet["psutil_version"]: + raise SafetyError("dependency_version_changed") + return psutil + + +def sample(psutil: ModuleType, pressure: PressureReader) -> Sensor: + started = time.monotonic_ns() + row = Sensor(started, pressure.pressure(), psutil.virtual_memory().free, psutil.swap_memory().used) + valid_sensor(asdict(row)) + if time.monotonic_ns() - started > 500_000_000: + raise SafetyError("sensor_call_stale") + return row + + +def observe_identity(psutil: ModuleType, pid: int) -> Identity: + process = psutil.Process(pid) + born = process.create_time() + try: + valid = type(born) in (int, float) and math.isfinite(born) and born > 0 + except (OverflowError, ValueError): + valid = False + if not valid: + raise SafetyError("birth_unavailable") + return Identity(pid, process.uids().real, int(born * 1_000_000_000), os.getpgid(pid)) + + +def announce(path: Path, purpose: str) -> None: + global _announcement_bytes + line = f"Create {path}: {purpose}\n" + _announcement_bytes += len(line.encode("utf-8")) + if _announcement_bytes > 65536: + raise SafetyError("announcement_byte_limit") + sys.stdout.write(line) + sys.stdout.flush() + + +def new_log(path: Path, purpose: str) -> int: + owned_path(path.parent, directory=True) + announce(path, purpose) + return os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) + + +def write_all(fd: int, data: bytes) -> None: + while data: + count = os.write(fd, data) + if count <= 0: + raise SafetyError("write_no_progress") + data = data[count:] + + +def write_pipe(fd: int, data: bytes, deadline: float) -> None: + if len(data) > LIMITS["input_bytes"]: + raise SafetyError("pipe_frame_input_limit") + while data: + remaining = deadline - time.monotonic() + if remaining <= 0: + raise SafetyError("pipe_write_deadline") + try: + count = os.write(fd, data) + except BlockingIOError: + select.select([], [fd], [], min(remaining, 0.01)) + continue + if count <= 0: + raise SafetyError("pipe_write_no_progress") + data = data[count:] + + +def group_gone(pgid: int) -> bool: + integer(pgid, minimum=1) + try: + os.killpg(pgid, 0) + except ProcessLookupError: + return True + except OSError: + return False + return False + + +def member_identity_matches(expected: Identity, actual: Identity, group: int, worker_uid: int) -> bool: + try: + for row in (expected, actual): + integer(row.pid, minimum=1) + integer(row.uid) + integer(row.birth_ns, minimum=1) + integer(row.pgid, minimum=1) + integer(group, minimum=1) + integer(worker_uid) + except SafetyError: + return False + return expected == actual and actual.uid == worker_uid and actual.pgid == group + + +def terminate_owned(child: subprocess.Popen, expected: Identity | None, members: list[Identity], psutil: ModuleType, result: dict) -> None: + deadline = time.monotonic() + LIMITS["termination_seconds"] + for sig, interval in ((signal.SIGTERM, 0.25), (signal.SIGKILL, 0.75)): + if time.monotonic() >= deadline: + raise SafetyError("termination_deadline") + if child.poll() is None: + if expected is None: + # Popen still owns an unreaped direct child; its PID cannot be reused. + child.send_signal(sig) + result["termination_authority"] = "unreaped_direct_popen_child" + elif same_owned_identity(expected, observe_identity(psutil, child.pid), os.getuid()): + if result["descendant_containment"] == "unproven": + os.killpg(expected.pgid, sig) + result["termination_authority"] = "fresh_owned_leader_group" + else: + child.send_signal(sig) + result["termination_authority"] = "fresh_owned_direct_child" + else: + raise SafetyError("termination_identity_changed") + if sig == signal.SIGKILL and members and expected is not None: + for member in members: + if time.monotonic() >= deadline: + raise SafetyError("member_termination_deadline") + try: + actual = observe_identity(psutil, member.pid) + except psutil.NoSuchProcess: + continue + if not member_identity_matches(member, actual, expected.pgid, os.getuid()): + raise SafetyError("member_termination_identity_changed") + psutil.Process(member.pid).send_signal(sig) + if not result["reaped"]: + try: + child.wait(timeout=max(0.001, min(interval, deadline - time.monotonic()))) + result["reaped"] = True + except subprocess.TimeoutExpired: + pass + if result["reaped"] and not members: + break + result["exit_code"] = child.returncode + if not result["reaped"]: + raise SafetyError("direct_child_reap_unproven") + if result["descendant_containment"] == "unproven": + if expected is None or len(result["descendants"]) != len(members): + raise SafetyError("member_inventory_unproven") + terminal = True + for member in members: + if time.monotonic() >= deadline: + raise SafetyError("containment_proof_deadline") + try: + actual = observe_identity(psutil, member.pid) + except psutil.NoSuchProcess: + continue + if member_identity_matches(member, actual, expected.pgid, os.getuid()): + terminal = False + result["descendant_containment"] = "proven_terminal" if terminal and group_gone(expected.pgid) else "unproven" + if result["descendant_containment"] != "proven_terminal": + raise SafetyError("owned_group_containment_unproven") + + +def execute_case(manifest: dict, case: dict, psutil: ModuleType, pressure: PressureReader) -> dict: + bootstrap = SOURCE_ROOT / "tests/native/mac-native-bootstrap-v1.py" + policy = MutationPolicy(case) + records: list[Sensor] = [] + result = {"case": case["id"], "implemented": True, "executed": False, "passed": False, + "blocked": None, "unproven": ["installed_package", "installed_host", "models", "physical_hard_cap"], + "proof_scope": PROOF_SCOPE, "import_mode": "isolated_source_module", "synthetic": True, + "source_commit": manifest["source_commit"], "sources": manifest["sources"], + "sensors": [asdict(row) for row in records], "rss_sampled_peak": 0, "limits": LIMITS, + "reaped": False, "exit_code": None, "identity": None, "output_bytes": 0, + "descendants": [], "descendant_containment": "not_observed"} + logs: dict[str, int] = {} + child = None + expected = None + members: list[Identity] = [] + selector = selectors.DefaultSelector() + held_lock = None + frames = bytearray() + protocol_result = None + start = time.monotonic() + try: + records = [] + for index in range(3): + records.append(sample(psutil, pressure)) + if index < 2: + time.sleep(1) + admission(records, time.monotonic_ns()) + if case["source"] is not None: + source = Path(case["source"]) + info = owned_path(source) + raw = read_bounded(source, 4096) + if file_identity(info) != case["source_identity"] or digest(raw) != case["source_sha256"] or len(raw) != case["source_bytes"]: + raise SafetyError("fixture_changed_before_launch") + for name, checksum in manifest["sources"].items(): + if digest(read_bounded(SOURCE_ROOT / name, 262144, private=False)) != checksum: + raise SafetyError("source_hash_changed") + if digest(read_bounded(bootstrap, 262144, private=False)) != manifest["bootstrap_sha256"]: + raise SafetyError("bootstrap_hash_changed") + result["sensors"] = [asdict(row) for row in records] + for channel in ("stdout", "stderr", "events"): + logs[channel] = new_log(RUN_ROOT / "logs" / (case["id"] + "-" + channel + "-v1.log"), "Retained native " + channel + " evidence") + if case["id"] == "flock-a4": + import fcntl + lock = RUN_ROOT / "locks/contention-v1.lock" + owned_path(lock) + held_lock = os.open(lock, os.O_RDWR | os.O_NOFOLLOW) + fcntl.flock(held_lock, fcntl.LOCK_EX | fcntl.LOCK_NB) + env = {"HOME": manifest["home"], "PATH": "/usr/bin:/bin", "LANG": "C.UTF-8", + "OMP_NUM_THREADS": "1", "OPENBLAS_NUM_THREADS": "1", "MKL_NUM_THREADS": "1", + "VECLIB_MAXIMUM_THREADS": "1", "NUMEXPR_NUM_THREADS": "1"} + request = encoded({"version": 1, "source_root": str(SOURCE_ROOT), "run_root": str(RUN_ROOT), + "home": manifest["home"], "sources": manifest["sources"], "limits": LIMITS, "case": case, + "supervisor_sha256": manifest["supervisor_sha256"]}) + b"\n" + if len(request) > LIMITS["input_bytes"] - LIMITS["events"] * 256: + raise SafetyError("input_limit") + fresh = sample(psutil, pressure) + admission([*records[:-1], fresh], time.monotonic_ns()) + result["sensors"].append(asdict(fresh)) + start = time.monotonic() + child = subprocess.Popen([sys.executable, "-I", "-S", "-B", str(bootstrap)], stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, + cwd=str(RUN_ROOT), close_fds=True, start_new_session=True) + result["executed"] = True + expected = observe_identity(psutil, child.pid) + if not same_owned_identity(expected, expected, os.getuid()): + raise SafetyError("child_identity_unowned") + result["identity"] = asdict(expected) + assert child.stdin is not None and child.stdout is not None and child.stderr is not None + for stream, channel in ((child.stdout, "stdout"), (child.stderr, "stderr")): + os.set_blocking(stream.fileno(), False) + selector.register(stream, selectors.EVENT_READ, channel) + os.set_blocking(child.stdin.fileno(), False) + write_pipe(child.stdin.fileno(), request, min(start + LIMITS["wall_seconds"], time.monotonic() + 0.1)) + input_count = len(request) + previous = fresh + ready = False + next_sample = time.monotonic() + while selector.get_map(): + now = time.monotonic() + if now - start >= LIMITS["wall_seconds"]: + raise SafetyError("child_wall_deadline") + if child.poll() is None and now >= next_sample: + current = sample(psutil, pressure) + if current.pressure != 1 or current.free_bytes < FREE_MIN or current.swap_used_bytes > previous.swap_used_bytes or time.monotonic_ns() - current.monotonic_ns > 500_000_000: + raise SafetyError("during_child_sensor_refusal") + actual = observe_identity(psutil, child.pid) + if not same_owned_identity(expected, actual, os.getuid()): + raise SafetyError("child_identity_changed") + process = psutil.Process(child.pid) + descendants = process.children(recursive=True) + if descendants: + result["descendants"] = [item.pid for item in descendants[:32]] + result["descendant_containment"] = "unproven" + if len(descendants) > 32: + raise SafetyError("descendant_inventory_limit") + members = [observe_identity(psutil, item.pid) for item in descendants] + if any(member.uid != os.getuid() or member.pgid != expected.pgid for member in members): + raise SafetyError("descendant_identity_unowned") + raise SafetyError("unexpected_descendant") + rss = integer(process.memory_info().rss) + result["rss_sampled_peak"] = max(result["rss_sampled_peak"], rss) + if rss > LIMITS["rss_abort_bytes"]: + raise SafetyError("sampled_rss_limit") + if len(result["sensors"]) >= 64: + raise SafetyError("sensor_inventory_limit") + result["sensors"].append(asdict(current)) + previous = current + next_sample = now + 0.1 + for key, _ in selector.select(0.01): + try: + chunk = os.read(key.fileobj.fileno(), 4096) + except BlockingIOError: + continue + if not chunk: + selector.unregister(key.fileobj) + continue + result["output_bytes"] += len(chunk) + if result["output_bytes"] > LIMITS["output_bytes"]: + raise SafetyError("combined_output_limit") + channel = key.data + write_all(logs[channel], chunk) + if channel != "stdout": + continue + frames.extend(chunk) + while b"\n" in frames: + line, _, rest = frames.partition(b"\n") + frames = bytearray(rest) + if len(line) > LIMITS["frame_bytes"]: + raise SafetyError("protocol_frame_limit") + frame_deadline = min(start + LIMITS["wall_seconds"], time.monotonic() + 0.1) + frame = json.loads(line) + if type(frame) is not dict: + raise SafetyError("protocol_object_required") + if frame.get("type") == "mutation": + if not ready or protocol_result is not None: + raise SafetyError("mutation_outside_ready_execution") + try: + grant = policy.decide(frame) + mutation_target(grant, frame) + except (SafetyError, OSError, ValueError) as error: + write_all(logs["events"], encoded({"request": frame, "decision": {"type": "deny", "reason": type(error).__name__ + ":" + str(error)[:256]}}) + b"\n") + os.fsync(logs["events"]) + raise + write_all(logs["events"], encoded({"request": frame, "decision": grant}) + b"\n") + os.fsync(logs["events"]) + announce(Path(grant["path"]), grant["purpose"]) + reply = encoded(grant) + b"\n" + input_count += len(reply) + if input_count > LIMITS["input_bytes"]: + raise SafetyError("input_limit") + write_pipe(child.stdin.fileno(), reply, frame_deadline) + elif frame == {"type": "lock_busy"} and ready and protocol_result is None and case["id"] == "flock-a4" and held_lock is not None: + os.close(held_lock) + held_lock = None + reply = b'{"type":"lock_released"}\n' + input_count += len(reply) + if input_count > LIMITS["input_bytes"]: + raise SafetyError("input_limit") + write_pipe(child.stdin.fileno(), reply, frame_deadline) + elif frame.get("type") == "result" and protocol_result is None: + if not ready: + raise SafetyError("result_before_ready") + protocol_result = validate_result(frame, case, manifest["sources"]) + elif frame.get("type") == "ready" and not ready: + validate_ready(frame, child.pid) + ready = True + result["limit_readbacks"] = frame["limits"] + else: + raise SafetyError("unexpected_protocol_frame") + if len(frames) > LIMITS["frame_bytes"]: + raise SafetyError("protocol_frame_limit") + if frames: + raise SafetyError("truncated_protocol_frame") + remaining = max(0.001, start + LIMITS["wall_seconds"] - time.monotonic()) + result["exit_code"] = child.wait(timeout=remaining) + result["reaped"] = True + expected_exit = 73 if case["id"] == "flock-a4" else 0 + if result["exit_code"] != expected_exit or type(protocol_result) is not dict or protocol_result.get("passed") is not True or protocol_result.get("proof_scope") != PROOF_SCOPE: + raise SafetyError("case_nonterminal_or_failed") + if case["id"] == "flock-a4": + lock_fd = os.open(RUN_ROOT / "locks/contention-v1.lock", os.O_RDWR | os.O_NOFOLLOW) + try: + fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + finally: + os.close(lock_fd) + if not group_gone(expected.pgid): + result["descendant_containment"] = "unproven" + raise SafetyError("post_exit_group_containment_unproven") + result["passed"] = True + result["child_result"] = protocol_result + except (SafetyError, OSError, ValueError, subprocess.SubprocessError, psutil.Error) as error: + result["blocked"] = type(error).__name__ + ":" + str(error)[:256] + finally: + if not result["sensors"]: + result["sensors"] = [asdict(row) for row in records] + if child is None: + result["reaped"] = True + if held_lock is not None: + try: + os.close(held_lock) + except OSError as error: + result["blocked"] = "lock_close_failed:" + str(error)[:256] + result["passed"] = False + if child is not None and (not result["reaped"] or result["descendant_containment"] == "unproven"): + try: + terminate_owned(child, expected, members, psutil, result) + except (SafetyError, OSError, ValueError, subprocess.SubprocessError, psutil.Error) as error: + result["blocked"] = "identity_or_reap_unproven:" + str(error)[:256] + result["passed"] = False + result["wall_seconds"] = time.monotonic() - start + try: + selector.close() + except OSError as error: + result["blocked"] = "selector_close_failed:" + str(error)[:256] + result["passed"] = False + if child is not None: + for stream in (child.stdin, child.stdout, child.stderr): + if stream is not None: + try: + stream.close() + except OSError as error: + result["blocked"] = "pipe_close_failed:" + str(error)[:256] + result["passed"] = False + for fd in logs.values(): + try: + os.fsync(fd) + except OSError as error: + result["blocked"] = "evidence_sync_failed:" + str(error)[:256] + result["passed"] = False + finally: + try: + os.close(fd) + except OSError as error: + result["blocked"] = "evidence_close_failed:" + str(error)[:256] + result["passed"] = False + return result + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--manifest", required=True) + parser.add_argument("--execute-root-authorized", action="store_true") + args = parser.parse_args() + if not args.execute_root_authorized or sys.platform != "darwin" or not sys.flags.isolated or not sys.flags.no_site or not sys.flags.dont_write_bytecode: + raise SafetyError("root_isolated_native_execution_required") + sys.dont_write_bytecode = True + os.umask(0o077) + manifest_path = canonical_path(args.manifest, RUN_ROOT) + target, version = evidence_paths(manifest_path) + owned_path(RUN_ROOT, directory=True) + owned_path(RUN_ROOT / "home", directory=True) + manifest_bytes = read_bounded(manifest_path, 65536) + manifest = validate_manifest(json.loads(manifest_bytes)) + if (version == "v1" and any(case["id"] == "capture-revision" for case in manifest["cases"])) or (version == "v2" and [case["id"] for case in manifest["cases"]] != ["capture-revision"]): + raise SafetyError("unexpected_manifest_case_stage") + if digest(read_bounded(Path(__file__).resolve(), 262144, private=False)) != manifest["supervisor_sha256"]: + raise SafetyError("supervisor_hash_changed") + results: list[dict] = [] + setup_failure = None + try: + psutil = pinned_runtime() + pressure = PressureReader() + for case in manifest["cases"]: + result = execute_case(manifest, case, psutil, pressure) + results.append(result) + if not result["passed"] or not result["reaped"]: + break + except (SafetyError, OSError, ValueError, ImportError) as error: + setup_failure = type(error).__name__ + ":" + str(error)[:256] + fd = new_log(target, "Retained Stage A results; native host/model/hard-cap acceptance remains unproven") + try: + data = encoded({"version": 1, "manifest_version": version, "manifest_sha256": digest(manifest_bytes), + "proof_scope": PROOF_SCOPE, "cases": results, "setup_failure": setup_failure, + "complete": setup_failure is None and len(results) == len(manifest["cases"]) and all(item["passed"] for item in results)}) + b"\n" + if len(data) > LIMITS["output_bytes"]: + raise SafetyError("results_byte_limit") + write_all(fd, data) + os.fsync(fd) + finally: + os.close(fd) + return 0 if setup_failure is None and len(results) == len(manifest["cases"]) and all(item["passed"] for item in results) else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/native/mac-native-acceptance-v2.py b/tests/native/mac-native-acceptance-v2.py new file mode 100644 index 00000000..005168f8 --- /dev/null +++ b/tests/native/mac-native-acceptance-v2.py @@ -0,0 +1,885 @@ +"""ROOT-run Stage A supervisor. Importing this module never starts native work. + +This instrumented source-module harness proves tiny filesystem/lock behavior. +It does not certify installed packages, host dispatch, models or a physical cap. +""" +from __future__ import annotations + +import argparse +import hashlib +import importlib.abc +import importlib.machinery +import importlib.util +import json +import math +import os +import re +import select +import selectors +import signal +import stat +import subprocess +import sys +import time +from dataclasses import asdict, dataclass +from pathlib import Path +from types import ModuleType + +RUN_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/native-mac-v1/run-0001") +SOURCE_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/worktrees/mac-native-acceptance-v1") +RUNTIME_PACKET = Path("/Users/j/dev/truememory-maintenance-v1/.work/reports/mac-native-runtime-pins-v1.json") +RUNTIME_PACKET_SHA = "8db57e3f700aebb3adeb383c719a4bc5e2525917f4c017c853ee4694be1ee1e4" +PROOF_SCOPE = "instrumented_unmodified_source_native_filesystem_lock_capture" +SOURCE_FILES = ( + "truememory/_platform.py", "truememory/ingest/hooks/_shared.py", + "truememory/ingest/completion.py", "truememory/ingest/transcript.py", + "truememory/ingest/provenance.py", +) +CASES = frozenset({"capture-a1", "capture-a2", "capture-revision", "negative-a3", "invalid-state", "flock-a4", "parser-a5"}) +LIMITS = {"wall_seconds": 5, "termination_seconds": 1, "cpu_soft": 2, "cpu_hard": 3, + "rss_abort_bytes": 67108864, "core_bytes": 0, "nofile": 64, + "fsize_bytes": 1048576, "input_bytes": 65536, "output_bytes": 262144, + "events": 32, "frame_bytes": 2048, "ack_ms": 100, "threads": 1} +FREE_MIN = 268435456 +MAX_INT = (1 << 63) - 1 +_SHA = re.compile(r"[0-9a-f]{64}\Z") +_PENDING = re.compile(r"state-[0-9a-f]{32}\.pending\Z") +_ATTEMPT = re.compile(r"v(?:[1-9]|[1-9][0-9])\Z") +_announcement_bytes = 0 + + +class SafetyError(ValueError): + pass + + +def integer(value: object, *, minimum: int = 0, maximum: int = MAX_INT) -> int: + if type(value) is not int or not minimum <= value <= maximum: + raise SafetyError("invalid_integer") + return value + + +def exact_keys(value: object, keys: set[str]) -> dict: + if type(value) is not dict or set(value) != keys: + raise SafetyError("invalid_fields") + return value + + +def encoded(value: object) -> bytes: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8") + + +def digest(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def hash_text(value: object) -> str: + if type(value) is not str or _SHA.fullmatch(value) is None: + raise SafetyError("invalid_hash") + return value + + +def canonical_path(value: object, root: Path) -> Path: + if type(value) is not str or not value or "\0" in value or len(value.encode("utf-8")) > 1024: + raise SafetyError("invalid_path") + path = Path(value) + if not path.is_absolute() or ".." in path.parts or str(path) != value: + raise SafetyError("noncanonical_path") + if path != root and not path.is_relative_to(root): + raise SafetyError("path_escape") + return path + + +def owned_path(path: Path, *, directory: bool = False) -> os.stat_result: + if path.resolve() != path or any(item.is_symlink() for item in (path, *path.parents)): + raise SafetyError("path_link") + info = path.lstat() + if info.st_uid != os.getuid() or info.st_mode & 0o077: + raise SafetyError("path_not_private_owned") + if directory: + if not stat.S_ISDIR(info.st_mode): + raise SafetyError("path_not_directory") + elif not stat.S_ISREG(info.st_mode) or info.st_nlink != 1: + raise SafetyError("path_not_regular_single_link") + return info + + +def read_bounded(path: Path, maximum: int, *, private: bool = True) -> bytes: + if private: + owned_path(path) + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + try: + before = os.fstat(fd) + if not stat.S_ISREG(before.st_mode) or before.st_size > maximum: + raise SafetyError("read_limit_or_type") + if private and (before.st_uid != os.getuid() or before.st_mode & 0o077 or before.st_nlink != 1): + raise SafetyError("read_not_private") + chunks: list[bytes] = [] + remaining = maximum + 1 + while remaining: + chunk = os.read(fd, min(remaining, 65536)) + if not chunk: + break + chunks.append(chunk) + remaining -= len(chunk) + after = os.fstat(fd) + data = b"".join(chunks) + if file_identity(before) != file_identity(after) or len(data) > maximum or len(data) != after.st_size: + raise SafetyError("read_changed_or_limit") + return data + finally: + os.close(fd) + + +def file_identity(info: os.stat_result) -> list[int]: + return [info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns, info.st_ctime_ns] + + +@dataclass(frozen=True) +class Sensor: + monotonic_ns: int + pressure: int + free_bytes: int + swap_used_bytes: int + + +def valid_sensor(value: object) -> Sensor: + row = exact_keys(value, {"monotonic_ns", "pressure", "free_bytes", "swap_used_bytes"}) + integer(row["monotonic_ns"], minimum=1) + integer(row["pressure"], minimum=1, maximum=4) + integer(row["free_bytes"]) + integer(row["swap_used_bytes"]) + if row["pressure"] not in (1, 2, 4): + raise SafetyError("unknown_pressure_abi") + return Sensor(**row) + + +def admission(samples: list[Sensor], now_ns: int) -> None: + integer(now_ns, minimum=1) + if len(samples) < 3: + raise SafetyError("sensor_count") + previous = None + for sample in samples: + valid_sensor(asdict(sample)) + if sample.pressure != 1 or sample.free_bytes < FREE_MIN: + raise SafetyError("pressure_or_headroom") + if previous is not None and (sample.monotonic_ns <= previous.monotonic_ns or sample.swap_used_bytes > previous.swap_used_bytes): + raise SafetyError("unordered_or_growing_swap") + previous = sample + if samples[-1].monotonic_ns - samples[0].monotonic_ns < 2_000_000_000: + raise SafetyError("sensor_window") + if not 0 <= now_ns - samples[-1].monotonic_ns <= 500_000_000: + raise SafetyError("sensor_stale") + + +@dataclass(frozen=True) +class Identity: + pid: int + uid: int + birth_ns: int + pgid: int + + +def same_owned_identity(expected: Identity, actual: Identity, worker_uid: int) -> bool: + try: + for row in (expected, actual): + integer(row.pid, minimum=1) + integer(row.uid) + integer(row.birth_ns, minimum=1) + integer(row.pgid, minimum=1) + integer(worker_uid) + except SafetyError: + return False + return expected == actual and actual.uid == worker_uid and actual.pgid == actual.pid + + +def account_directory(case: dict) -> Path: + key = digest(encoded(["native-acceptance-v1", case["database"]])) + return RUN_ROOT / "completion" / key + + +def capture_metadata(case: dict) -> dict: + return {"version": 1, "user_id": "native-acceptance-v1", "database": case["database"], + "host": "claude", "session_id": case["session"], "source": case["source"], + "source_suffix": ".jsonl", "file_identity": case["source_identity"], + "byte_count": case["source_bytes"], "sha256": case["source_sha256"]} + + +def validate_case(value: object) -> dict: + row = exact_keys(value, {"id", "source", "source_sha256", "source_bytes", "source_identity", "session", "database", "previous"}) + if type(row["id"]) is not str or row["id"] not in CASES: + raise SafetyError("unsupported_case") + base = RUN_ROOT / "home/.claude/projects/native-acceptance-v1" + filename = {"capture-revision": "capture-a2", "invalid-state": "negative-a3"}.get(row["id"], row["id"]) + if row["id"] != "flock-a4": + if canonical_path(row["source"], RUN_ROOT) != base / (filename + ".jsonl"): + raise SafetyError("unexpected_fixture") + hash_text(row["source_sha256"]) + integer(row["source_bytes"], minimum=1, maximum=4096) + if type(row["source_identity"]) is not list or len(row["source_identity"]) != 5: + raise SafetyError("fixture_identity") + for number in row["source_identity"]: + integer(number) + if row["source_identity"][2] != row["source_bytes"]: + raise SafetyError("fixture_size_identity") + elif any(row[name] is not None for name in ("source", "source_sha256", "source_bytes", "source_identity")): + raise SafetyError("unexpected_lock_fixture") + if row["session"] != "synthetic-native-" + filename: + raise SafetyError("unexpected_session") + database_case = "capture-a2" if row["id"] == "capture-revision" else "negative-a3" if row["id"] == "invalid-state" else row["id"] + if row["database"] != str(RUN_ROOT / (database_case + "-v1.db")): + raise SafetyError("unexpected_database_locator") + if row["id"] == "capture-revision": + old = exact_keys(row["previous"], {"key", "sha256", "byte_count"}) + hash_text(old["key"]) + hash_text(old["sha256"]) + integer(old["byte_count"], minimum=1, maximum=4096) + if old["key"] == digest(encoded(capture_metadata(row))): + raise SafetyError("revision_not_new_evidence") + elif row["previous"] is not None: + raise SafetyError("unexpected_previous") + return row + + +def validate_manifest(value: object) -> dict: + row = exact_keys(value, {"version", "stage", "synthetic", "root_execution_go", "run_root", "home", "source_root", "source_commit", "sources", "supervisor_sha256", "bootstrap_sha256", "limits", "cases"}) + if type(row["version"]) is not int or row["version"] != 1 or row["stage"] != "A" or row["synthetic"] is not True or row["root_execution_go"] is not True: + raise SafetyError("manifest_authority_or_version") + if row["run_root"] != str(RUN_ROOT) or row["home"] != str(RUN_ROOT / "home") or row["source_root"] != str(SOURCE_ROOT): + raise SafetyError("unexpected_root") + if type(row["source_commit"]) is not str or re.fullmatch(r"[0-9a-f]{40}", row["source_commit"]) is None: + raise SafetyError("source_commit") + sources = exact_keys(row["sources"], set(SOURCE_FILES)) + for value in sources.values(): + hash_text(value) + hash_text(row["supervisor_sha256"]) + hash_text(row["bootstrap_sha256"]) + if type(row["limits"]) is not dict or row["limits"] != LIMITS or any(type(v) is not int for v in row["limits"].values()): + raise SafetyError("limit_change") + if type(row["cases"]) is not list or not 1 <= len(row["cases"]) <= len(CASES): + raise SafetyError("case_count") + cases = [validate_case(case) for case in row["cases"]] + if len({case["id"] for case in cases}) != len(cases): + raise SafetyError("duplicate_case") + return row + + +def validate_ready(value: object, pid: int) -> dict: + row = exact_keys(value, {"type", "version", "pid", "limits", "audit_installed"}) + integer(pid, minimum=1) + if row["type"] != "ready" or type(row["version"]) is not int or row["version"] != 1 or type(row["pid"]) is not int or row["pid"] != pid or row["audit_installed"] is not True: + raise SafetyError("limits_ready_mismatch") + expected = {"cpu": [2, 3], "core": [0, 0], "nofile": [64, 64], "fsize": [1048576, 1048576]} + limits = exact_keys(row["limits"], set(expected)) + for name, pair in limits.items(): + if type(pair) is not list or len(pair) != 2 or any(type(number) is not int for number in pair) or pair != expected[name]: + raise SafetyError("limits_ready_mismatch") + return row + + +def validate_result(value: object, case: dict, sources: dict) -> dict: + row = exact_keys(value, {"type", "version", "case", "passed", "proof_scope", "import_mode", "sources", "details"}) + if row["type"] != "result" or type(row["version"]) is not int or row["version"] != 1 or row["case"] != case["id"] or row["passed"] is not True or row["proof_scope"] != PROOF_SCOPE or row["import_mode"] != "isolated_source_module" or row["sources"] != sources or type(row["details"]) is not dict: + raise SafetyError("result_scope_mismatch") + return row + + +def validate_attempt_version(value: object) -> str: + if type(value) is not str or _ATTEMPT.fullmatch(value) is None: + raise SafetyError("invalid_attempt_version") + return value + + +def evidence_paths(manifest_path: Path) -> tuple[Path, str]: + name = manifest_path.name + if manifest_path.parent != RUN_ROOT or not name.startswith("manifest-") or not name.endswith(".json"): + raise SafetyError("unexpected_manifest_path") + version = validate_attempt_version(name[len("manifest-"):-len(".json")]) + return RUN_ROOT / ("results-" + version + ".json"), version + + +class MutationPolicy: + def __init__(self, case: dict) -> None: + self.case = case + self.account = account_directory(case) + self.directory_paths = {self.account, self.account.parent, *(self.account / name for name in ("captures", "receipts", "logs"))} + self.fixed = {self.account / "state.lock": "Completion account lock", self.account / "state.json": "Atomic completion state"} + if case["id"] not in {"flock-a4", "parser-a5"}: + self.fixed[self.account / "captures" / (digest(encoded(capture_metadata(case))) + ".jsonl")] = "Immutable synthetic source capture" + if case["id"] in {"negative-a3", "parser-a5"}: + self.directory_paths = set() + self.fixed = {} + elif case["id"] == "invalid-state": + self.fixed = {self.account / "state.lock": "Completion account lock"} + elif case["id"] == "flock-a4": + self.directory_paths = set() + self.fixed = {RUN_ROOT / "locks/contention-v1.lock": "Native lock contention witness"} + self.pending: set[Path] = set() + self.sequence = 0 + + def decide(self, value: object) -> dict: + row = exact_keys(value, {"type", "version", "seq", "event", "path", "source", "flags"}) + if row["type"] != "mutation" or type(row["version"]) is not int or row["version"] != 1: + raise SafetyError("mutation_shape") + seq = integer(row["seq"], minimum=1, maximum=LIMITS["events"]) + if seq != self.sequence + 1: + raise SafetyError("mutation_sequence") + path = canonical_path(row["path"], RUN_ROOT) + flags = integer(row["flags"], maximum=(1 << 31) - 1) + purpose = self.fixed.get(path) + if row["event"] == "os.mkdir": + if row["source"] is not None or path not in self.directory_paths or flags != 0o700: + raise SafetyError("unplanned_directory") + purpose = "Private completion directory" + elif row["event"] == "open": + if row["source"] is not None or flags & os.O_TRUNC: + raise SafetyError("unplanned_open") + access = flags & os.O_ACCMODE + extras = flags & ~(os.O_ACCMODE | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW | getattr(os, "O_CLOEXEC", 0)) + if extras or access not in (os.O_WRONLY, os.O_RDWR) or not flags & os.O_CREAT: + raise SafetyError("unplanned_open_flags") + if path.parent == self.account and _PENDING.fullmatch(path.name): + if self.case["id"] not in {"capture-a1", "capture-a2", "capture-revision"} or access != os.O_WRONLY or not flags & os.O_EXCL or len(self.pending | {path}) > 1: + raise SafetyError("pending_slot_limit") + self.pending.add(path) + purpose = "Atomic completion-state temporary; failed evidence retained" + elif path not in self.fixed or path.name == "state.json": + raise SafetyError("unplanned_file") + elif path.name == "state.lock" or path == RUN_ROOT / "locks/contention-v1.lock": + if access != os.O_RDWR or flags & os.O_EXCL: + raise SafetyError("lock_flags") + elif access != os.O_WRONLY or not flags & os.O_EXCL: + raise SafetyError("capture_flags") + elif row["event"] == "os.rename": + source = canonical_path(row["source"], RUN_ROOT) + if flags != 0 or path != self.account / "state.json" or source not in self.pending: + raise SafetyError("unplanned_replace") + purpose = "Publish current-run completion state from its approved temporary" + else: + raise SafetyError("unsupported_mutation") + self.sequence = seq + return {"type": "grant", "version": 1, "seq": seq, "path": str(path), "purpose": purpose} + + +def mutation_target(grant: dict, request: dict) -> None: + path = Path(grant["path"]) + owned_path(path.parent, directory=True) + if path.exists() or path.is_symlink(): + owned_path(path, directory=request["event"] == "os.mkdir") + if request["event"] == "os.rename": + owned_path(Path(request["source"])) + + +class PressureReader: + def __init__(self) -> None: + import ctypes + self.ctypes = ctypes + self.library = ctypes.CDLL("/usr/lib/libSystem.B.dylib", use_errno=True) + self.read = self.library.sysctlbyname + self.read.argtypes = [ctypes.c_char_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.c_void_p, ctypes.c_size_t] + self.read.restype = ctypes.c_int + + def pressure(self) -> int: + c = self.ctypes + output = c.c_uint32() + size = c.c_size_t(c.sizeof(output)) + if self.read(b"kern.memorystatus_vm_pressure_level", c.byref(output), c.byref(size), None, 0) != 0 or size.value != 4: + raise SafetyError("pressure_unavailable") + if output.value not in (1, 2, 4): + raise SafetyError("pressure_abi_unknown") + return output.value + + +class PinnedPythonLoader(importlib.abc.Loader): + def __init__(self, path: Path, checksum: str) -> None: + self.path, self.checksum = path, checksum + + def create_module(self, spec: object) -> None: + return None + + def exec_module(self, module: ModuleType) -> None: + data = read_bounded(self.path, 262144, private=False) + if digest(data) != self.checksum: + raise SafetyError("dependency_hash_changed") + exec(compile(data, str(self.path), "exec", dont_inherit=True), module.__dict__) + + +class PinnedPsutilFinder(importlib.abc.MetaPathFinder): + def __init__(self, packet: dict) -> None: + self.root = Path(packet["dependency_root"]) + self.files = packet["dependency_files"] + + def find_spec(self, fullname: str, path: object = None, target: object = None) -> object: + if fullname != "psutil" and not fullname.startswith("psutil."): + return None + relative = fullname.split(".")[1:] + candidate = self.root.joinpath(*relative) if relative else self.root + source = candidate / "__init__.py" if not relative else candidate.with_suffix(".py") + entry = self.files.get(str(source)) + if entry is not None: + return importlib.util.spec_from_file_location(fullname, source, loader=PinnedPythonLoader(source, entry["sha256"]), submodule_search_locations=[str(self.root)] if not relative else None) + for suffix in importlib.machinery.EXTENSION_SUFFIXES: + extension = Path(str(candidate) + suffix) + entry = self.files.get(str(extension)) + if entry is not None: + if digest(read_bounded(extension, 262144, private=False)) != entry["sha256"]: + raise SafetyError("dependency_extension_changed") + return importlib.util.spec_from_file_location(fullname, extension) + raise SafetyError("unpinned_dependency_module") + + +def pinned_runtime() -> ModuleType: + raw = read_bounded(RUNTIME_PACKET, 65536, private=False) + if digest(raw) != RUNTIME_PACKET_SHA: + raise SafetyError("runtime_packet_changed") + packet = json.loads(raw) + if Path(sys.executable).resolve() != Path(packet["python_realpath"]) or sys.version_info[:3] != (3, 12, 12): + raise SafetyError("interpreter_mismatch") + if digest(read_bounded(Path(packet["python_realpath"]), 1048576, private=False)) != packet["python_executable_sha256"]: + raise SafetyError("interpreter_hash_changed") + if "psutil" in sys.modules: + raise SafetyError("dependency_preloaded") + for name, entry in packet["dependency_files"].items(): + if digest(read_bounded(Path(name), 262144, private=False)) != entry["sha256"]: + raise SafetyError("dependency_hash_changed") + sys.meta_path.insert(0, PinnedPsutilFinder(packet)) + import psutil + if psutil.__version__ != packet["psutil_version"]: + raise SafetyError("dependency_version_changed") + return psutil + + +def sample(psutil: ModuleType, pressure: PressureReader) -> Sensor: + started = time.monotonic_ns() + row = Sensor(started, pressure.pressure(), psutil.virtual_memory().free, psutil.swap_memory().used) + valid_sensor(asdict(row)) + if time.monotonic_ns() - started > 500_000_000: + raise SafetyError("sensor_call_stale") + return row + + +def observe_identity(psutil: ModuleType, pid: int) -> Identity: + process = psutil.Process(pid) + born = process.create_time() + try: + valid = type(born) in (int, float) and math.isfinite(born) and born > 0 + except (OverflowError, ValueError): + valid = False + if not valid: + raise SafetyError("birth_unavailable") + return Identity(pid, process.uids().real, int(born * 1_000_000_000), os.getpgid(pid)) + + +def announce(path: Path, purpose: str) -> None: + global _announcement_bytes + line = f"Create {path}: {purpose}\n" + _announcement_bytes += len(line.encode("utf-8")) + if _announcement_bytes > 65536: + raise SafetyError("announcement_byte_limit") + sys.stdout.write(line) + sys.stdout.flush() + + +def new_log(path: Path, purpose: str) -> int: + owned_path(path.parent, directory=True) + announce(path, purpose) + return os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) + + +def write_all(fd: int, data: bytes) -> None: + while data: + count = os.write(fd, data) + if count <= 0: + raise SafetyError("write_no_progress") + data = data[count:] + + +def write_pipe(fd: int, data: bytes, deadline: float) -> None: + if len(data) > LIMITS["input_bytes"]: + raise SafetyError("pipe_frame_input_limit") + while data: + remaining = deadline - time.monotonic() + if remaining <= 0: + raise SafetyError("pipe_write_deadline") + try: + count = os.write(fd, data) + except BlockingIOError: + select.select([], [fd], [], min(remaining, 0.01)) + continue + if count <= 0: + raise SafetyError("pipe_write_no_progress") + data = data[count:] + + +def group_gone(pgid: int) -> bool: + integer(pgid, minimum=1) + try: + os.killpg(pgid, 0) + except ProcessLookupError: + return True + except OSError: + return False + return False + + +def member_identity_matches(expected: Identity, actual: Identity, group: int, worker_uid: int) -> bool: + try: + for row in (expected, actual): + integer(row.pid, minimum=1) + integer(row.uid) + integer(row.birth_ns, minimum=1) + integer(row.pgid, minimum=1) + integer(group, minimum=1) + integer(worker_uid) + except SafetyError: + return False + return expected == actual and actual.uid == worker_uid and actual.pgid == group + + +def terminate_owned(child: subprocess.Popen, expected: Identity | None, members: list[Identity], psutil: ModuleType, result: dict) -> None: + deadline = time.monotonic() + LIMITS["termination_seconds"] + for sig, interval in ((signal.SIGTERM, 0.25), (signal.SIGKILL, 0.75)): + if time.monotonic() >= deadline: + raise SafetyError("termination_deadline") + if child.poll() is None: + if expected is None: + # Popen still owns an unreaped direct child; its PID cannot be reused. + child.send_signal(sig) + result["termination_authority"] = "unreaped_direct_popen_child" + elif same_owned_identity(expected, observe_identity(psutil, child.pid), os.getuid()): + if result["descendant_containment"] == "unproven": + os.killpg(expected.pgid, sig) + result["termination_authority"] = "fresh_owned_leader_group" + else: + child.send_signal(sig) + result["termination_authority"] = "fresh_owned_direct_child" + else: + raise SafetyError("termination_identity_changed") + if sig == signal.SIGKILL and members and expected is not None: + for member in members: + if time.monotonic() >= deadline: + raise SafetyError("member_termination_deadline") + try: + actual = observe_identity(psutil, member.pid) + except psutil.NoSuchProcess: + continue + if not member_identity_matches(member, actual, expected.pgid, os.getuid()): + raise SafetyError("member_termination_identity_changed") + psutil.Process(member.pid).send_signal(sig) + if not result["reaped"]: + try: + child.wait(timeout=max(0.001, min(interval, deadline - time.monotonic()))) + result["reaped"] = True + except subprocess.TimeoutExpired: + pass + if result["reaped"] and not members: + break + result["exit_code"] = child.returncode + if not result["reaped"]: + raise SafetyError("direct_child_reap_unproven") + if result["descendant_containment"] == "unproven": + if expected is None or len(result["descendants"]) != len(members): + raise SafetyError("member_inventory_unproven") + terminal = True + for member in members: + if time.monotonic() >= deadline: + raise SafetyError("containment_proof_deadline") + try: + actual = observe_identity(psutil, member.pid) + except psutil.NoSuchProcess: + continue + if member_identity_matches(member, actual, expected.pgid, os.getuid()): + terminal = False + result["descendant_containment"] = "proven_terminal" if terminal and group_gone(expected.pgid) else "unproven" + if result["descendant_containment"] != "proven_terminal": + raise SafetyError("owned_group_containment_unproven") + + +def execute_case(manifest: dict, case: dict, psutil: ModuleType, pressure: PressureReader, attempt_version: str = "v1") -> dict: + attempt_version = validate_attempt_version(attempt_version) + bootstrap = SOURCE_ROOT / "tests/native/mac-native-bootstrap-v2.py" + policy = MutationPolicy(case) + records: list[Sensor] = [] + result = {"case": case["id"], "implemented": True, "executed": False, "passed": False, + "blocked": None, "unproven": ["installed_package", "installed_host", "models", "physical_hard_cap"], + "proof_scope": PROOF_SCOPE, "import_mode": "isolated_source_module", "synthetic": True, + "source_commit": manifest["source_commit"], "sources": manifest["sources"], + "sensors": [asdict(row) for row in records], "rss_sampled_peak": 0, "limits": LIMITS, + "reaped": False, "exit_code": None, "identity": None, "output_bytes": 0, + "descendants": [], "descendant_containment": "not_observed"} + logs: dict[str, int] = {} + child = None + expected = None + members: list[Identity] = [] + selector = selectors.DefaultSelector() + held_lock = None + frames = bytearray() + protocol_result = None + start = time.monotonic() + try: + records = [] + for index in range(3): + records.append(sample(psutil, pressure)) + if index < 2: + time.sleep(1) + admission(records, time.monotonic_ns()) + if case["source"] is not None: + source = Path(case["source"]) + info = owned_path(source) + raw = read_bounded(source, 4096) + if file_identity(info) != case["source_identity"] or digest(raw) != case["source_sha256"] or len(raw) != case["source_bytes"]: + raise SafetyError("fixture_changed_before_launch") + for name, checksum in manifest["sources"].items(): + if digest(read_bounded(SOURCE_ROOT / name, 262144, private=False)) != checksum: + raise SafetyError("source_hash_changed") + if digest(read_bounded(bootstrap, 262144, private=False)) != manifest["bootstrap_sha256"]: + raise SafetyError("bootstrap_hash_changed") + result["sensors"] = [asdict(row) for row in records] + for channel in ("stdout", "stderr", "events"): + logs[channel] = new_log(RUN_ROOT / "logs" / (case["id"] + "-" + channel + "-" + attempt_version + ".log"), "Retained native " + channel + " evidence") + if case["id"] == "flock-a4": + import fcntl + lock = RUN_ROOT / "locks/contention-v1.lock" + owned_path(lock) + held_lock = os.open(lock, os.O_RDWR | os.O_NOFOLLOW) + fcntl.flock(held_lock, fcntl.LOCK_EX | fcntl.LOCK_NB) + env = {"HOME": manifest["home"], "PATH": "/usr/bin:/bin", "LANG": "C.UTF-8", + "OMP_NUM_THREADS": "1", "OPENBLAS_NUM_THREADS": "1", "MKL_NUM_THREADS": "1", + "VECLIB_MAXIMUM_THREADS": "1", "NUMEXPR_NUM_THREADS": "1"} + request = encoded({"version": 1, "source_root": str(SOURCE_ROOT), "run_root": str(RUN_ROOT), + "home": manifest["home"], "sources": manifest["sources"], "limits": LIMITS, "case": case, + "supervisor_sha256": manifest["supervisor_sha256"]}) + b"\n" + if len(request) > LIMITS["input_bytes"] - LIMITS["events"] * 256: + raise SafetyError("input_limit") + fresh = sample(psutil, pressure) + admission([*records[:-1], fresh], time.monotonic_ns()) + result["sensors"].append(asdict(fresh)) + start = time.monotonic() + child = subprocess.Popen([sys.executable, "-I", "-S", "-B", str(bootstrap)], stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, + cwd=str(RUN_ROOT), close_fds=True, start_new_session=True) + result["executed"] = True + expected = observe_identity(psutil, child.pid) + if not same_owned_identity(expected, expected, os.getuid()): + raise SafetyError("child_identity_unowned") + result["identity"] = asdict(expected) + assert child.stdin is not None and child.stdout is not None and child.stderr is not None + for stream, channel in ((child.stdout, "stdout"), (child.stderr, "stderr")): + os.set_blocking(stream.fileno(), False) + selector.register(stream, selectors.EVENT_READ, channel) + os.set_blocking(child.stdin.fileno(), False) + write_pipe(child.stdin.fileno(), request, min(start + LIMITS["wall_seconds"], time.monotonic() + 0.1)) + input_count = len(request) + previous = fresh + ready = False + next_sample = time.monotonic() + while selector.get_map(): + now = time.monotonic() + if now - start >= LIMITS["wall_seconds"]: + raise SafetyError("child_wall_deadline") + if child.poll() is None and now >= next_sample: + current = sample(psutil, pressure) + if current.pressure != 1 or current.free_bytes < FREE_MIN or current.swap_used_bytes > previous.swap_used_bytes or time.monotonic_ns() - current.monotonic_ns > 500_000_000: + raise SafetyError("during_child_sensor_refusal") + actual = observe_identity(psutil, child.pid) + if not same_owned_identity(expected, actual, os.getuid()): + raise SafetyError("child_identity_changed") + process = psutil.Process(child.pid) + descendants = process.children(recursive=True) + if descendants: + result["descendants"] = [item.pid for item in descendants[:32]] + result["descendant_containment"] = "unproven" + if len(descendants) > 32: + raise SafetyError("descendant_inventory_limit") + members = [observe_identity(psutil, item.pid) for item in descendants] + if any(member.uid != os.getuid() or member.pgid != expected.pgid for member in members): + raise SafetyError("descendant_identity_unowned") + raise SafetyError("unexpected_descendant") + rss = integer(process.memory_info().rss) + result["rss_sampled_peak"] = max(result["rss_sampled_peak"], rss) + if rss > LIMITS["rss_abort_bytes"]: + raise SafetyError("sampled_rss_limit") + if len(result["sensors"]) >= 64: + raise SafetyError("sensor_inventory_limit") + result["sensors"].append(asdict(current)) + previous = current + next_sample = now + 0.1 + for key, _ in selector.select(0.01): + try: + chunk = os.read(key.fileobj.fileno(), 4096) + except BlockingIOError: + continue + if not chunk: + selector.unregister(key.fileobj) + continue + result["output_bytes"] += len(chunk) + if result["output_bytes"] > LIMITS["output_bytes"]: + raise SafetyError("combined_output_limit") + channel = key.data + write_all(logs[channel], chunk) + if channel != "stdout": + continue + frames.extend(chunk) + while b"\n" in frames: + line, _, rest = frames.partition(b"\n") + frames = bytearray(rest) + if len(line) > LIMITS["frame_bytes"]: + raise SafetyError("protocol_frame_limit") + frame_deadline = min(start + LIMITS["wall_seconds"], time.monotonic() + 0.1) + frame = json.loads(line) + if type(frame) is not dict: + raise SafetyError("protocol_object_required") + if frame.get("type") == "mutation": + if not ready or protocol_result is not None: + raise SafetyError("mutation_outside_ready_execution") + try: + grant = policy.decide(frame) + mutation_target(grant, frame) + except (SafetyError, OSError, ValueError) as error: + write_all(logs["events"], encoded({"request": frame, "decision": {"type": "deny", "reason": type(error).__name__ + ":" + str(error)[:256]}}) + b"\n") + os.fsync(logs["events"]) + raise + write_all(logs["events"], encoded({"request": frame, "decision": grant}) + b"\n") + os.fsync(logs["events"]) + announce(Path(grant["path"]), grant["purpose"]) + reply = encoded(grant) + b"\n" + input_count += len(reply) + if input_count > LIMITS["input_bytes"]: + raise SafetyError("input_limit") + write_pipe(child.stdin.fileno(), reply, frame_deadline) + elif frame == {"type": "lock_busy"} and ready and protocol_result is None and case["id"] == "flock-a4" and held_lock is not None: + os.close(held_lock) + held_lock = None + reply = b'{"type":"lock_released"}\n' + input_count += len(reply) + if input_count > LIMITS["input_bytes"]: + raise SafetyError("input_limit") + write_pipe(child.stdin.fileno(), reply, frame_deadline) + elif frame.get("type") == "result" and protocol_result is None: + if not ready: + raise SafetyError("result_before_ready") + protocol_result = validate_result(frame, case, manifest["sources"]) + elif frame.get("type") == "ready" and not ready: + validate_ready(frame, child.pid) + ready = True + result["limit_readbacks"] = frame["limits"] + else: + raise SafetyError("unexpected_protocol_frame") + if len(frames) > LIMITS["frame_bytes"]: + raise SafetyError("protocol_frame_limit") + if frames: + raise SafetyError("truncated_protocol_frame") + remaining = max(0.001, start + LIMITS["wall_seconds"] - time.monotonic()) + result["exit_code"] = child.wait(timeout=remaining) + result["reaped"] = True + expected_exit = 73 if case["id"] == "flock-a4" else 0 + if result["exit_code"] != expected_exit or type(protocol_result) is not dict or protocol_result.get("passed") is not True or protocol_result.get("proof_scope") != PROOF_SCOPE: + raise SafetyError("case_nonterminal_or_failed") + if case["id"] == "flock-a4": + lock_fd = os.open(RUN_ROOT / "locks/contention-v1.lock", os.O_RDWR | os.O_NOFOLLOW) + try: + fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + finally: + os.close(lock_fd) + if not group_gone(expected.pgid): + result["descendant_containment"] = "unproven" + raise SafetyError("post_exit_group_containment_unproven") + result["passed"] = True + result["child_result"] = protocol_result + except (SafetyError, OSError, ValueError, subprocess.SubprocessError, psutil.Error) as error: + result["blocked"] = type(error).__name__ + ":" + str(error)[:256] + finally: + if not result["sensors"]: + result["sensors"] = [asdict(row) for row in records] + if child is None: + result["reaped"] = True + if held_lock is not None: + try: + os.close(held_lock) + except OSError as error: + result["blocked"] = "lock_close_failed:" + str(error)[:256] + result["passed"] = False + if child is not None and (not result["reaped"] or result["descendant_containment"] == "unproven"): + try: + terminate_owned(child, expected, members, psutil, result) + except (SafetyError, OSError, ValueError, subprocess.SubprocessError, psutil.Error) as error: + result["blocked"] = "identity_or_reap_unproven:" + str(error)[:256] + result["passed"] = False + result["wall_seconds"] = time.monotonic() - start + try: + selector.close() + except OSError as error: + result["blocked"] = "selector_close_failed:" + str(error)[:256] + result["passed"] = False + if child is not None: + for stream in (child.stdin, child.stdout, child.stderr): + if stream is not None: + try: + stream.close() + except OSError as error: + result["blocked"] = "pipe_close_failed:" + str(error)[:256] + result["passed"] = False + for fd in logs.values(): + try: + os.fsync(fd) + except OSError as error: + result["blocked"] = "evidence_sync_failed:" + str(error)[:256] + result["passed"] = False + finally: + try: + os.close(fd) + except OSError as error: + result["blocked"] = "evidence_close_failed:" + str(error)[:256] + result["passed"] = False + return result + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--manifest", required=True) + parser.add_argument("--execute-root-authorized", action="store_true") + args = parser.parse_args() + if not args.execute_root_authorized or sys.platform != "darwin" or not sys.flags.isolated or not sys.flags.no_site or not sys.flags.dont_write_bytecode: + raise SafetyError("root_isolated_native_execution_required") + sys.dont_write_bytecode = True + os.umask(0o077) + manifest_path = canonical_path(args.manifest, RUN_ROOT) + target, version = evidence_paths(manifest_path) + owned_path(RUN_ROOT, directory=True) + owned_path(RUN_ROOT / "home", directory=True) + manifest_bytes = read_bounded(manifest_path, 65536) + manifest = validate_manifest(json.loads(manifest_bytes)) + if (version != "v2" and any(case["id"] == "capture-revision" for case in manifest["cases"])) or (version == "v2" and [case["id"] for case in manifest["cases"]] != ["capture-revision"]): + raise SafetyError("unexpected_manifest_case_stage") + if digest(read_bounded(Path(__file__).resolve(), 262144, private=False)) != manifest["supervisor_sha256"]: + raise SafetyError("supervisor_hash_changed") + results: list[dict] = [] + setup_failure = None + # Reserve before runtime admission so a retained refusal cannot be replayed. + fd = new_log(target, "Retained Stage A results; native host/model/hard-cap acceptance remains unproven") + try: + try: + psutil = pinned_runtime() + pressure = PressureReader() + for case in manifest["cases"]: + result = execute_case(manifest, case, psutil, pressure, attempt_version=version) + results.append(result) + if not result["passed"] or not result["reaped"]: + break + except (SafetyError, OSError, ValueError, ImportError) as error: + setup_failure = type(error).__name__ + ":" + str(error)[:256] + data = encoded({"version": 1, "manifest_version": version, "manifest_sha256": digest(manifest_bytes), + "proof_scope": PROOF_SCOPE, "cases": results, "setup_failure": setup_failure, + "complete": setup_failure is None and len(results) == len(manifest["cases"]) and all(item["passed"] for item in results)}) + b"\n" + if len(data) > LIMITS["output_bytes"]: + raise SafetyError("results_byte_limit") + write_all(fd, data) + finally: + try: + os.fsync(fd) + finally: + os.close(fd) + return 0 if setup_failure is None and len(results) == len(manifest["cases"]) and all(item["passed"] for item in results) else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/native/mac-native-acceptance-v4.py b/tests/native/mac-native-acceptance-v4.py new file mode 100644 index 00000000..db6db16e --- /dev/null +++ b/tests/native/mac-native-acceptance-v4.py @@ -0,0 +1,895 @@ +"""ROOT-run Stage A supervisor. Importing this module never starts native work. + +This instrumented source-module harness proves tiny filesystem/lock behavior. +It does not certify installed packages, host dispatch, models or a physical cap. +""" +from __future__ import annotations + +import argparse +import hashlib +import importlib.abc +import importlib.machinery +import importlib.util +import json +import math +import os +import re +import select +import selectors +import signal +import stat +import subprocess +import sys +import time +from dataclasses import asdict, dataclass +from pathlib import Path +from types import ModuleType + +RUN_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/native-mac-v1/run-0001") +SOURCE_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/worktrees/mac-native-acceptance-v1") +RUNTIME_PACKET = Path("/Users/j/dev/truememory-maintenance-v1/.work/reports/mac-native-runtime-pins-v1.json") +RUNTIME_PACKET_SHA = "8db57e3f700aebb3adeb383c719a4bc5e2525917f4c017c853ee4694be1ee1e4" +PROOF_SCOPE = "instrumented_unmodified_source_native_filesystem_lock_capture" +SOURCE_FILES = ( + "truememory/_platform.py", "truememory/ingest/hooks/_shared.py", + "truememory/ingest/completion.py", "truememory/ingest/transcript.py", + "truememory/ingest/provenance.py", +) +CASES = frozenset({"capture-a1", "capture-a2", "capture-revision", "negative-a3", "invalid-state", "flock-a4", "parser-a5"}) +LIMITS = {"wall_seconds": 5, "termination_seconds": 1, "cpu_soft": 2, "cpu_hard": 3, + "rss_abort_bytes": 67108864, "core_bytes": 0, "nofile": 64, + "fsize_bytes": 1048576, "input_bytes": 65536, "output_bytes": 262144, + "events": 32, "frame_bytes": 2048, "ack_ms": 100, "threads": 1} +FREE_MIN = 268435456 +MAX_INT = (1 << 63) - 1 +_SHA = re.compile(r"[0-9a-f]{64}\Z") +_PENDING = re.compile(r"state-[0-9a-f]{32}\.pending\Z") +_ATTEMPT = re.compile(r"v(?:[1-9]|[1-9][0-9])\Z") +_announcement_bytes = 0 + + +class SafetyError(ValueError): + pass + + +def integer(value: object, *, minimum: int = 0, maximum: int = MAX_INT) -> int: + if type(value) is not int or not minimum <= value <= maximum: + raise SafetyError("invalid_integer") + return value + + +def exact_keys(value: object, keys: set[str]) -> dict: + if type(value) is not dict or set(value) != keys: + raise SafetyError("invalid_fields") + return value + + +def encoded(value: object) -> bytes: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8") + + +def digest(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def hash_text(value: object) -> str: + if type(value) is not str or _SHA.fullmatch(value) is None: + raise SafetyError("invalid_hash") + return value + + +def canonical_path(value: object, root: Path) -> Path: + if type(value) is not str or not value or "\0" in value or len(value.encode("utf-8")) > 1024: + raise SafetyError("invalid_path") + path = Path(value) + if not path.is_absolute() or ".." in path.parts or str(path) != value: + raise SafetyError("noncanonical_path") + if path != root and not path.is_relative_to(root): + raise SafetyError("path_escape") + return path + + +def owned_path(path: Path, *, directory: bool = False) -> os.stat_result: + if path.resolve() != path or any(item.is_symlink() for item in (path, *path.parents)): + raise SafetyError("path_link") + info = path.lstat() + if info.st_uid != os.getuid() or info.st_mode & 0o077: + raise SafetyError("path_not_private_owned") + if directory: + if not stat.S_ISDIR(info.st_mode): + raise SafetyError("path_not_directory") + elif not stat.S_ISREG(info.st_mode) or info.st_nlink != 1: + raise SafetyError("path_not_regular_single_link") + return info + + +def read_bounded(path: Path, maximum: int, *, private: bool = True) -> bytes: + if private: + owned_path(path) + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + try: + before = os.fstat(fd) + if not stat.S_ISREG(before.st_mode) or before.st_size > maximum: + raise SafetyError("read_limit_or_type") + if private and (before.st_uid != os.getuid() or before.st_mode & 0o077 or before.st_nlink != 1): + raise SafetyError("read_not_private") + chunks: list[bytes] = [] + remaining = maximum + 1 + while remaining: + chunk = os.read(fd, min(remaining, 65536)) + if not chunk: + break + chunks.append(chunk) + remaining -= len(chunk) + after = os.fstat(fd) + data = b"".join(chunks) + if file_identity(before) != file_identity(after) or len(data) > maximum or len(data) != after.st_size: + raise SafetyError("read_changed_or_limit") + return data + finally: + os.close(fd) + + +def file_identity(info: os.stat_result) -> list[int]: + return [info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns, info.st_ctime_ns] + + +@dataclass(frozen=True) +class Sensor: + monotonic_ns: int + pressure: int + free_bytes: int + swap_used_bytes: int + + +def valid_sensor(value: object) -> Sensor: + row = exact_keys(value, {"monotonic_ns", "pressure", "free_bytes", "swap_used_bytes"}) + integer(row["monotonic_ns"], minimum=1) + integer(row["pressure"], minimum=1, maximum=4) + integer(row["free_bytes"]) + integer(row["swap_used_bytes"]) + if row["pressure"] not in (1, 2, 4): + raise SafetyError("unknown_pressure_abi") + return Sensor(**row) + + +def admission(samples: list[Sensor], now_ns: int) -> None: + integer(now_ns, minimum=1) + if len(samples) < 3: + raise SafetyError("sensor_count") + previous = None + for sample in samples: + valid_sensor(asdict(sample)) + if sample.pressure != 1 or sample.free_bytes < FREE_MIN: + raise SafetyError("pressure_or_headroom") + if previous is not None and (sample.monotonic_ns <= previous.monotonic_ns or sample.swap_used_bytes > previous.swap_used_bytes): + raise SafetyError("unordered_or_growing_swap") + previous = sample + if samples[-1].monotonic_ns - samples[0].monotonic_ns < 2_000_000_000: + raise SafetyError("sensor_window") + if not 0 <= now_ns - samples[-1].monotonic_ns <= 500_000_000: + raise SafetyError("sensor_stale") + + +@dataclass(frozen=True) +class Identity: + pid: int + uid: int + birth_ns: int + pgid: int + + +def same_owned_identity(expected: Identity, actual: Identity, worker_uid: int) -> bool: + try: + for row in (expected, actual): + integer(row.pid, minimum=1) + integer(row.uid) + integer(row.birth_ns, minimum=1) + integer(row.pgid, minimum=1) + integer(worker_uid) + except SafetyError: + return False + return expected == actual and actual.uid == worker_uid and actual.pgid == actual.pid + + +def account_directory(case: dict) -> Path: + key = digest(encoded(["native-acceptance-v1", case["database"]])) + return RUN_ROOT / "completion" / key + + +def capture_metadata(case: dict) -> dict: + return {"version": 1, "user_id": "native-acceptance-v1", "database": case["database"], + "host": "claude", "session_id": case["session"], "source": case["source"], + "source_suffix": ".jsonl", "file_identity": case["source_identity"], + "byte_count": case["source_bytes"], "sha256": case["source_sha256"]} + + +def validate_case(value: object) -> dict: + row = exact_keys(value, {"id", "source", "source_sha256", "source_bytes", "source_identity", "session", "database", "previous"}) + if type(row["id"]) is not str or row["id"] not in CASES: + raise SafetyError("unsupported_case") + base = RUN_ROOT / "home/.claude/projects/native-acceptance-v1" + filename = {"capture-revision": "capture-a2", "invalid-state": "negative-a3"}.get(row["id"], row["id"]) + if row["id"] != "flock-a4": + if canonical_path(row["source"], RUN_ROOT) != base / (filename + ".jsonl"): + raise SafetyError("unexpected_fixture") + hash_text(row["source_sha256"]) + integer(row["source_bytes"], minimum=1, maximum=4096) + if type(row["source_identity"]) is not list or len(row["source_identity"]) != 5: + raise SafetyError("fixture_identity") + for number in row["source_identity"]: + integer(number) + if row["source_identity"][2] != row["source_bytes"]: + raise SafetyError("fixture_size_identity") + elif any(row[name] is not None for name in ("source", "source_sha256", "source_bytes", "source_identity")): + raise SafetyError("unexpected_lock_fixture") + if row["session"] != "synthetic-native-" + filename: + raise SafetyError("unexpected_session") + database_case = "capture-a2" if row["id"] == "capture-revision" else "negative-a3" if row["id"] == "invalid-state" else row["id"] + if row["database"] != str(RUN_ROOT / (database_case + "-v1.db")): + raise SafetyError("unexpected_database_locator") + if row["id"] == "capture-revision": + old = exact_keys(row["previous"], {"key", "sha256", "byte_count"}) + hash_text(old["key"]) + hash_text(old["sha256"]) + integer(old["byte_count"], minimum=1, maximum=4096) + if old["key"] == digest(encoded(capture_metadata(row))): + raise SafetyError("revision_not_new_evidence") + elif row["previous"] is not None: + raise SafetyError("unexpected_previous") + return row + + +def validate_manifest(value: object) -> dict: + row = exact_keys(value, {"version", "stage", "synthetic", "root_execution_go", "run_root", "home", "source_root", "source_commit", "sources", "supervisor_sha256", "bootstrap_sha256", "limits", "cases"}) + if type(row["version"]) is not int or row["version"] != 1 or row["stage"] != "A" or row["synthetic"] is not True or row["root_execution_go"] is not True: + raise SafetyError("manifest_authority_or_version") + if row["run_root"] != str(RUN_ROOT) or row["home"] != str(RUN_ROOT / "home") or row["source_root"] != str(SOURCE_ROOT): + raise SafetyError("unexpected_root") + if type(row["source_commit"]) is not str or re.fullmatch(r"[0-9a-f]{40}", row["source_commit"]) is None: + raise SafetyError("source_commit") + sources = exact_keys(row["sources"], set(SOURCE_FILES)) + for value in sources.values(): + hash_text(value) + hash_text(row["supervisor_sha256"]) + hash_text(row["bootstrap_sha256"]) + if type(row["limits"]) is not dict or row["limits"] != LIMITS or any(type(v) is not int for v in row["limits"].values()): + raise SafetyError("limit_change") + if type(row["cases"]) is not list or not 1 <= len(row["cases"]) <= len(CASES): + raise SafetyError("case_count") + cases = [validate_case(case) for case in row["cases"]] + if len({case["id"] for case in cases}) != len(cases): + raise SafetyError("duplicate_case") + return row + + +def validate_ready(value: object, pid: int) -> dict: + row = exact_keys(value, {"type", "version", "pid", "limits", "audit_installed"}) + integer(pid, minimum=1) + if row["type"] != "ready" or type(row["version"]) is not int or row["version"] != 1 or type(row["pid"]) is not int or row["pid"] != pid or row["audit_installed"] is not True: + raise SafetyError("limits_ready_mismatch") + expected = {"cpu": [2, 3], "core": [0, 0], "nofile": [64, 64], "fsize": [1048576, 1048576]} + limits = exact_keys(row["limits"], set(expected)) + for name, pair in limits.items(): + if type(pair) is not list or len(pair) != 2 or any(type(number) is not int for number in pair) or pair != expected[name]: + raise SafetyError("limits_ready_mismatch") + return row + + +def validate_result(value: object, case: dict, sources: dict) -> dict: + row = exact_keys(value, {"type", "version", "case", "passed", "proof_scope", "import_mode", "sources", "details"}) + if row["type"] != "result" or type(row["version"]) is not int or row["version"] != 1 or row["case"] != case["id"] or row["passed"] is not True or row["proof_scope"] != PROOF_SCOPE or row["import_mode"] != "isolated_source_module" or row["sources"] != sources or type(row["details"]) is not dict: + raise SafetyError("result_scope_mismatch") + return row + + +def validate_attempt_version(value: object) -> str: + if type(value) is not str or _ATTEMPT.fullmatch(value) is None: + raise SafetyError("invalid_attempt_version") + return value + + +def evidence_paths(manifest_path: Path) -> tuple[Path, str]: + name = manifest_path.name + if manifest_path.parent != RUN_ROOT or not name.startswith("manifest-") or not name.endswith(".json"): + raise SafetyError("unexpected_manifest_path") + version = validate_attempt_version(name[len("manifest-"):-len(".json")]) + return RUN_ROOT / ("results-" + version + ".json"), version + + +class MutationPolicy: + def __init__(self, case: dict) -> None: + self.case = case + self.account = account_directory(case) + self.directory_paths = {self.account, self.account.parent, *(self.account / name for name in ("captures", "receipts", "logs"))} + self.fixed = {self.account / "state.lock": "Completion account lock", self.account / "state.json": "Atomic completion state"} + if case["id"] not in {"flock-a4", "parser-a5"}: + self.fixed[self.account / "captures" / (digest(encoded(capture_metadata(case))) + ".jsonl")] = "Immutable synthetic source capture" + if case["id"] in {"negative-a3", "parser-a5"}: + self.directory_paths = set() + self.fixed = {} + elif case["id"] == "invalid-state": + self.fixed = {self.account / "state.lock": "Completion account lock"} + elif case["id"] == "flock-a4": + self.directory_paths = set() + self.fixed = {RUN_ROOT / "locks/contention-v1.lock": "Native lock contention witness"} + self.pending: set[Path] = set() + self.sequence = 0 + + def decide(self, value: object) -> dict: + row = exact_keys(value, {"type", "version", "seq", "event", "path", "source", "flags"}) + if row["type"] != "mutation" or type(row["version"]) is not int or row["version"] != 1: + raise SafetyError("mutation_shape") + seq = integer(row["seq"], minimum=1, maximum=LIMITS["events"]) + if seq != self.sequence + 1: + raise SafetyError("mutation_sequence") + path = canonical_path(row["path"], RUN_ROOT) + flags = integer(row["flags"], maximum=(1 << 31) - 1) + purpose = self.fixed.get(path) + if row["event"] == "os.mkdir": + if row["source"] is not None or path not in self.directory_paths or flags != 0o700: + raise SafetyError("unplanned_directory") + purpose = "Private completion directory" + elif row["event"] == "open" and flags & getattr(os, "O_DIRECTORY", 0): + expected = os.O_RDONLY | os.O_DIRECTORY + if (row["source"] is not None or self.case["id"] not in {"capture-a1", "capture-a2", "capture-revision"} + or path not in {self.account, self.account / "captures"} + or flags & ~getattr(os, "O_CLOEXEC", 0) != expected): + raise SafetyError("unplanned_directory_sync") + purpose = "Sync exact retained private capture directory" + elif row["event"] == "open": + if row["source"] is not None or flags & os.O_TRUNC: + raise SafetyError("unplanned_open") + access = flags & os.O_ACCMODE + extras = flags & ~(os.O_ACCMODE | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW | getattr(os, "O_CLOEXEC", 0)) + if extras or access not in (os.O_WRONLY, os.O_RDWR) or not flags & os.O_CREAT: + raise SafetyError("unplanned_open_flags") + if path.parent == self.account and _PENDING.fullmatch(path.name): + if self.case["id"] not in {"capture-a1", "capture-a2", "capture-revision"} or access != os.O_WRONLY or not flags & os.O_EXCL or len(self.pending | {path}) > 1: + raise SafetyError("pending_slot_limit") + self.pending.add(path) + purpose = "Atomic completion-state temporary; failed evidence retained" + elif path not in self.fixed or path.name == "state.json": + raise SafetyError("unplanned_file") + elif path.name == "state.lock" or path == RUN_ROOT / "locks/contention-v1.lock": + if access != os.O_RDWR or flags & os.O_EXCL: + raise SafetyError("lock_flags") + elif access != os.O_WRONLY or not flags & os.O_EXCL: + raise SafetyError("capture_flags") + elif row["event"] == "os.rename": + source = canonical_path(row["source"], RUN_ROOT) + if flags != 0 or path != self.account / "state.json" or source not in self.pending: + raise SafetyError("unplanned_replace") + purpose = "Publish current-run completion state from its approved temporary" + else: + raise SafetyError("unsupported_mutation") + self.sequence = seq + return {"type": "grant", "version": 1, "seq": seq, "path": str(path), "purpose": purpose} + + +def mutation_target(grant: dict, request: dict) -> None: + path = Path(grant["path"]) + owned_path(path.parent, directory=True) + if request["event"] == "open" and request["flags"] & getattr(os, "O_DIRECTORY", 0): + owned_path(path, directory=True) + return + if path.exists() or path.is_symlink(): + owned_path(path, directory=request["event"] == "os.mkdir") + if request["event"] == "os.rename": + owned_path(Path(request["source"])) + + +class PressureReader: + def __init__(self) -> None: + import ctypes + self.ctypes = ctypes + self.library = ctypes.CDLL("/usr/lib/libSystem.B.dylib", use_errno=True) + self.read = self.library.sysctlbyname + self.read.argtypes = [ctypes.c_char_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.c_void_p, ctypes.c_size_t] + self.read.restype = ctypes.c_int + + def pressure(self) -> int: + c = self.ctypes + output = c.c_uint32() + size = c.c_size_t(c.sizeof(output)) + if self.read(b"kern.memorystatus_vm_pressure_level", c.byref(output), c.byref(size), None, 0) != 0 or size.value != 4: + raise SafetyError("pressure_unavailable") + if output.value not in (1, 2, 4): + raise SafetyError("pressure_abi_unknown") + return output.value + + +class PinnedPythonLoader(importlib.abc.Loader): + def __init__(self, path: Path, checksum: str) -> None: + self.path, self.checksum = path, checksum + + def create_module(self, spec: object) -> None: + return None + + def exec_module(self, module: ModuleType) -> None: + data = read_bounded(self.path, 262144, private=False) + if digest(data) != self.checksum: + raise SafetyError("dependency_hash_changed") + exec(compile(data, str(self.path), "exec", dont_inherit=True), module.__dict__) + + +class PinnedPsutilFinder(importlib.abc.MetaPathFinder): + def __init__(self, packet: dict) -> None: + self.root = Path(packet["dependency_root"]) + self.files = packet["dependency_files"] + + def find_spec(self, fullname: str, path: object = None, target: object = None) -> object: + if fullname != "psutil" and not fullname.startswith("psutil."): + return None + relative = fullname.split(".")[1:] + candidate = self.root.joinpath(*relative) if relative else self.root + source = candidate / "__init__.py" if not relative else candidate.with_suffix(".py") + entry = self.files.get(str(source)) + if entry is not None: + return importlib.util.spec_from_file_location(fullname, source, loader=PinnedPythonLoader(source, entry["sha256"]), submodule_search_locations=[str(self.root)] if not relative else None) + for suffix in importlib.machinery.EXTENSION_SUFFIXES: + extension = Path(str(candidate) + suffix) + entry = self.files.get(str(extension)) + if entry is not None: + if digest(read_bounded(extension, 262144, private=False)) != entry["sha256"]: + raise SafetyError("dependency_extension_changed") + return importlib.util.spec_from_file_location(fullname, extension) + raise SafetyError("unpinned_dependency_module") + + +def pinned_runtime() -> ModuleType: + raw = read_bounded(RUNTIME_PACKET, 65536, private=False) + if digest(raw) != RUNTIME_PACKET_SHA: + raise SafetyError("runtime_packet_changed") + packet = json.loads(raw) + if Path(sys.executable).resolve() != Path(packet["python_realpath"]) or sys.version_info[:3] != (3, 12, 12): + raise SafetyError("interpreter_mismatch") + if digest(read_bounded(Path(packet["python_realpath"]), 1048576, private=False)) != packet["python_executable_sha256"]: + raise SafetyError("interpreter_hash_changed") + if "psutil" in sys.modules: + raise SafetyError("dependency_preloaded") + for name, entry in packet["dependency_files"].items(): + if digest(read_bounded(Path(name), 262144, private=False)) != entry["sha256"]: + raise SafetyError("dependency_hash_changed") + sys.meta_path.insert(0, PinnedPsutilFinder(packet)) + import psutil + if psutil.__version__ != packet["psutil_version"]: + raise SafetyError("dependency_version_changed") + return psutil + + +def sample(psutil: ModuleType, pressure: PressureReader) -> Sensor: + started = time.monotonic_ns() + row = Sensor(started, pressure.pressure(), psutil.virtual_memory().free, psutil.swap_memory().used) + valid_sensor(asdict(row)) + if time.monotonic_ns() - started > 500_000_000: + raise SafetyError("sensor_call_stale") + return row + + +def observe_identity(psutil: ModuleType, pid: int) -> Identity: + process = psutil.Process(pid) + born = process.create_time() + try: + valid = type(born) in (int, float) and math.isfinite(born) and born > 0 + except (OverflowError, ValueError): + valid = False + if not valid: + raise SafetyError("birth_unavailable") + return Identity(pid, process.uids().real, int(born * 1_000_000_000), os.getpgid(pid)) + + +def announce(path: Path, purpose: str) -> None: + global _announcement_bytes + line = f"Create {path}: {purpose}\n" + _announcement_bytes += len(line.encode("utf-8")) + if _announcement_bytes > 65536: + raise SafetyError("announcement_byte_limit") + sys.stdout.write(line) + sys.stdout.flush() + + +def new_log(path: Path, purpose: str) -> int: + owned_path(path.parent, directory=True) + announce(path, purpose) + return os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) + + +def write_all(fd: int, data: bytes) -> None: + while data: + count = os.write(fd, data) + if count <= 0: + raise SafetyError("write_no_progress") + data = data[count:] + + +def write_pipe(fd: int, data: bytes, deadline: float) -> None: + if len(data) > LIMITS["input_bytes"]: + raise SafetyError("pipe_frame_input_limit") + while data: + remaining = deadline - time.monotonic() + if remaining <= 0: + raise SafetyError("pipe_write_deadline") + try: + count = os.write(fd, data) + except BlockingIOError: + select.select([], [fd], [], min(remaining, 0.01)) + continue + if count <= 0: + raise SafetyError("pipe_write_no_progress") + data = data[count:] + + +def group_gone(pgid: int) -> bool: + integer(pgid, minimum=1) + try: + os.killpg(pgid, 0) + except ProcessLookupError: + return True + except OSError: + return False + return False + + +def member_identity_matches(expected: Identity, actual: Identity, group: int, worker_uid: int) -> bool: + try: + for row in (expected, actual): + integer(row.pid, minimum=1) + integer(row.uid) + integer(row.birth_ns, minimum=1) + integer(row.pgid, minimum=1) + integer(group, minimum=1) + integer(worker_uid) + except SafetyError: + return False + return expected == actual and actual.uid == worker_uid and actual.pgid == group + + +def terminate_owned(child: subprocess.Popen, expected: Identity | None, members: list[Identity], psutil: ModuleType, result: dict) -> None: + deadline = time.monotonic() + LIMITS["termination_seconds"] + for sig, interval in ((signal.SIGTERM, 0.25), (signal.SIGKILL, 0.75)): + if time.monotonic() >= deadline: + raise SafetyError("termination_deadline") + if child.poll() is None: + if expected is None: + # Popen still owns an unreaped direct child; its PID cannot be reused. + child.send_signal(sig) + result["termination_authority"] = "unreaped_direct_popen_child" + elif same_owned_identity(expected, observe_identity(psutil, child.pid), os.getuid()): + if result["descendant_containment"] == "unproven": + os.killpg(expected.pgid, sig) + result["termination_authority"] = "fresh_owned_leader_group" + else: + child.send_signal(sig) + result["termination_authority"] = "fresh_owned_direct_child" + else: + raise SafetyError("termination_identity_changed") + if sig == signal.SIGKILL and members and expected is not None: + for member in members: + if time.monotonic() >= deadline: + raise SafetyError("member_termination_deadline") + try: + actual = observe_identity(psutil, member.pid) + except psutil.NoSuchProcess: + continue + if not member_identity_matches(member, actual, expected.pgid, os.getuid()): + raise SafetyError("member_termination_identity_changed") + psutil.Process(member.pid).send_signal(sig) + if not result["reaped"]: + try: + child.wait(timeout=max(0.001, min(interval, deadline - time.monotonic()))) + result["reaped"] = True + except subprocess.TimeoutExpired: + pass + if result["reaped"] and not members: + break + result["exit_code"] = child.returncode + if not result["reaped"]: + raise SafetyError("direct_child_reap_unproven") + if result["descendant_containment"] == "unproven": + if expected is None or len(result["descendants"]) != len(members): + raise SafetyError("member_inventory_unproven") + terminal = True + for member in members: + if time.monotonic() >= deadline: + raise SafetyError("containment_proof_deadline") + try: + actual = observe_identity(psutil, member.pid) + except psutil.NoSuchProcess: + continue + if member_identity_matches(member, actual, expected.pgid, os.getuid()): + terminal = False + result["descendant_containment"] = "proven_terminal" if terminal and group_gone(expected.pgid) else "unproven" + if result["descendant_containment"] != "proven_terminal": + raise SafetyError("owned_group_containment_unproven") + + +def execute_case(manifest: dict, case: dict, psutil: ModuleType, pressure: PressureReader, attempt_version: str = "v1") -> dict: + attempt_version = validate_attempt_version(attempt_version) + bootstrap = SOURCE_ROOT / "tests/native/mac-native-bootstrap-v4.py" + policy = MutationPolicy(case) + records: list[Sensor] = [] + result = {"case": case["id"], "implemented": True, "executed": False, "passed": False, + "blocked": None, "unproven": ["installed_package", "installed_host", "models", "physical_hard_cap"], + "proof_scope": PROOF_SCOPE, "import_mode": "isolated_source_module", "synthetic": True, + "source_commit": manifest["source_commit"], "sources": manifest["sources"], + "sensors": [asdict(row) for row in records], "rss_sampled_peak": 0, "limits": LIMITS, + "reaped": False, "exit_code": None, "identity": None, "output_bytes": 0, + "descendants": [], "descendant_containment": "not_observed"} + logs: dict[str, int] = {} + child = None + expected = None + members: list[Identity] = [] + selector = selectors.DefaultSelector() + held_lock = None + frames = bytearray() + protocol_result = None + start = time.monotonic() + try: + records = [] + for index in range(3): + records.append(sample(psutil, pressure)) + if index < 2: + time.sleep(1) + admission(records, time.monotonic_ns()) + if case["source"] is not None: + source = Path(case["source"]) + info = owned_path(source) + raw = read_bounded(source, 4096) + if file_identity(info) != case["source_identity"] or digest(raw) != case["source_sha256"] or len(raw) != case["source_bytes"]: + raise SafetyError("fixture_changed_before_launch") + for name, checksum in manifest["sources"].items(): + if digest(read_bounded(SOURCE_ROOT / name, 262144, private=False)) != checksum: + raise SafetyError("source_hash_changed") + if digest(read_bounded(bootstrap, 262144, private=False)) != manifest["bootstrap_sha256"]: + raise SafetyError("bootstrap_hash_changed") + result["sensors"] = [asdict(row) for row in records] + for channel in ("stdout", "stderr", "events"): + logs[channel] = new_log(RUN_ROOT / "logs" / (case["id"] + "-" + channel + "-" + attempt_version + ".log"), "Retained native " + channel + " evidence") + if case["id"] == "flock-a4": + import fcntl + lock = RUN_ROOT / "locks/contention-v1.lock" + owned_path(lock) + held_lock = os.open(lock, os.O_RDWR | os.O_NOFOLLOW) + fcntl.flock(held_lock, fcntl.LOCK_EX | fcntl.LOCK_NB) + env = {"HOME": manifest["home"], "PATH": "/usr/bin:/bin", "LANG": "C.UTF-8", + "OMP_NUM_THREADS": "1", "OPENBLAS_NUM_THREADS": "1", "MKL_NUM_THREADS": "1", + "VECLIB_MAXIMUM_THREADS": "1", "NUMEXPR_NUM_THREADS": "1"} + request = encoded({"version": 1, "source_root": str(SOURCE_ROOT), "run_root": str(RUN_ROOT), + "home": manifest["home"], "sources": manifest["sources"], "limits": LIMITS, "case": case, + "supervisor_sha256": manifest["supervisor_sha256"]}) + b"\n" + if len(request) > LIMITS["input_bytes"] - LIMITS["events"] * 256: + raise SafetyError("input_limit") + fresh = sample(psutil, pressure) + admission([*records[:-1], fresh], time.monotonic_ns()) + result["sensors"].append(asdict(fresh)) + start = time.monotonic() + child = subprocess.Popen([sys.executable, "-I", "-S", "-B", str(bootstrap)], stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, + cwd=str(RUN_ROOT), close_fds=True, start_new_session=True) + result["executed"] = True + expected = observe_identity(psutil, child.pid) + if not same_owned_identity(expected, expected, os.getuid()): + raise SafetyError("child_identity_unowned") + result["identity"] = asdict(expected) + assert child.stdin is not None and child.stdout is not None and child.stderr is not None + for stream, channel in ((child.stdout, "stdout"), (child.stderr, "stderr")): + os.set_blocking(stream.fileno(), False) + selector.register(stream, selectors.EVENT_READ, channel) + os.set_blocking(child.stdin.fileno(), False) + write_pipe(child.stdin.fileno(), request, min(start + LIMITS["wall_seconds"], time.monotonic() + 0.1)) + input_count = len(request) + previous = fresh + ready = False + next_sample = time.monotonic() + while selector.get_map(): + now = time.monotonic() + if now - start >= LIMITS["wall_seconds"]: + raise SafetyError("child_wall_deadline") + if child.poll() is None and now >= next_sample: + current = sample(psutil, pressure) + if current.pressure != 1 or current.free_bytes < FREE_MIN or current.swap_used_bytes > previous.swap_used_bytes or time.monotonic_ns() - current.monotonic_ns > 500_000_000: + raise SafetyError("during_child_sensor_refusal") + actual = observe_identity(psutil, child.pid) + if not same_owned_identity(expected, actual, os.getuid()): + raise SafetyError("child_identity_changed") + process = psutil.Process(child.pid) + descendants = process.children(recursive=True) + if descendants: + result["descendants"] = [item.pid for item in descendants[:32]] + result["descendant_containment"] = "unproven" + if len(descendants) > 32: + raise SafetyError("descendant_inventory_limit") + members = [observe_identity(psutil, item.pid) for item in descendants] + if any(member.uid != os.getuid() or member.pgid != expected.pgid for member in members): + raise SafetyError("descendant_identity_unowned") + raise SafetyError("unexpected_descendant") + rss = integer(process.memory_info().rss) + result["rss_sampled_peak"] = max(result["rss_sampled_peak"], rss) + if rss > LIMITS["rss_abort_bytes"]: + raise SafetyError("sampled_rss_limit") + if len(result["sensors"]) >= 64: + raise SafetyError("sensor_inventory_limit") + result["sensors"].append(asdict(current)) + previous = current + next_sample = now + 0.1 + for key, _ in selector.select(0.01): + try: + chunk = os.read(key.fileobj.fileno(), 4096) + except BlockingIOError: + continue + if not chunk: + selector.unregister(key.fileobj) + continue + result["output_bytes"] += len(chunk) + if result["output_bytes"] > LIMITS["output_bytes"]: + raise SafetyError("combined_output_limit") + channel = key.data + write_all(logs[channel], chunk) + if channel != "stdout": + continue + frames.extend(chunk) + while b"\n" in frames: + line, _, rest = frames.partition(b"\n") + frames = bytearray(rest) + if len(line) > LIMITS["frame_bytes"]: + raise SafetyError("protocol_frame_limit") + frame_deadline = min(start + LIMITS["wall_seconds"], time.monotonic() + 0.1) + frame = json.loads(line) + if type(frame) is not dict: + raise SafetyError("protocol_object_required") + if frame.get("type") == "mutation": + if not ready or protocol_result is not None: + raise SafetyError("mutation_outside_ready_execution") + try: + grant = policy.decide(frame) + mutation_target(grant, frame) + except (SafetyError, OSError, ValueError) as error: + write_all(logs["events"], encoded({"request": frame, "decision": {"type": "deny", "reason": type(error).__name__ + ":" + str(error)[:256]}}) + b"\n") + os.fsync(logs["events"]) + raise + write_all(logs["events"], encoded({"request": frame, "decision": grant}) + b"\n") + os.fsync(logs["events"]) + announce(Path(grant["path"]), grant["purpose"]) + reply = encoded(grant) + b"\n" + input_count += len(reply) + if input_count > LIMITS["input_bytes"]: + raise SafetyError("input_limit") + write_pipe(child.stdin.fileno(), reply, frame_deadline) + elif frame == {"type": "lock_busy"} and ready and protocol_result is None and case["id"] == "flock-a4" and held_lock is not None: + os.close(held_lock) + held_lock = None + reply = b'{"type":"lock_released"}\n' + input_count += len(reply) + if input_count > LIMITS["input_bytes"]: + raise SafetyError("input_limit") + write_pipe(child.stdin.fileno(), reply, frame_deadline) + elif frame.get("type") == "result" and protocol_result is None: + if not ready: + raise SafetyError("result_before_ready") + protocol_result = validate_result(frame, case, manifest["sources"]) + elif frame.get("type") == "ready" and not ready: + validate_ready(frame, child.pid) + ready = True + result["limit_readbacks"] = frame["limits"] + else: + raise SafetyError("unexpected_protocol_frame") + if len(frames) > LIMITS["frame_bytes"]: + raise SafetyError("protocol_frame_limit") + if frames: + raise SafetyError("truncated_protocol_frame") + remaining = max(0.001, start + LIMITS["wall_seconds"] - time.monotonic()) + result["exit_code"] = child.wait(timeout=remaining) + result["reaped"] = True + expected_exit = 73 if case["id"] == "flock-a4" else 0 + if result["exit_code"] != expected_exit or type(protocol_result) is not dict or protocol_result.get("passed") is not True or protocol_result.get("proof_scope") != PROOF_SCOPE: + raise SafetyError("case_nonterminal_or_failed") + if case["id"] == "flock-a4": + lock_fd = os.open(RUN_ROOT / "locks/contention-v1.lock", os.O_RDWR | os.O_NOFOLLOW) + try: + fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + finally: + os.close(lock_fd) + if not group_gone(expected.pgid): + result["descendant_containment"] = "unproven" + raise SafetyError("post_exit_group_containment_unproven") + result["passed"] = True + result["child_result"] = protocol_result + except (SafetyError, OSError, ValueError, subprocess.SubprocessError, psutil.Error) as error: + result["blocked"] = type(error).__name__ + ":" + str(error)[:256] + finally: + if not result["sensors"]: + result["sensors"] = [asdict(row) for row in records] + if child is None: + result["reaped"] = True + if held_lock is not None: + try: + os.close(held_lock) + except OSError as error: + result["blocked"] = "lock_close_failed:" + str(error)[:256] + result["passed"] = False + if child is not None and (not result["reaped"] or result["descendant_containment"] == "unproven"): + try: + terminate_owned(child, expected, members, psutil, result) + except (SafetyError, OSError, ValueError, subprocess.SubprocessError, psutil.Error) as error: + result["blocked"] = "identity_or_reap_unproven:" + str(error)[:256] + result["passed"] = False + result["wall_seconds"] = time.monotonic() - start + try: + selector.close() + except OSError as error: + result["blocked"] = "selector_close_failed:" + str(error)[:256] + result["passed"] = False + if child is not None: + for stream in (child.stdin, child.stdout, child.stderr): + if stream is not None: + try: + stream.close() + except OSError as error: + result["blocked"] = "pipe_close_failed:" + str(error)[:256] + result["passed"] = False + for fd in logs.values(): + try: + os.fsync(fd) + except OSError as error: + result["blocked"] = "evidence_sync_failed:" + str(error)[:256] + result["passed"] = False + finally: + try: + os.close(fd) + except OSError as error: + result["blocked"] = "evidence_close_failed:" + str(error)[:256] + result["passed"] = False + return result + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--manifest", required=True) + parser.add_argument("--execute-root-authorized", action="store_true") + args = parser.parse_args() + if not args.execute_root_authorized or sys.platform != "darwin" or not sys.flags.isolated or not sys.flags.no_site or not sys.flags.dont_write_bytecode: + raise SafetyError("root_isolated_native_execution_required") + sys.dont_write_bytecode = True + os.umask(0o077) + manifest_path = canonical_path(args.manifest, RUN_ROOT) + target, version = evidence_paths(manifest_path) + owned_path(RUN_ROOT, directory=True) + owned_path(RUN_ROOT / "home", directory=True) + manifest_bytes = read_bounded(manifest_path, 65536) + manifest = validate_manifest(json.loads(manifest_bytes)) + if (version != "v2" and any(case["id"] == "capture-revision" for case in manifest["cases"])) or (version == "v2" and [case["id"] for case in manifest["cases"]] != ["capture-revision"]): + raise SafetyError("unexpected_manifest_case_stage") + if digest(read_bounded(Path(__file__).resolve(), 262144, private=False)) != manifest["supervisor_sha256"]: + raise SafetyError("supervisor_hash_changed") + results: list[dict] = [] + setup_failure = None + # Reserve before runtime admission so a retained refusal cannot be replayed. + fd = new_log(target, "Retained Stage A results; native host/model/hard-cap acceptance remains unproven") + try: + try: + psutil = pinned_runtime() + pressure = PressureReader() + for case in manifest["cases"]: + result = execute_case(manifest, case, psutil, pressure, attempt_version=version) + results.append(result) + if not result["passed"] or not result["reaped"]: + break + except (SafetyError, OSError, ValueError, ImportError) as error: + setup_failure = type(error).__name__ + ":" + str(error)[:256] + data = encoded({"version": 1, "manifest_version": version, "manifest_sha256": digest(manifest_bytes), + "proof_scope": PROOF_SCOPE, "cases": results, "setup_failure": setup_failure, + "complete": setup_failure is None and len(results) == len(manifest["cases"]) and all(item["passed"] for item in results)}) + b"\n" + if len(data) > LIMITS["output_bytes"]: + raise SafetyError("results_byte_limit") + write_all(fd, data) + finally: + try: + os.fsync(fd) + finally: + os.close(fd) + return 0 if setup_failure is None and len(results) == len(manifest["cases"]) and all(item["passed"] for item in results) else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/native/mac-native-acceptance-v6.py b/tests/native/mac-native-acceptance-v6.py new file mode 100644 index 00000000..6927fd3d --- /dev/null +++ b/tests/native/mac-native-acceptance-v6.py @@ -0,0 +1,966 @@ +"""ROOT-run Stage A supervisor. Importing this module never starts native work. + +This instrumented source-module harness proves tiny filesystem/lock behavior. +It does not certify installed packages, host dispatch, models or a physical cap. +""" +from __future__ import annotations + +import argparse +import hashlib +import importlib.abc +import importlib.machinery +import importlib.util +import json +import math +import os +import re +import select +import selectors +import signal +import stat +import subprocess +import sys +import time +from dataclasses import asdict, dataclass +from pathlib import Path +from types import ModuleType + +RUN_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/native-mac-v1/run-0001") +SOURCE_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/worktrees/mac-native-acceptance-v1") +RUNTIME_PACKET = Path("/Users/j/dev/truememory-maintenance-v1/.work/reports/mac-native-runtime-pins-v1.json") +RUNTIME_PACKET_SHA = "8db57e3f700aebb3adeb383c719a4bc5e2525917f4c017c853ee4694be1ee1e4" +PROOF_SCOPE = "instrumented_unmodified_source_native_filesystem_lock_capture" +SOURCE_FILES = ( + "truememory/_platform.py", "truememory/ingest/hooks/_shared.py", + "truememory/ingest/completion.py", "truememory/ingest/transcript.py", + "truememory/ingest/provenance.py", +) +CASES = frozenset({"capture-a1", "capture-a2", "capture-revision", "negative-a3", "invalid-state", "flock-a4", "parser-a5"}) +LIMITS = {"wall_seconds": 5, "termination_seconds": 1, "cpu_soft": 2, "cpu_hard": 3, + "rss_abort_bytes": 67108864, "core_bytes": 0, "nofile": 64, + "fsize_bytes": 1048576, "input_bytes": 65536, "output_bytes": 262144, + "events": 32, "frame_bytes": 2048, "ack_ms": 100, "threads": 1} +FREE_MIN = 268435456 +MAX_INT = (1 << 63) - 1 +_SHA = re.compile(r"[0-9a-f]{64}\Z") +_PENDING = re.compile(r"state-[0-9a-f]{32}\.pending\Z") +_ATTEMPT = re.compile(r"v(?:[1-9]|[1-9][0-9])\Z") +_announcement_bytes = 0 + + +class SafetyError(ValueError): + pass + + +def integer(value: object, *, minimum: int = 0, maximum: int = MAX_INT) -> int: + if type(value) is not int or not minimum <= value <= maximum: + raise SafetyError("invalid_integer") + return value + + +def exact_keys(value: object, keys: set[str]) -> dict: + if type(value) is not dict or set(value) != keys: + raise SafetyError("invalid_fields") + return value + + +def encoded(value: object) -> bytes: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8") + + +def digest(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def hash_text(value: object) -> str: + if type(value) is not str or _SHA.fullmatch(value) is None: + raise SafetyError("invalid_hash") + return value + + +def canonical_path(value: object, root: Path) -> Path: + if type(value) is not str or not value or "\0" in value or len(value.encode("utf-8")) > 1024: + raise SafetyError("invalid_path") + path = Path(value) + if not path.is_absolute() or ".." in path.parts or str(path) != value: + raise SafetyError("noncanonical_path") + if path != root and not path.is_relative_to(root): + raise SafetyError("path_escape") + return path + + +def owned_path(path: Path, *, directory: bool = False) -> os.stat_result: + if path.resolve() != path or any(item.is_symlink() for item in (path, *path.parents)): + raise SafetyError("path_link") + info = path.lstat() + if info.st_uid != os.getuid() or info.st_mode & 0o077: + raise SafetyError("path_not_private_owned") + if directory: + if not stat.S_ISDIR(info.st_mode): + raise SafetyError("path_not_directory") + elif not stat.S_ISREG(info.st_mode) or info.st_nlink != 1: + raise SafetyError("path_not_regular_single_link") + return info + + +def read_bounded(path: Path, maximum: int, *, private: bool = True) -> bytes: + if private: + owned_path(path) + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + try: + before = os.fstat(fd) + if not stat.S_ISREG(before.st_mode) or before.st_size > maximum: + raise SafetyError("read_limit_or_type") + if private and (before.st_uid != os.getuid() or before.st_mode & 0o077 or before.st_nlink != 1): + raise SafetyError("read_not_private") + chunks: list[bytes] = [] + remaining = maximum + 1 + while remaining: + chunk = os.read(fd, min(remaining, 65536)) + if not chunk: + break + chunks.append(chunk) + remaining -= len(chunk) + after = os.fstat(fd) + data = b"".join(chunks) + if file_identity(before) != file_identity(after) or len(data) > maximum or len(data) != after.st_size: + raise SafetyError("read_changed_or_limit") + return data + finally: + os.close(fd) + + +def file_identity(info: os.stat_result) -> list[int]: + return [info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns, info.st_ctime_ns] + + +@dataclass(frozen=True) +class Sensor: + monotonic_ns: int + pressure: int + free_bytes: int + swap_used_bytes: int + + +def valid_sensor(value: object) -> Sensor: + row = exact_keys(value, {"monotonic_ns", "pressure", "free_bytes", "swap_used_bytes"}) + integer(row["monotonic_ns"], minimum=1) + integer(row["pressure"], minimum=1, maximum=4) + integer(row["free_bytes"]) + integer(row["swap_used_bytes"]) + if row["pressure"] not in (1, 2, 4): + raise SafetyError("unknown_pressure_abi") + return Sensor(**row) + + +def admission(samples: list[Sensor], now_ns: int) -> None: + integer(now_ns, minimum=1) + if len(samples) < 3: + raise SafetyError("sensor_count") + previous = None + for sample in samples: + valid_sensor(asdict(sample)) + if sample.pressure != 1 or sample.free_bytes < FREE_MIN: + raise SafetyError("pressure_or_headroom") + if previous is not None and (sample.monotonic_ns <= previous.monotonic_ns or sample.swap_used_bytes > previous.swap_used_bytes): + raise SafetyError("unordered_or_growing_swap") + previous = sample + if samples[-1].monotonic_ns - samples[0].monotonic_ns < 2_000_000_000: + raise SafetyError("sensor_window") + if not 0 <= now_ns - samples[-1].monotonic_ns <= 500_000_000: + raise SafetyError("sensor_stale") + + +@dataclass(frozen=True) +class Identity: + pid: int + uid: int + birth_ns: int + pgid: int + + +def same_owned_identity(expected: Identity, actual: Identity, worker_uid: int) -> bool: + try: + for row in (expected, actual): + integer(row.pid, minimum=1) + integer(row.uid) + integer(row.birth_ns, minimum=1) + integer(row.pgid, minimum=1) + integer(worker_uid) + except SafetyError: + return False + return expected == actual and actual.uid == worker_uid and actual.pgid == actual.pid + + +def account_directory(case: dict) -> Path: + key = digest(encoded(["native-acceptance-v1", case["database"]])) + return RUN_ROOT / "completion" / key + + +def capture_metadata(case: dict) -> dict: + return {"version": 1, "user_id": "native-acceptance-v1", "database": case["database"], + "host": "claude", "session_id": case["session"], "source": case["source"], + "source_suffix": ".jsonl", "file_identity": case["source_identity"], + "byte_count": case["source_bytes"], "sha256": case["source_sha256"]} + + +def validate_case(value: object) -> dict: + row = exact_keys(value, {"id", "source", "source_sha256", "source_bytes", "source_identity", "session", "database", "previous"}) + if type(row["id"]) is not str or row["id"] not in CASES: + raise SafetyError("unsupported_case") + base = RUN_ROOT / "home/.claude/projects/native-acceptance-v1" + filename = {"capture-revision": "capture-a2", "invalid-state": "negative-a3"}.get(row["id"], row["id"]) + if row["id"] != "flock-a4": + if canonical_path(row["source"], RUN_ROOT) != base / (filename + ".jsonl"): + raise SafetyError("unexpected_fixture") + hash_text(row["source_sha256"]) + integer(row["source_bytes"], minimum=1, maximum=4096) + if type(row["source_identity"]) is not list or len(row["source_identity"]) != 5: + raise SafetyError("fixture_identity") + for number in row["source_identity"]: + integer(number) + if row["source_identity"][2] != row["source_bytes"]: + raise SafetyError("fixture_size_identity") + elif any(row[name] is not None for name in ("source", "source_sha256", "source_bytes", "source_identity")): + raise SafetyError("unexpected_lock_fixture") + if row["session"] != "synthetic-native-" + filename: + raise SafetyError("unexpected_session") + database_case = "capture-a2" if row["id"] == "capture-revision" else "negative-a3" if row["id"] == "invalid-state" else row["id"] + if row["database"] != str(RUN_ROOT / (database_case + "-v1.db")): + raise SafetyError("unexpected_database_locator") + if row["id"] == "capture-revision": + old = exact_keys(row["previous"], {"key", "sha256", "byte_count"}) + hash_text(old["key"]) + hash_text(old["sha256"]) + integer(old["byte_count"], minimum=1, maximum=4096) + if old["key"] == digest(encoded(capture_metadata(row))): + raise SafetyError("revision_not_new_evidence") + elif row["previous"] is not None: + raise SafetyError("unexpected_previous") + return row + + +def validate_manifest(value: object) -> dict: + row = exact_keys(value, {"version", "stage", "synthetic", "root_execution_go", "run_root", "home", "source_root", "source_commit", "sources", "supervisor_sha256", "bootstrap_sha256", "limits", "cases"}) + if type(row["version"]) is not int or row["version"] != 1 or row["stage"] != "A" or row["synthetic"] is not True or row["root_execution_go"] is not True: + raise SafetyError("manifest_authority_or_version") + if row["run_root"] != str(RUN_ROOT) or row["home"] != str(RUN_ROOT / "home") or row["source_root"] != str(SOURCE_ROOT): + raise SafetyError("unexpected_root") + if type(row["source_commit"]) is not str or re.fullmatch(r"[0-9a-f]{40}", row["source_commit"]) is None: + raise SafetyError("source_commit") + sources = exact_keys(row["sources"], set(SOURCE_FILES)) + for value in sources.values(): + hash_text(value) + hash_text(row["supervisor_sha256"]) + hash_text(row["bootstrap_sha256"]) + if type(row["limits"]) is not dict or row["limits"] != LIMITS or any(type(v) is not int for v in row["limits"].values()): + raise SafetyError("limit_change") + if type(row["cases"]) is not list or not 1 <= len(row["cases"]) <= len(CASES): + raise SafetyError("case_count") + cases = [validate_case(case) for case in row["cases"]] + if len({case["id"] for case in cases}) != len(cases): + raise SafetyError("duplicate_case") + return row + + +def capture_read_catalog(case: dict) -> dict[Path, tuple[int, str]]: + if case["id"] not in {"capture-a1", "capture-a2", "capture-revision"}: + return {} + validate_case(case) + directory = account_directory(case) / "captures" + current = directory / (digest(encoded(capture_metadata(case))) + ".jsonl") + catalog = {current: (case["source_bytes"], case["source_sha256"])} + if case["id"] == "capture-revision": + previous = case["previous"] + catalog[directory / (previous["key"] + ".jsonl")] = (previous["byte_count"], previous["sha256"]) + if len(catalog) > 2: + raise SafetyError("capture_read_catalog_limit") + return catalog + + +def verify_capture_read_target(case: dict, path: Path) -> None: + import fcntl + catalog = capture_read_catalog(case) + if path not in catalog: + raise SafetyError("unplanned_capture_read") + expected_size, expected_hash = catalog[path] + owned_path(path.parent.parent, directory=True) + owned_path(path.parent, directory=True) + before_path = owned_path(path) + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + owned_identity: tuple[int, int, int, int] | None = None + try: + if type(fd) is not int or fd < 3: + raise SafetyError("capture_read_target_descriptor_type") + before = os.fstat(fd) + if (not stat.S_ISREG(before.st_mode) or before.st_uid != os.getuid() + or before.st_mode & 0o077 or before.st_nlink != 1 + or file_identity(before) != file_identity(before_path) + or fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_ACCMODE != os.O_RDONLY): + raise SafetyError("capture_read_target_identity") + owned_identity = (before.st_dev, before.st_ino, stat.S_IFMT(before.st_mode), before.st_uid) + if before.st_size != expected_size: + raise SafetyError("capture_read_target_size") + checksum = hashlib.sha256() + offset = 0 + while offset <= expected_size: + chunk = os.pread(fd, expected_size + 1 - offset, offset) + if not chunk: + break + checksum.update(chunk) + offset += len(chunk) + after = os.fstat(fd) + after_path = owned_path(path) + if (offset != expected_size or checksum.hexdigest() != expected_hash + or file_identity(before) != file_identity(after) + or file_identity(after) != file_identity(after_path) + or after.st_uid != os.getuid() or after.st_mode & 0o077 + or after.st_nlink != 1 or not stat.S_ISREG(after.st_mode)): + raise SafetyError("capture_read_target_changed_or_hash") + finally: + if owned_identity is not None: + current = os.fstat(fd) + if ((current.st_dev, current.st_ino, stat.S_IFMT(current.st_mode), current.st_uid) != owned_identity + or fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_ACCMODE != os.O_RDONLY): + raise SafetyError("capture_read_target_close_unproven") + os.close(fd) + + +def validate_ready(value: object, pid: int) -> dict: + row = exact_keys(value, {"type", "version", "pid", "limits", "audit_installed"}) + integer(pid, minimum=1) + if row["type"] != "ready" or type(row["version"]) is not int or row["version"] != 1 or type(row["pid"]) is not int or row["pid"] != pid or row["audit_installed"] is not True: + raise SafetyError("limits_ready_mismatch") + expected = {"cpu": [2, 3], "core": [0, 0], "nofile": [64, 64], "fsize": [1048576, 1048576]} + limits = exact_keys(row["limits"], set(expected)) + for name, pair in limits.items(): + if type(pair) is not list or len(pair) != 2 or any(type(number) is not int for number in pair) or pair != expected[name]: + raise SafetyError("limits_ready_mismatch") + return row + + +def validate_result(value: object, case: dict, sources: dict) -> dict: + row = exact_keys(value, {"type", "version", "case", "passed", "proof_scope", "import_mode", "sources", "details"}) + if row["type"] != "result" or type(row["version"]) is not int or row["version"] != 1 or row["case"] != case["id"] or row["passed"] is not True or row["proof_scope"] != PROOF_SCOPE or row["import_mode"] != "isolated_source_module" or row["sources"] != sources or type(row["details"]) is not dict: + raise SafetyError("result_scope_mismatch") + return row + + +def validate_attempt_version(value: object) -> str: + if type(value) is not str or _ATTEMPT.fullmatch(value) is None: + raise SafetyError("invalid_attempt_version") + return value + + +def evidence_paths(manifest_path: Path) -> tuple[Path, str]: + name = manifest_path.name + if manifest_path.parent != RUN_ROOT or not name.startswith("manifest-") or not name.endswith(".json"): + raise SafetyError("unexpected_manifest_path") + version = validate_attempt_version(name[len("manifest-"):-len(".json")]) + return RUN_ROOT / ("results-" + version + ".json"), version + + +class MutationPolicy: + def __init__(self, case: dict) -> None: + self.case = case + self.account = account_directory(case) + self.directory_paths = {self.account, self.account.parent, *(self.account / name for name in ("captures", "receipts", "logs"))} + self.fixed = {self.account / "state.lock": "Completion account lock", self.account / "state.json": "Atomic completion state"} + if case["id"] not in {"flock-a4", "parser-a5"}: + self.fixed[self.account / "captures" / (digest(encoded(capture_metadata(case))) + ".jsonl")] = "Immutable synthetic source capture" + if case["id"] in {"negative-a3", "parser-a5"}: + self.directory_paths = set() + self.fixed = {} + elif case["id"] == "invalid-state": + self.fixed = {self.account / "state.lock": "Completion account lock"} + elif case["id"] == "flock-a4": + self.directory_paths = set() + self.fixed = {RUN_ROOT / "locks/contention-v1.lock": "Native lock contention witness"} + self.pending: set[Path] = set() + self.sequence = 0 + + def decide(self, value: object) -> dict: + row = exact_keys(value, {"type", "version", "seq", "event", "path", "source", "flags"}) + if row["type"] != "mutation" or type(row["version"]) is not int or row["version"] != 1: + raise SafetyError("mutation_shape") + seq = integer(row["seq"], minimum=1, maximum=LIMITS["events"]) + if seq != self.sequence + 1: + raise SafetyError("mutation_sequence") + path = canonical_path(row["path"], RUN_ROOT) + flags = integer(row["flags"], maximum=(1 << 31) - 1) + purpose = self.fixed.get(path) + if row["event"] == "os.mkdir": + if row["source"] is not None or path not in self.directory_paths or flags != 0o700: + raise SafetyError("unplanned_directory") + purpose = "Private completion directory" + elif row["event"] == "open" and flags & getattr(os, "O_DIRECTORY", 0): + expected = os.O_RDONLY | os.O_DIRECTORY + if (row["source"] is not None or self.case["id"] not in {"capture-a1", "capture-a2", "capture-revision"} + or path not in {self.account, self.account / "captures"} + or flags & ~getattr(os, "O_CLOEXEC", 0) != expected): + raise SafetyError("unplanned_directory_sync") + purpose = "Sync exact retained private capture directory" + elif row["event"] == "open" and flags & os.O_ACCMODE == os.O_RDONLY and not flags & os.O_CREAT: + allowed = {os.O_RDONLY, os.O_RDONLY | os.O_NOFOLLOW} + if (row["source"] is not None or path not in capture_read_catalog(self.case) + or flags & ~getattr(os, "O_CLOEXEC", 0) not in allowed): + raise SafetyError("unplanned_capture_read") + purpose = "Read exact retained synthetic capture with fresh identity and hash proof" + elif row["event"] == "open": + if row["source"] is not None or flags & os.O_TRUNC: + raise SafetyError("unplanned_open") + access = flags & os.O_ACCMODE + extras = flags & ~(os.O_ACCMODE | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW | getattr(os, "O_CLOEXEC", 0)) + if extras or access not in (os.O_WRONLY, os.O_RDWR) or not flags & os.O_CREAT: + raise SafetyError("unplanned_open_flags") + if path.parent == self.account and _PENDING.fullmatch(path.name): + if self.case["id"] not in {"capture-a1", "capture-a2", "capture-revision"} or access != os.O_WRONLY or not flags & os.O_EXCL or len(self.pending | {path}) > 1: + raise SafetyError("pending_slot_limit") + self.pending.add(path) + purpose = "Atomic completion-state temporary; failed evidence retained" + elif path not in self.fixed or path.name == "state.json": + raise SafetyError("unplanned_file") + elif path.name == "state.lock" or path == RUN_ROOT / "locks/contention-v1.lock": + if access != os.O_RDWR or flags & os.O_EXCL: + raise SafetyError("lock_flags") + elif access != os.O_WRONLY or not flags & os.O_EXCL: + raise SafetyError("capture_flags") + elif row["event"] == "os.rename": + source = canonical_path(row["source"], RUN_ROOT) + if flags != 0 or path != self.account / "state.json" or source not in self.pending: + raise SafetyError("unplanned_replace") + purpose = "Publish current-run completion state from its approved temporary" + else: + raise SafetyError("unsupported_mutation") + self.sequence = seq + return {"type": "grant", "version": 1, "seq": seq, "path": str(path), "purpose": purpose} + + +def mutation_target(grant: dict, request: dict) -> None: + path = Path(grant["path"]) + owned_path(path.parent, directory=True) + if request["event"] == "open" and request["flags"] & getattr(os, "O_DIRECTORY", 0): + owned_path(path, directory=True) + return + if path.exists() or path.is_symlink(): + owned_path(path, directory=request["event"] == "os.mkdir") + if request["event"] == "os.rename": + owned_path(Path(request["source"])) + + +class PressureReader: + def __init__(self) -> None: + import ctypes + self.ctypes = ctypes + self.library = ctypes.CDLL("/usr/lib/libSystem.B.dylib", use_errno=True) + self.read = self.library.sysctlbyname + self.read.argtypes = [ctypes.c_char_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.c_void_p, ctypes.c_size_t] + self.read.restype = ctypes.c_int + + def pressure(self) -> int: + c = self.ctypes + output = c.c_uint32() + size = c.c_size_t(c.sizeof(output)) + if self.read(b"kern.memorystatus_vm_pressure_level", c.byref(output), c.byref(size), None, 0) != 0 or size.value != 4: + raise SafetyError("pressure_unavailable") + if output.value not in (1, 2, 4): + raise SafetyError("pressure_abi_unknown") + return output.value + + +class PinnedPythonLoader(importlib.abc.Loader): + def __init__(self, path: Path, checksum: str) -> None: + self.path, self.checksum = path, checksum + + def create_module(self, spec: object) -> None: + return None + + def exec_module(self, module: ModuleType) -> None: + data = read_bounded(self.path, 262144, private=False) + if digest(data) != self.checksum: + raise SafetyError("dependency_hash_changed") + exec(compile(data, str(self.path), "exec", dont_inherit=True), module.__dict__) + + +class PinnedPsutilFinder(importlib.abc.MetaPathFinder): + def __init__(self, packet: dict) -> None: + self.root = Path(packet["dependency_root"]) + self.files = packet["dependency_files"] + + def find_spec(self, fullname: str, path: object = None, target: object = None) -> object: + if fullname != "psutil" and not fullname.startswith("psutil."): + return None + relative = fullname.split(".")[1:] + candidate = self.root.joinpath(*relative) if relative else self.root + source = candidate / "__init__.py" if not relative else candidate.with_suffix(".py") + entry = self.files.get(str(source)) + if entry is not None: + return importlib.util.spec_from_file_location(fullname, source, loader=PinnedPythonLoader(source, entry["sha256"]), submodule_search_locations=[str(self.root)] if not relative else None) + for suffix in importlib.machinery.EXTENSION_SUFFIXES: + extension = Path(str(candidate) + suffix) + entry = self.files.get(str(extension)) + if entry is not None: + if digest(read_bounded(extension, 262144, private=False)) != entry["sha256"]: + raise SafetyError("dependency_extension_changed") + return importlib.util.spec_from_file_location(fullname, extension) + raise SafetyError("unpinned_dependency_module") + + +def pinned_runtime() -> ModuleType: + raw = read_bounded(RUNTIME_PACKET, 65536, private=False) + if digest(raw) != RUNTIME_PACKET_SHA: + raise SafetyError("runtime_packet_changed") + packet = json.loads(raw) + if Path(sys.executable).resolve() != Path(packet["python_realpath"]) or sys.version_info[:3] != (3, 12, 12): + raise SafetyError("interpreter_mismatch") + if digest(read_bounded(Path(packet["python_realpath"]), 1048576, private=False)) != packet["python_executable_sha256"]: + raise SafetyError("interpreter_hash_changed") + if "psutil" in sys.modules: + raise SafetyError("dependency_preloaded") + for name, entry in packet["dependency_files"].items(): + if digest(read_bounded(Path(name), 262144, private=False)) != entry["sha256"]: + raise SafetyError("dependency_hash_changed") + sys.meta_path.insert(0, PinnedPsutilFinder(packet)) + import psutil + if psutil.__version__ != packet["psutil_version"]: + raise SafetyError("dependency_version_changed") + return psutil + + +def sample(psutil: ModuleType, pressure: PressureReader) -> Sensor: + started = time.monotonic_ns() + row = Sensor(started, pressure.pressure(), psutil.virtual_memory().free, psutil.swap_memory().used) + valid_sensor(asdict(row)) + if time.monotonic_ns() - started > 500_000_000: + raise SafetyError("sensor_call_stale") + return row + + +def observe_identity(psutil: ModuleType, pid: int) -> Identity: + process = psutil.Process(pid) + born = process.create_time() + try: + valid = type(born) in (int, float) and math.isfinite(born) and born > 0 + except (OverflowError, ValueError): + valid = False + if not valid: + raise SafetyError("birth_unavailable") + return Identity(pid, process.uids().real, int(born * 1_000_000_000), os.getpgid(pid)) + + +def announce(path: Path, purpose: str) -> None: + global _announcement_bytes + line = f"Create {path}: {purpose}\n" + _announcement_bytes += len(line.encode("utf-8")) + if _announcement_bytes > 65536: + raise SafetyError("announcement_byte_limit") + sys.stdout.write(line) + sys.stdout.flush() + + +def new_log(path: Path, purpose: str) -> int: + owned_path(path.parent, directory=True) + announce(path, purpose) + return os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) + + +def write_all(fd: int, data: bytes) -> None: + while data: + count = os.write(fd, data) + if count <= 0: + raise SafetyError("write_no_progress") + data = data[count:] + + +def write_pipe(fd: int, data: bytes, deadline: float) -> None: + if len(data) > LIMITS["input_bytes"]: + raise SafetyError("pipe_frame_input_limit") + while data: + remaining = deadline - time.monotonic() + if remaining <= 0: + raise SafetyError("pipe_write_deadline") + try: + count = os.write(fd, data) + except BlockingIOError: + select.select([], [fd], [], min(remaining, 0.01)) + continue + if count <= 0: + raise SafetyError("pipe_write_no_progress") + data = data[count:] + + +def group_gone(pgid: int) -> bool: + integer(pgid, minimum=1) + try: + os.killpg(pgid, 0) + except ProcessLookupError: + return True + except OSError: + return False + return False + + +def member_identity_matches(expected: Identity, actual: Identity, group: int, worker_uid: int) -> bool: + try: + for row in (expected, actual): + integer(row.pid, minimum=1) + integer(row.uid) + integer(row.birth_ns, minimum=1) + integer(row.pgid, minimum=1) + integer(group, minimum=1) + integer(worker_uid) + except SafetyError: + return False + return expected == actual and actual.uid == worker_uid and actual.pgid == group + + +def terminate_owned(child: subprocess.Popen, expected: Identity | None, members: list[Identity], psutil: ModuleType, result: dict) -> None: + deadline = time.monotonic() + LIMITS["termination_seconds"] + for sig, interval in ((signal.SIGTERM, 0.25), (signal.SIGKILL, 0.75)): + if time.monotonic() >= deadline: + raise SafetyError("termination_deadline") + if child.poll() is None: + if expected is None: + # Popen still owns an unreaped direct child; its PID cannot be reused. + child.send_signal(sig) + result["termination_authority"] = "unreaped_direct_popen_child" + elif same_owned_identity(expected, observe_identity(psutil, child.pid), os.getuid()): + if result["descendant_containment"] == "unproven": + os.killpg(expected.pgid, sig) + result["termination_authority"] = "fresh_owned_leader_group" + else: + child.send_signal(sig) + result["termination_authority"] = "fresh_owned_direct_child" + else: + raise SafetyError("termination_identity_changed") + if sig == signal.SIGKILL and members and expected is not None: + for member in members: + if time.monotonic() >= deadline: + raise SafetyError("member_termination_deadline") + try: + actual = observe_identity(psutil, member.pid) + except psutil.NoSuchProcess: + continue + if not member_identity_matches(member, actual, expected.pgid, os.getuid()): + raise SafetyError("member_termination_identity_changed") + psutil.Process(member.pid).send_signal(sig) + if not result["reaped"]: + try: + child.wait(timeout=max(0.001, min(interval, deadline - time.monotonic()))) + result["reaped"] = True + except subprocess.TimeoutExpired: + pass + if result["reaped"] and not members: + break + result["exit_code"] = child.returncode + if not result["reaped"]: + raise SafetyError("direct_child_reap_unproven") + if result["descendant_containment"] == "unproven": + if expected is None or len(result["descendants"]) != len(members): + raise SafetyError("member_inventory_unproven") + terminal = True + for member in members: + if time.monotonic() >= deadline: + raise SafetyError("containment_proof_deadline") + try: + actual = observe_identity(psutil, member.pid) + except psutil.NoSuchProcess: + continue + if member_identity_matches(member, actual, expected.pgid, os.getuid()): + terminal = False + result["descendant_containment"] = "proven_terminal" if terminal and group_gone(expected.pgid) else "unproven" + if result["descendant_containment"] != "proven_terminal": + raise SafetyError("owned_group_containment_unproven") + + +def execute_case(manifest: dict, case: dict, psutil: ModuleType, pressure: PressureReader, attempt_version: str = "v1") -> dict: + attempt_version = validate_attempt_version(attempt_version) + bootstrap = SOURCE_ROOT / "tests/native/mac-native-bootstrap-v6.py" + policy = MutationPolicy(case) + records: list[Sensor] = [] + result = {"case": case["id"], "implemented": True, "executed": False, "passed": False, + "blocked": None, "unproven": ["installed_package", "installed_host", "models", "physical_hard_cap"], + "proof_scope": PROOF_SCOPE, "import_mode": "isolated_source_module", "synthetic": True, + "source_commit": manifest["source_commit"], "sources": manifest["sources"], + "sensors": [asdict(row) for row in records], "rss_sampled_peak": 0, "limits": LIMITS, + "reaped": False, "exit_code": None, "identity": None, "output_bytes": 0, + "descendants": [], "descendant_containment": "not_observed"} + logs: dict[str, int] = {} + child = None + expected = None + members: list[Identity] = [] + selector = selectors.DefaultSelector() + held_lock = None + frames = bytearray() + protocol_result = None + start = time.monotonic() + try: + records = [] + for index in range(3): + records.append(sample(psutil, pressure)) + if index < 2: + time.sleep(1) + admission(records, time.monotonic_ns()) + if case["source"] is not None: + source = Path(case["source"]) + info = owned_path(source) + raw = read_bounded(source, 4096) + if file_identity(info) != case["source_identity"] or digest(raw) != case["source_sha256"] or len(raw) != case["source_bytes"]: + raise SafetyError("fixture_changed_before_launch") + for name, checksum in manifest["sources"].items(): + if digest(read_bounded(SOURCE_ROOT / name, 262144, private=False)) != checksum: + raise SafetyError("source_hash_changed") + if digest(read_bounded(bootstrap, 262144, private=False)) != manifest["bootstrap_sha256"]: + raise SafetyError("bootstrap_hash_changed") + result["sensors"] = [asdict(row) for row in records] + for channel in ("stdout", "stderr", "events"): + logs[channel] = new_log(RUN_ROOT / "logs" / (case["id"] + "-" + channel + "-" + attempt_version + ".log"), "Retained native " + channel + " evidence") + if case["id"] == "flock-a4": + import fcntl + lock = RUN_ROOT / "locks/contention-v1.lock" + owned_path(lock) + held_lock = os.open(lock, os.O_RDWR | os.O_NOFOLLOW) + fcntl.flock(held_lock, fcntl.LOCK_EX | fcntl.LOCK_NB) + env = {"HOME": manifest["home"], "PATH": "/usr/bin:/bin", "LANG": "C.UTF-8", + "OMP_NUM_THREADS": "1", "OPENBLAS_NUM_THREADS": "1", "MKL_NUM_THREADS": "1", + "VECLIB_MAXIMUM_THREADS": "1", "NUMEXPR_NUM_THREADS": "1"} + request = encoded({"version": 1, "source_root": str(SOURCE_ROOT), "run_root": str(RUN_ROOT), + "home": manifest["home"], "sources": manifest["sources"], "limits": LIMITS, "case": case, + "supervisor_sha256": manifest["supervisor_sha256"]}) + b"\n" + if len(request) > LIMITS["input_bytes"] - LIMITS["events"] * 256: + raise SafetyError("input_limit") + fresh = sample(psutil, pressure) + admission([*records[:-1], fresh], time.monotonic_ns()) + result["sensors"].append(asdict(fresh)) + start = time.monotonic() + child = subprocess.Popen([sys.executable, "-I", "-S", "-B", str(bootstrap)], stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, + cwd=str(RUN_ROOT), close_fds=True, start_new_session=True) + result["executed"] = True + expected = observe_identity(psutil, child.pid) + if not same_owned_identity(expected, expected, os.getuid()): + raise SafetyError("child_identity_unowned") + result["identity"] = asdict(expected) + assert child.stdin is not None and child.stdout is not None and child.stderr is not None + for stream, channel in ((child.stdout, "stdout"), (child.stderr, "stderr")): + os.set_blocking(stream.fileno(), False) + selector.register(stream, selectors.EVENT_READ, channel) + os.set_blocking(child.stdin.fileno(), False) + write_pipe(child.stdin.fileno(), request, min(start + LIMITS["wall_seconds"], time.monotonic() + 0.1)) + input_count = len(request) + previous = fresh + ready = False + next_sample = time.monotonic() + while selector.get_map(): + now = time.monotonic() + if now - start >= LIMITS["wall_seconds"]: + raise SafetyError("child_wall_deadline") + if child.poll() is None and now >= next_sample: + current = sample(psutil, pressure) + if current.pressure != 1 or current.free_bytes < FREE_MIN or current.swap_used_bytes > previous.swap_used_bytes or time.monotonic_ns() - current.monotonic_ns > 500_000_000: + raise SafetyError("during_child_sensor_refusal") + actual = observe_identity(psutil, child.pid) + if not same_owned_identity(expected, actual, os.getuid()): + raise SafetyError("child_identity_changed") + process = psutil.Process(child.pid) + descendants = process.children(recursive=True) + if descendants: + result["descendants"] = [item.pid for item in descendants[:32]] + result["descendant_containment"] = "unproven" + if len(descendants) > 32: + raise SafetyError("descendant_inventory_limit") + members = [observe_identity(psutil, item.pid) for item in descendants] + if any(member.uid != os.getuid() or member.pgid != expected.pgid for member in members): + raise SafetyError("descendant_identity_unowned") + raise SafetyError("unexpected_descendant") + rss = integer(process.memory_info().rss) + result["rss_sampled_peak"] = max(result["rss_sampled_peak"], rss) + if rss > LIMITS["rss_abort_bytes"]: + raise SafetyError("sampled_rss_limit") + if len(result["sensors"]) >= 64: + raise SafetyError("sensor_inventory_limit") + result["sensors"].append(asdict(current)) + previous = current + next_sample = now + 0.1 + for key, _ in selector.select(0.01): + try: + chunk = os.read(key.fileobj.fileno(), 4096) + except BlockingIOError: + continue + if not chunk: + selector.unregister(key.fileobj) + continue + result["output_bytes"] += len(chunk) + if result["output_bytes"] > LIMITS["output_bytes"]: + raise SafetyError("combined_output_limit") + channel = key.data + write_all(logs[channel], chunk) + if channel != "stdout": + continue + frames.extend(chunk) + while b"\n" in frames: + line, _, rest = frames.partition(b"\n") + frames = bytearray(rest) + if len(line) > LIMITS["frame_bytes"]: + raise SafetyError("protocol_frame_limit") + frame_deadline = min(start + LIMITS["wall_seconds"], time.monotonic() + 0.1) + frame = json.loads(line) + if type(frame) is not dict: + raise SafetyError("protocol_object_required") + if frame.get("type") == "mutation": + if not ready or protocol_result is not None: + raise SafetyError("mutation_outside_ready_execution") + try: + grant = policy.decide(frame) + mutation_target(grant, frame) + if frame["event"] == "open" and frame["flags"] & os.O_ACCMODE == os.O_RDONLY and not frame["flags"] & getattr(os, "O_DIRECTORY", 0): + verify_capture_read_target(case, Path(grant["path"])) + except (SafetyError, OSError, ValueError) as error: + write_all(logs["events"], encoded({"request": frame, "decision": {"type": "deny", "reason": type(error).__name__ + ":" + str(error)[:256]}}) + b"\n") + os.fsync(logs["events"]) + raise + write_all(logs["events"], encoded({"request": frame, "decision": grant}) + b"\n") + os.fsync(logs["events"]) + announce(Path(grant["path"]), grant["purpose"]) + reply = encoded(grant) + b"\n" + input_count += len(reply) + if input_count > LIMITS["input_bytes"]: + raise SafetyError("input_limit") + write_pipe(child.stdin.fileno(), reply, frame_deadline) + elif frame == {"type": "lock_busy"} and ready and protocol_result is None and case["id"] == "flock-a4" and held_lock is not None: + os.close(held_lock) + held_lock = None + reply = b'{"type":"lock_released"}\n' + input_count += len(reply) + if input_count > LIMITS["input_bytes"]: + raise SafetyError("input_limit") + write_pipe(child.stdin.fileno(), reply, frame_deadline) + elif frame.get("type") == "result" and protocol_result is None: + if not ready: + raise SafetyError("result_before_ready") + protocol_result = validate_result(frame, case, manifest["sources"]) + elif frame.get("type") == "ready" and not ready: + validate_ready(frame, child.pid) + ready = True + result["limit_readbacks"] = frame["limits"] + else: + raise SafetyError("unexpected_protocol_frame") + if len(frames) > LIMITS["frame_bytes"]: + raise SafetyError("protocol_frame_limit") + if frames: + raise SafetyError("truncated_protocol_frame") + remaining = max(0.001, start + LIMITS["wall_seconds"] - time.monotonic()) + result["exit_code"] = child.wait(timeout=remaining) + result["reaped"] = True + expected_exit = 73 if case["id"] == "flock-a4" else 0 + if result["exit_code"] != expected_exit or type(protocol_result) is not dict or protocol_result.get("passed") is not True or protocol_result.get("proof_scope") != PROOF_SCOPE: + raise SafetyError("case_nonterminal_or_failed") + if case["id"] == "flock-a4": + lock_fd = os.open(RUN_ROOT / "locks/contention-v1.lock", os.O_RDWR | os.O_NOFOLLOW) + try: + fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + finally: + os.close(lock_fd) + if not group_gone(expected.pgid): + result["descendant_containment"] = "unproven" + raise SafetyError("post_exit_group_containment_unproven") + result["passed"] = True + result["child_result"] = protocol_result + except (SafetyError, OSError, ValueError, subprocess.SubprocessError, psutil.Error) as error: + result["blocked"] = type(error).__name__ + ":" + str(error)[:256] + finally: + if not result["sensors"]: + result["sensors"] = [asdict(row) for row in records] + if child is None: + result["reaped"] = True + if held_lock is not None: + try: + os.close(held_lock) + except OSError as error: + result["blocked"] = "lock_close_failed:" + str(error)[:256] + result["passed"] = False + if child is not None and (not result["reaped"] or result["descendant_containment"] == "unproven"): + try: + terminate_owned(child, expected, members, psutil, result) + except (SafetyError, OSError, ValueError, subprocess.SubprocessError, psutil.Error) as error: + result["blocked"] = "identity_or_reap_unproven:" + str(error)[:256] + result["passed"] = False + result["wall_seconds"] = time.monotonic() - start + try: + selector.close() + except OSError as error: + result["blocked"] = "selector_close_failed:" + str(error)[:256] + result["passed"] = False + if child is not None: + for stream in (child.stdin, child.stdout, child.stderr): + if stream is not None: + try: + stream.close() + except OSError as error: + result["blocked"] = "pipe_close_failed:" + str(error)[:256] + result["passed"] = False + for fd in logs.values(): + try: + os.fsync(fd) + except OSError as error: + result["blocked"] = "evidence_sync_failed:" + str(error)[:256] + result["passed"] = False + finally: + try: + os.close(fd) + except OSError as error: + result["blocked"] = "evidence_close_failed:" + str(error)[:256] + result["passed"] = False + return result + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--manifest", required=True) + parser.add_argument("--execute-root-authorized", action="store_true") + args = parser.parse_args() + if not args.execute_root_authorized or sys.platform != "darwin" or not sys.flags.isolated or not sys.flags.no_site or not sys.flags.dont_write_bytecode: + raise SafetyError("root_isolated_native_execution_required") + sys.dont_write_bytecode = True + os.umask(0o077) + manifest_path = canonical_path(args.manifest, RUN_ROOT) + target, version = evidence_paths(manifest_path) + owned_path(RUN_ROOT, directory=True) + owned_path(RUN_ROOT / "home", directory=True) + manifest_bytes = read_bounded(manifest_path, 65536) + manifest = validate_manifest(json.loads(manifest_bytes)) + if (version != "v2" and any(case["id"] == "capture-revision" for case in manifest["cases"])) or (version == "v2" and [case["id"] for case in manifest["cases"]] != ["capture-revision"]): + raise SafetyError("unexpected_manifest_case_stage") + if digest(read_bounded(Path(__file__).resolve(), 262144, private=False)) != manifest["supervisor_sha256"]: + raise SafetyError("supervisor_hash_changed") + results: list[dict] = [] + setup_failure = None + # Reserve before runtime admission so a retained refusal cannot be replayed. + fd = new_log(target, "Retained Stage A results; native host/model/hard-cap acceptance remains unproven") + try: + try: + psutil = pinned_runtime() + pressure = PressureReader() + for case in manifest["cases"]: + result = execute_case(manifest, case, psutil, pressure, attempt_version=version) + results.append(result) + if not result["passed"] or not result["reaped"]: + break + except (SafetyError, OSError, ValueError, ImportError) as error: + setup_failure = type(error).__name__ + ":" + str(error)[:256] + data = encoded({"version": 1, "manifest_version": version, "manifest_sha256": digest(manifest_bytes), + "proof_scope": PROOF_SCOPE, "cases": results, "setup_failure": setup_failure, + "complete": setup_failure is None and len(results) == len(manifest["cases"]) and all(item["passed"] for item in results)}) + b"\n" + if len(data) > LIMITS["output_bytes"]: + raise SafetyError("results_byte_limit") + write_all(fd, data) + finally: + try: + os.fsync(fd) + finally: + os.close(fd) + return 0 if setup_failure is None and len(results) == len(manifest["cases"]) and all(item["passed"] for item in results) else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/native/mac-native-acceptance-v7.py b/tests/native/mac-native-acceptance-v7.py new file mode 100644 index 00000000..16a69af3 --- /dev/null +++ b/tests/native/mac-native-acceptance-v7.py @@ -0,0 +1,966 @@ +"""ROOT-run Stage A supervisor. Importing this module never starts native work. + +This instrumented source-module harness proves tiny filesystem/lock behavior. +It does not certify installed packages, host dispatch, models or a physical cap. +""" +from __future__ import annotations + +import argparse +import hashlib +import importlib.abc +import importlib.machinery +import importlib.util +import json +import math +import os +import re +import select +import selectors +import signal +import stat +import subprocess +import sys +import time +from dataclasses import asdict, dataclass +from pathlib import Path +from types import ModuleType + +RUN_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/native-mac-v1/run-0003") +SOURCE_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/worktrees/mac-native-acceptance-v1") +RUNTIME_PACKET = Path("/Users/j/dev/truememory-maintenance-v1/.work/reports/mac-native-runtime-pins-v1.json") +RUNTIME_PACKET_SHA = "8db57e3f700aebb3adeb383c719a4bc5e2525917f4c017c853ee4694be1ee1e4" +PROOF_SCOPE = "instrumented_unmodified_source_native_filesystem_lock_capture" +SOURCE_FILES = ( + "truememory/_platform.py", "truememory/ingest/hooks/_shared.py", + "truememory/ingest/completion.py", "truememory/ingest/transcript.py", + "truememory/ingest/provenance.py", +) +CASES = frozenset({"capture-a1", "capture-a2", "capture-revision", "negative-a3", "invalid-state", "flock-a4", "parser-a5"}) +LIMITS = {"wall_seconds": 5, "termination_seconds": 1, "cpu_soft": 2, "cpu_hard": 3, + "rss_abort_bytes": 67108864, "core_bytes": 0, "nofile": 64, + "fsize_bytes": 1048576, "input_bytes": 65536, "output_bytes": 262144, + "events": 32, "frame_bytes": 2048, "ack_ms": 100, "threads": 1} +FREE_MIN = 268435456 +MAX_INT = (1 << 63) - 1 +_SHA = re.compile(r"[0-9a-f]{64}\Z") +_PENDING = re.compile(r"state-[0-9a-f]{32}\.pending\Z") +_ATTEMPT = re.compile(r"v(?:[1-9]|[1-9][0-9])\Z") +_announcement_bytes = 0 + + +class SafetyError(ValueError): + pass + + +def integer(value: object, *, minimum: int = 0, maximum: int = MAX_INT) -> int: + if type(value) is not int or not minimum <= value <= maximum: + raise SafetyError("invalid_integer") + return value + + +def exact_keys(value: object, keys: set[str]) -> dict: + if type(value) is not dict or set(value) != keys: + raise SafetyError("invalid_fields") + return value + + +def encoded(value: object) -> bytes: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8") + + +def digest(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def hash_text(value: object) -> str: + if type(value) is not str or _SHA.fullmatch(value) is None: + raise SafetyError("invalid_hash") + return value + + +def canonical_path(value: object, root: Path) -> Path: + if type(value) is not str or not value or "\0" in value or len(value.encode("utf-8")) > 1024: + raise SafetyError("invalid_path") + path = Path(value) + if not path.is_absolute() or ".." in path.parts or str(path) != value: + raise SafetyError("noncanonical_path") + if path != root and not path.is_relative_to(root): + raise SafetyError("path_escape") + return path + + +def owned_path(path: Path, *, directory: bool = False) -> os.stat_result: + if path.resolve() != path or any(item.is_symlink() for item in (path, *path.parents)): + raise SafetyError("path_link") + info = path.lstat() + if info.st_uid != os.getuid() or info.st_mode & 0o077: + raise SafetyError("path_not_private_owned") + if directory: + if not stat.S_ISDIR(info.st_mode): + raise SafetyError("path_not_directory") + elif not stat.S_ISREG(info.st_mode) or info.st_nlink != 1: + raise SafetyError("path_not_regular_single_link") + return info + + +def read_bounded(path: Path, maximum: int, *, private: bool = True) -> bytes: + if private: + owned_path(path) + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + try: + before = os.fstat(fd) + if not stat.S_ISREG(before.st_mode) or before.st_size > maximum: + raise SafetyError("read_limit_or_type") + if private and (before.st_uid != os.getuid() or before.st_mode & 0o077 or before.st_nlink != 1): + raise SafetyError("read_not_private") + chunks: list[bytes] = [] + remaining = maximum + 1 + while remaining: + chunk = os.read(fd, min(remaining, 65536)) + if not chunk: + break + chunks.append(chunk) + remaining -= len(chunk) + after = os.fstat(fd) + data = b"".join(chunks) + if file_identity(before) != file_identity(after) or len(data) > maximum or len(data) != after.st_size: + raise SafetyError("read_changed_or_limit") + return data + finally: + os.close(fd) + + +def file_identity(info: os.stat_result) -> list[int]: + return [info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns, info.st_ctime_ns] + + +@dataclass(frozen=True) +class Sensor: + monotonic_ns: int + pressure: int + free_bytes: int + swap_used_bytes: int + + +def valid_sensor(value: object) -> Sensor: + row = exact_keys(value, {"monotonic_ns", "pressure", "free_bytes", "swap_used_bytes"}) + integer(row["monotonic_ns"], minimum=1) + integer(row["pressure"], minimum=1, maximum=4) + integer(row["free_bytes"]) + integer(row["swap_used_bytes"]) + if row["pressure"] not in (1, 2, 4): + raise SafetyError("unknown_pressure_abi") + return Sensor(**row) + + +def admission(samples: list[Sensor], now_ns: int) -> None: + integer(now_ns, minimum=1) + if len(samples) < 3: + raise SafetyError("sensor_count") + previous = None + for sample in samples: + valid_sensor(asdict(sample)) + if sample.pressure != 1 or sample.free_bytes < FREE_MIN: + raise SafetyError("pressure_or_headroom") + if previous is not None and (sample.monotonic_ns <= previous.monotonic_ns or sample.swap_used_bytes > previous.swap_used_bytes): + raise SafetyError("unordered_or_growing_swap") + previous = sample + if samples[-1].monotonic_ns - samples[0].monotonic_ns < 2_000_000_000: + raise SafetyError("sensor_window") + if not 0 <= now_ns - samples[-1].monotonic_ns <= 500_000_000: + raise SafetyError("sensor_stale") + + +@dataclass(frozen=True) +class Identity: + pid: int + uid: int + birth_ns: int + pgid: int + + +def same_owned_identity(expected: Identity, actual: Identity, worker_uid: int) -> bool: + try: + for row in (expected, actual): + integer(row.pid, minimum=1) + integer(row.uid) + integer(row.birth_ns, minimum=1) + integer(row.pgid, minimum=1) + integer(worker_uid) + except SafetyError: + return False + return expected == actual and actual.uid == worker_uid and actual.pgid == actual.pid + + +def account_directory(case: dict) -> Path: + key = digest(encoded(["native-acceptance-v1", case["database"]])) + return RUN_ROOT / "completion" / key + + +def capture_metadata(case: dict) -> dict: + return {"version": 1, "user_id": "native-acceptance-v1", "database": case["database"], + "host": "claude", "session_id": case["session"], "source": case["source"], + "source_suffix": ".jsonl", "file_identity": case["source_identity"], + "byte_count": case["source_bytes"], "sha256": case["source_sha256"]} + + +def validate_case(value: object) -> dict: + row = exact_keys(value, {"id", "source", "source_sha256", "source_bytes", "source_identity", "session", "database", "previous"}) + if type(row["id"]) is not str or row["id"] not in CASES: + raise SafetyError("unsupported_case") + base = RUN_ROOT / "home/.claude/projects/native-acceptance-v1" + filename = {"capture-revision": "capture-a2", "invalid-state": "negative-a3"}.get(row["id"], row["id"]) + if row["id"] != "flock-a4": + if canonical_path(row["source"], RUN_ROOT) != base / (filename + ".jsonl"): + raise SafetyError("unexpected_fixture") + hash_text(row["source_sha256"]) + integer(row["source_bytes"], minimum=1, maximum=4096) + if type(row["source_identity"]) is not list or len(row["source_identity"]) != 5: + raise SafetyError("fixture_identity") + for number in row["source_identity"]: + integer(number) + if row["source_identity"][2] != row["source_bytes"]: + raise SafetyError("fixture_size_identity") + elif any(row[name] is not None for name in ("source", "source_sha256", "source_bytes", "source_identity")): + raise SafetyError("unexpected_lock_fixture") + if row["session"] != "synthetic-native-" + filename: + raise SafetyError("unexpected_session") + database_case = "capture-a2" if row["id"] == "capture-revision" else "negative-a3" if row["id"] == "invalid-state" else row["id"] + if row["database"] != str(RUN_ROOT / (database_case + "-v1.db")): + raise SafetyError("unexpected_database_locator") + if row["id"] == "capture-revision": + old = exact_keys(row["previous"], {"key", "sha256", "byte_count"}) + hash_text(old["key"]) + hash_text(old["sha256"]) + integer(old["byte_count"], minimum=1, maximum=4096) + if old["key"] == digest(encoded(capture_metadata(row))): + raise SafetyError("revision_not_new_evidence") + elif row["previous"] is not None: + raise SafetyError("unexpected_previous") + return row + + +def validate_manifest(value: object) -> dict: + row = exact_keys(value, {"version", "stage", "synthetic", "root_execution_go", "run_root", "home", "source_root", "source_commit", "sources", "supervisor_sha256", "bootstrap_sha256", "limits", "cases"}) + if type(row["version"]) is not int or row["version"] != 1 or row["stage"] != "A" or row["synthetic"] is not True or row["root_execution_go"] is not True: + raise SafetyError("manifest_authority_or_version") + if row["run_root"] != str(RUN_ROOT) or row["home"] != str(RUN_ROOT / "home") or row["source_root"] != str(SOURCE_ROOT): + raise SafetyError("unexpected_root") + if type(row["source_commit"]) is not str or re.fullmatch(r"[0-9a-f]{40}", row["source_commit"]) is None: + raise SafetyError("source_commit") + sources = exact_keys(row["sources"], set(SOURCE_FILES)) + for value in sources.values(): + hash_text(value) + hash_text(row["supervisor_sha256"]) + hash_text(row["bootstrap_sha256"]) + if type(row["limits"]) is not dict or row["limits"] != LIMITS or any(type(v) is not int for v in row["limits"].values()): + raise SafetyError("limit_change") + if type(row["cases"]) is not list or not 1 <= len(row["cases"]) <= len(CASES): + raise SafetyError("case_count") + cases = [validate_case(case) for case in row["cases"]] + if len({case["id"] for case in cases}) != len(cases): + raise SafetyError("duplicate_case") + return row + + +def capture_read_catalog(case: dict) -> dict[Path, tuple[int, str]]: + if case["id"] not in {"capture-a1", "capture-a2", "capture-revision"}: + return {} + validate_case(case) + directory = account_directory(case) / "captures" + current = directory / (digest(encoded(capture_metadata(case))) + ".jsonl") + catalog = {current: (case["source_bytes"], case["source_sha256"])} + if case["id"] == "capture-revision": + previous = case["previous"] + catalog[directory / (previous["key"] + ".jsonl")] = (previous["byte_count"], previous["sha256"]) + if len(catalog) > 2: + raise SafetyError("capture_read_catalog_limit") + return catalog + + +def verify_capture_read_target(case: dict, path: Path) -> None: + import fcntl + catalog = capture_read_catalog(case) + if path not in catalog: + raise SafetyError("unplanned_capture_read") + expected_size, expected_hash = catalog[path] + owned_path(path.parent.parent, directory=True) + owned_path(path.parent, directory=True) + before_path = owned_path(path) + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + owned_identity: tuple[int, int, int, int] | None = None + try: + if type(fd) is not int or fd < 3: + raise SafetyError("capture_read_target_descriptor_type") + before = os.fstat(fd) + if (not stat.S_ISREG(before.st_mode) or before.st_uid != os.getuid() + or before.st_mode & 0o077 or before.st_nlink != 1 + or file_identity(before) != file_identity(before_path) + or fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_ACCMODE != os.O_RDONLY): + raise SafetyError("capture_read_target_identity") + owned_identity = (before.st_dev, before.st_ino, stat.S_IFMT(before.st_mode), before.st_uid) + if before.st_size != expected_size: + raise SafetyError("capture_read_target_size") + checksum = hashlib.sha256() + offset = 0 + while offset <= expected_size: + chunk = os.pread(fd, expected_size + 1 - offset, offset) + if not chunk: + break + checksum.update(chunk) + offset += len(chunk) + after = os.fstat(fd) + after_path = owned_path(path) + if (offset != expected_size or checksum.hexdigest() != expected_hash + or file_identity(before) != file_identity(after) + or file_identity(after) != file_identity(after_path) + or after.st_uid != os.getuid() or after.st_mode & 0o077 + or after.st_nlink != 1 or not stat.S_ISREG(after.st_mode)): + raise SafetyError("capture_read_target_changed_or_hash") + finally: + if owned_identity is not None: + current = os.fstat(fd) + if ((current.st_dev, current.st_ino, stat.S_IFMT(current.st_mode), current.st_uid) != owned_identity + or fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_ACCMODE != os.O_RDONLY): + raise SafetyError("capture_read_target_close_unproven") + os.close(fd) + + +def validate_ready(value: object, pid: int) -> dict: + row = exact_keys(value, {"type", "version", "pid", "limits", "audit_installed"}) + integer(pid, minimum=1) + if row["type"] != "ready" or type(row["version"]) is not int or row["version"] != 1 or type(row["pid"]) is not int or row["pid"] != pid or row["audit_installed"] is not True: + raise SafetyError("limits_ready_mismatch") + expected = {"cpu": [2, 3], "core": [0, 0], "nofile": [64, 64], "fsize": [1048576, 1048576]} + limits = exact_keys(row["limits"], set(expected)) + for name, pair in limits.items(): + if type(pair) is not list or len(pair) != 2 or any(type(number) is not int for number in pair) or pair != expected[name]: + raise SafetyError("limits_ready_mismatch") + return row + + +def validate_result(value: object, case: dict, sources: dict) -> dict: + row = exact_keys(value, {"type", "version", "case", "passed", "proof_scope", "import_mode", "sources", "details"}) + if row["type"] != "result" or type(row["version"]) is not int or row["version"] != 1 or row["case"] != case["id"] or row["passed"] is not True or row["proof_scope"] != PROOF_SCOPE or row["import_mode"] != "isolated_source_module" or row["sources"] != sources or type(row["details"]) is not dict: + raise SafetyError("result_scope_mismatch") + return row + + +def validate_attempt_version(value: object) -> str: + if type(value) is not str or _ATTEMPT.fullmatch(value) is None: + raise SafetyError("invalid_attempt_version") + return value + + +def evidence_paths(manifest_path: Path) -> tuple[Path, str]: + name = manifest_path.name + if manifest_path.parent != RUN_ROOT or not name.startswith("manifest-") or not name.endswith(".json"): + raise SafetyError("unexpected_manifest_path") + version = validate_attempt_version(name[len("manifest-"):-len(".json")]) + return RUN_ROOT / ("results-" + version + ".json"), version + + +class MutationPolicy: + def __init__(self, case: dict) -> None: + self.case = case + self.account = account_directory(case) + self.directory_paths = {self.account, self.account.parent, *(self.account / name for name in ("captures", "receipts", "logs"))} + self.fixed = {self.account / "state.lock": "Completion account lock", self.account / "state.json": "Atomic completion state"} + if case["id"] not in {"flock-a4", "parser-a5"}: + self.fixed[self.account / "captures" / (digest(encoded(capture_metadata(case))) + ".jsonl")] = "Immutable synthetic source capture" + if case["id"] in {"negative-a3", "parser-a5"}: + self.directory_paths = set() + self.fixed = {} + elif case["id"] == "invalid-state": + self.fixed = {self.account / "state.lock": "Completion account lock"} + elif case["id"] == "flock-a4": + self.directory_paths = set() + self.fixed = {RUN_ROOT / "locks/contention-v1.lock": "Native lock contention witness"} + self.pending: set[Path] = set() + self.sequence = 0 + + def decide(self, value: object) -> dict: + row = exact_keys(value, {"type", "version", "seq", "event", "path", "source", "flags"}) + if row["type"] != "mutation" or type(row["version"]) is not int or row["version"] != 1: + raise SafetyError("mutation_shape") + seq = integer(row["seq"], minimum=1, maximum=LIMITS["events"]) + if seq != self.sequence + 1: + raise SafetyError("mutation_sequence") + path = canonical_path(row["path"], RUN_ROOT) + flags = integer(row["flags"], maximum=(1 << 31) - 1) + purpose = self.fixed.get(path) + if row["event"] == "os.mkdir": + if row["source"] is not None or path not in self.directory_paths or flags != 0o700: + raise SafetyError("unplanned_directory") + purpose = "Private completion directory" + elif row["event"] == "open" and flags & getattr(os, "O_DIRECTORY", 0): + expected = os.O_RDONLY | os.O_DIRECTORY + if (row["source"] is not None or self.case["id"] not in {"capture-a1", "capture-a2", "capture-revision"} + or path not in {self.account, self.account / "captures"} + or flags & ~getattr(os, "O_CLOEXEC", 0) != expected): + raise SafetyError("unplanned_directory_sync") + purpose = "Sync exact retained private capture directory" + elif row["event"] == "open" and flags & os.O_ACCMODE == os.O_RDONLY and not flags & os.O_CREAT: + allowed = {os.O_RDONLY, os.O_RDONLY | os.O_NOFOLLOW} + if (row["source"] is not None or path not in capture_read_catalog(self.case) + or flags & ~getattr(os, "O_CLOEXEC", 0) not in allowed): + raise SafetyError("unplanned_capture_read") + purpose = "Read exact retained synthetic capture with fresh identity and hash proof" + elif row["event"] == "open": + if row["source"] is not None or flags & os.O_TRUNC: + raise SafetyError("unplanned_open") + access = flags & os.O_ACCMODE + extras = flags & ~(os.O_ACCMODE | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW | getattr(os, "O_CLOEXEC", 0)) + if extras or access not in (os.O_WRONLY, os.O_RDWR) or not flags & os.O_CREAT: + raise SafetyError("unplanned_open_flags") + if path.parent == self.account and _PENDING.fullmatch(path.name): + if self.case["id"] not in {"capture-a1", "capture-a2", "capture-revision"} or access != os.O_WRONLY or not flags & os.O_EXCL or len(self.pending | {path}) > 1: + raise SafetyError("pending_slot_limit") + self.pending.add(path) + purpose = "Atomic completion-state temporary; failed evidence retained" + elif path not in self.fixed or path.name == "state.json": + raise SafetyError("unplanned_file") + elif path.name == "state.lock" or path == RUN_ROOT / "locks/contention-v1.lock": + if access != os.O_RDWR or flags & os.O_EXCL: + raise SafetyError("lock_flags") + elif access != os.O_WRONLY or not flags & os.O_EXCL: + raise SafetyError("capture_flags") + elif row["event"] == "os.rename": + source = canonical_path(row["source"], RUN_ROOT) + if flags != 0 or path != self.account / "state.json" or source not in self.pending: + raise SafetyError("unplanned_replace") + purpose = "Publish current-run completion state from its approved temporary" + else: + raise SafetyError("unsupported_mutation") + self.sequence = seq + return {"type": "grant", "version": 1, "seq": seq, "path": str(path), "purpose": purpose} + + +def mutation_target(grant: dict, request: dict) -> None: + path = Path(grant["path"]) + owned_path(path.parent, directory=True) + if request["event"] == "open" and request["flags"] & getattr(os, "O_DIRECTORY", 0): + owned_path(path, directory=True) + return + if path.exists() or path.is_symlink(): + owned_path(path, directory=request["event"] == "os.mkdir") + if request["event"] == "os.rename": + owned_path(Path(request["source"])) + + +class PressureReader: + def __init__(self) -> None: + import ctypes + self.ctypes = ctypes + self.library = ctypes.CDLL("/usr/lib/libSystem.B.dylib", use_errno=True) + self.read = self.library.sysctlbyname + self.read.argtypes = [ctypes.c_char_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.c_void_p, ctypes.c_size_t] + self.read.restype = ctypes.c_int + + def pressure(self) -> int: + c = self.ctypes + output = c.c_uint32() + size = c.c_size_t(c.sizeof(output)) + if self.read(b"kern.memorystatus_vm_pressure_level", c.byref(output), c.byref(size), None, 0) != 0 or size.value != 4: + raise SafetyError("pressure_unavailable") + if output.value not in (1, 2, 4): + raise SafetyError("pressure_abi_unknown") + return output.value + + +class PinnedPythonLoader(importlib.abc.Loader): + def __init__(self, path: Path, checksum: str) -> None: + self.path, self.checksum = path, checksum + + def create_module(self, spec: object) -> None: + return None + + def exec_module(self, module: ModuleType) -> None: + data = read_bounded(self.path, 262144, private=False) + if digest(data) != self.checksum: + raise SafetyError("dependency_hash_changed") + exec(compile(data, str(self.path), "exec", dont_inherit=True), module.__dict__) + + +class PinnedPsutilFinder(importlib.abc.MetaPathFinder): + def __init__(self, packet: dict) -> None: + self.root = Path(packet["dependency_root"]) + self.files = packet["dependency_files"] + + def find_spec(self, fullname: str, path: object = None, target: object = None) -> object: + if fullname != "psutil" and not fullname.startswith("psutil."): + return None + relative = fullname.split(".")[1:] + candidate = self.root.joinpath(*relative) if relative else self.root + source = candidate / "__init__.py" if not relative else candidate.with_suffix(".py") + entry = self.files.get(str(source)) + if entry is not None: + return importlib.util.spec_from_file_location(fullname, source, loader=PinnedPythonLoader(source, entry["sha256"]), submodule_search_locations=[str(self.root)] if not relative else None) + for suffix in importlib.machinery.EXTENSION_SUFFIXES: + extension = Path(str(candidate) + suffix) + entry = self.files.get(str(extension)) + if entry is not None: + if digest(read_bounded(extension, 262144, private=False)) != entry["sha256"]: + raise SafetyError("dependency_extension_changed") + return importlib.util.spec_from_file_location(fullname, extension) + raise SafetyError("unpinned_dependency_module") + + +def pinned_runtime() -> ModuleType: + raw = read_bounded(RUNTIME_PACKET, 65536, private=False) + if digest(raw) != RUNTIME_PACKET_SHA: + raise SafetyError("runtime_packet_changed") + packet = json.loads(raw) + if Path(sys.executable).resolve() != Path(packet["python_realpath"]) or sys.version_info[:3] != (3, 12, 12): + raise SafetyError("interpreter_mismatch") + if digest(read_bounded(Path(packet["python_realpath"]), 1048576, private=False)) != packet["python_executable_sha256"]: + raise SafetyError("interpreter_hash_changed") + if "psutil" in sys.modules: + raise SafetyError("dependency_preloaded") + for name, entry in packet["dependency_files"].items(): + if digest(read_bounded(Path(name), 262144, private=False)) != entry["sha256"]: + raise SafetyError("dependency_hash_changed") + sys.meta_path.insert(0, PinnedPsutilFinder(packet)) + import psutil + if psutil.__version__ != packet["psutil_version"]: + raise SafetyError("dependency_version_changed") + return psutil + + +def sample(psutil: ModuleType, pressure: PressureReader) -> Sensor: + started = time.monotonic_ns() + row = Sensor(started, pressure.pressure(), psutil.virtual_memory().free, psutil.swap_memory().used) + valid_sensor(asdict(row)) + if time.monotonic_ns() - started > 500_000_000: + raise SafetyError("sensor_call_stale") + return row + + +def observe_identity(psutil: ModuleType, pid: int) -> Identity: + process = psutil.Process(pid) + born = process.create_time() + try: + valid = type(born) in (int, float) and math.isfinite(born) and born > 0 + except (OverflowError, ValueError): + valid = False + if not valid: + raise SafetyError("birth_unavailable") + return Identity(pid, process.uids().real, int(born * 1_000_000_000), os.getpgid(pid)) + + +def announce(path: Path, purpose: str) -> None: + global _announcement_bytes + line = f"Create {path}: {purpose}\n" + _announcement_bytes += len(line.encode("utf-8")) + if _announcement_bytes > 65536: + raise SafetyError("announcement_byte_limit") + sys.stdout.write(line) + sys.stdout.flush() + + +def new_log(path: Path, purpose: str) -> int: + owned_path(path.parent, directory=True) + announce(path, purpose) + return os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) + + +def write_all(fd: int, data: bytes) -> None: + while data: + count = os.write(fd, data) + if count <= 0: + raise SafetyError("write_no_progress") + data = data[count:] + + +def write_pipe(fd: int, data: bytes, deadline: float) -> None: + if len(data) > LIMITS["input_bytes"]: + raise SafetyError("pipe_frame_input_limit") + while data: + remaining = deadline - time.monotonic() + if remaining <= 0: + raise SafetyError("pipe_write_deadline") + try: + count = os.write(fd, data) + except BlockingIOError: + select.select([], [fd], [], min(remaining, 0.01)) + continue + if count <= 0: + raise SafetyError("pipe_write_no_progress") + data = data[count:] + + +def group_gone(pgid: int) -> bool: + integer(pgid, minimum=1) + try: + os.killpg(pgid, 0) + except ProcessLookupError: + return True + except OSError: + return False + return False + + +def member_identity_matches(expected: Identity, actual: Identity, group: int, worker_uid: int) -> bool: + try: + for row in (expected, actual): + integer(row.pid, minimum=1) + integer(row.uid) + integer(row.birth_ns, minimum=1) + integer(row.pgid, minimum=1) + integer(group, minimum=1) + integer(worker_uid) + except SafetyError: + return False + return expected == actual and actual.uid == worker_uid and actual.pgid == group + + +def terminate_owned(child: subprocess.Popen, expected: Identity | None, members: list[Identity], psutil: ModuleType, result: dict) -> None: + deadline = time.monotonic() + LIMITS["termination_seconds"] + for sig, interval in ((signal.SIGTERM, 0.25), (signal.SIGKILL, 0.75)): + if time.monotonic() >= deadline: + raise SafetyError("termination_deadline") + if child.poll() is None: + if expected is None: + # Popen still owns an unreaped direct child; its PID cannot be reused. + child.send_signal(sig) + result["termination_authority"] = "unreaped_direct_popen_child" + elif same_owned_identity(expected, observe_identity(psutil, child.pid), os.getuid()): + if result["descendant_containment"] == "unproven": + os.killpg(expected.pgid, sig) + result["termination_authority"] = "fresh_owned_leader_group" + else: + child.send_signal(sig) + result["termination_authority"] = "fresh_owned_direct_child" + else: + raise SafetyError("termination_identity_changed") + if sig == signal.SIGKILL and members and expected is not None: + for member in members: + if time.monotonic() >= deadline: + raise SafetyError("member_termination_deadline") + try: + actual = observe_identity(psutil, member.pid) + except psutil.NoSuchProcess: + continue + if not member_identity_matches(member, actual, expected.pgid, os.getuid()): + raise SafetyError("member_termination_identity_changed") + psutil.Process(member.pid).send_signal(sig) + if not result["reaped"]: + try: + child.wait(timeout=max(0.001, min(interval, deadline - time.monotonic()))) + result["reaped"] = True + except subprocess.TimeoutExpired: + pass + if result["reaped"] and not members: + break + result["exit_code"] = child.returncode + if not result["reaped"]: + raise SafetyError("direct_child_reap_unproven") + if result["descendant_containment"] == "unproven": + if expected is None or len(result["descendants"]) != len(members): + raise SafetyError("member_inventory_unproven") + terminal = True + for member in members: + if time.monotonic() >= deadline: + raise SafetyError("containment_proof_deadline") + try: + actual = observe_identity(psutil, member.pid) + except psutil.NoSuchProcess: + continue + if member_identity_matches(member, actual, expected.pgid, os.getuid()): + terminal = False + result["descendant_containment"] = "proven_terminal" if terminal and group_gone(expected.pgid) else "unproven" + if result["descendant_containment"] != "proven_terminal": + raise SafetyError("owned_group_containment_unproven") + + +def execute_case(manifest: dict, case: dict, psutil: ModuleType, pressure: PressureReader, attempt_version: str = "v1") -> dict: + attempt_version = validate_attempt_version(attempt_version) + bootstrap = SOURCE_ROOT / "tests/native/mac-native-bootstrap-v7.py" + policy = MutationPolicy(case) + records: list[Sensor] = [] + result = {"case": case["id"], "implemented": True, "executed": False, "passed": False, + "blocked": None, "unproven": ["installed_package", "installed_host", "models", "physical_hard_cap"], + "proof_scope": PROOF_SCOPE, "import_mode": "isolated_source_module", "synthetic": True, + "source_commit": manifest["source_commit"], "sources": manifest["sources"], + "sensors": [asdict(row) for row in records], "rss_sampled_peak": 0, "limits": LIMITS, + "reaped": False, "exit_code": None, "identity": None, "output_bytes": 0, + "descendants": [], "descendant_containment": "not_observed"} + logs: dict[str, int] = {} + child = None + expected = None + members: list[Identity] = [] + selector = selectors.DefaultSelector() + held_lock = None + frames = bytearray() + protocol_result = None + start = time.monotonic() + try: + records = [] + for index in range(3): + records.append(sample(psutil, pressure)) + if index < 2: + time.sleep(1) + admission(records, time.monotonic_ns()) + if case["source"] is not None: + source = Path(case["source"]) + info = owned_path(source) + raw = read_bounded(source, 4096) + if file_identity(info) != case["source_identity"] or digest(raw) != case["source_sha256"] or len(raw) != case["source_bytes"]: + raise SafetyError("fixture_changed_before_launch") + for name, checksum in manifest["sources"].items(): + if digest(read_bounded(SOURCE_ROOT / name, 262144, private=False)) != checksum: + raise SafetyError("source_hash_changed") + if digest(read_bounded(bootstrap, 262144, private=False)) != manifest["bootstrap_sha256"]: + raise SafetyError("bootstrap_hash_changed") + result["sensors"] = [asdict(row) for row in records] + for channel in ("stdout", "stderr", "events"): + logs[channel] = new_log(RUN_ROOT / "logs" / (case["id"] + "-" + channel + "-" + attempt_version + ".log"), "Retained native " + channel + " evidence") + if case["id"] == "flock-a4": + import fcntl + lock = RUN_ROOT / "locks/contention-v1.lock" + owned_path(lock) + held_lock = os.open(lock, os.O_RDWR | os.O_NOFOLLOW) + fcntl.flock(held_lock, fcntl.LOCK_EX | fcntl.LOCK_NB) + env = {"HOME": manifest["home"], "PATH": "/usr/bin:/bin", "LANG": "C.UTF-8", + "OMP_NUM_THREADS": "1", "OPENBLAS_NUM_THREADS": "1", "MKL_NUM_THREADS": "1", + "VECLIB_MAXIMUM_THREADS": "1", "NUMEXPR_NUM_THREADS": "1"} + request = encoded({"version": 1, "source_root": str(SOURCE_ROOT), "run_root": str(RUN_ROOT), + "home": manifest["home"], "sources": manifest["sources"], "limits": LIMITS, "case": case, + "supervisor_sha256": manifest["supervisor_sha256"]}) + b"\n" + if len(request) > LIMITS["input_bytes"] - LIMITS["events"] * 256: + raise SafetyError("input_limit") + fresh = sample(psutil, pressure) + admission([*records[:-1], fresh], time.monotonic_ns()) + result["sensors"].append(asdict(fresh)) + start = time.monotonic() + child = subprocess.Popen([sys.executable, "-I", "-S", "-B", str(bootstrap)], stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, + cwd=str(RUN_ROOT), close_fds=True, start_new_session=True) + result["executed"] = True + expected = observe_identity(psutil, child.pid) + if not same_owned_identity(expected, expected, os.getuid()): + raise SafetyError("child_identity_unowned") + result["identity"] = asdict(expected) + assert child.stdin is not None and child.stdout is not None and child.stderr is not None + for stream, channel in ((child.stdout, "stdout"), (child.stderr, "stderr")): + os.set_blocking(stream.fileno(), False) + selector.register(stream, selectors.EVENT_READ, channel) + os.set_blocking(child.stdin.fileno(), False) + write_pipe(child.stdin.fileno(), request, min(start + LIMITS["wall_seconds"], time.monotonic() + 0.1)) + input_count = len(request) + previous = fresh + ready = False + next_sample = time.monotonic() + while selector.get_map(): + now = time.monotonic() + if now - start >= LIMITS["wall_seconds"]: + raise SafetyError("child_wall_deadline") + if child.poll() is None and now >= next_sample: + current = sample(psutil, pressure) + if current.pressure != 1 or current.free_bytes < FREE_MIN or current.swap_used_bytes > previous.swap_used_bytes or time.monotonic_ns() - current.monotonic_ns > 500_000_000: + raise SafetyError("during_child_sensor_refusal") + actual = observe_identity(psutil, child.pid) + if not same_owned_identity(expected, actual, os.getuid()): + raise SafetyError("child_identity_changed") + process = psutil.Process(child.pid) + descendants = process.children(recursive=True) + if descendants: + result["descendants"] = [item.pid for item in descendants[:32]] + result["descendant_containment"] = "unproven" + if len(descendants) > 32: + raise SafetyError("descendant_inventory_limit") + members = [observe_identity(psutil, item.pid) for item in descendants] + if any(member.uid != os.getuid() or member.pgid != expected.pgid for member in members): + raise SafetyError("descendant_identity_unowned") + raise SafetyError("unexpected_descendant") + rss = integer(process.memory_info().rss) + result["rss_sampled_peak"] = max(result["rss_sampled_peak"], rss) + if rss > LIMITS["rss_abort_bytes"]: + raise SafetyError("sampled_rss_limit") + if len(result["sensors"]) >= 64: + raise SafetyError("sensor_inventory_limit") + result["sensors"].append(asdict(current)) + previous = current + next_sample = now + 0.1 + for key, _ in selector.select(0.01): + try: + chunk = os.read(key.fileobj.fileno(), 4096) + except BlockingIOError: + continue + if not chunk: + selector.unregister(key.fileobj) + continue + result["output_bytes"] += len(chunk) + if result["output_bytes"] > LIMITS["output_bytes"]: + raise SafetyError("combined_output_limit") + channel = key.data + write_all(logs[channel], chunk) + if channel != "stdout": + continue + frames.extend(chunk) + while b"\n" in frames: + line, _, rest = frames.partition(b"\n") + frames = bytearray(rest) + if len(line) > LIMITS["frame_bytes"]: + raise SafetyError("protocol_frame_limit") + frame_deadline = min(start + LIMITS["wall_seconds"], time.monotonic() + 0.1) + frame = json.loads(line) + if type(frame) is not dict: + raise SafetyError("protocol_object_required") + if frame.get("type") == "mutation": + if not ready or protocol_result is not None: + raise SafetyError("mutation_outside_ready_execution") + try: + grant = policy.decide(frame) + mutation_target(grant, frame) + if frame["event"] == "open" and frame["flags"] & os.O_ACCMODE == os.O_RDONLY and not frame["flags"] & getattr(os, "O_DIRECTORY", 0): + verify_capture_read_target(case, Path(grant["path"])) + except (SafetyError, OSError, ValueError) as error: + write_all(logs["events"], encoded({"request": frame, "decision": {"type": "deny", "reason": type(error).__name__ + ":" + str(error)[:256]}}) + b"\n") + os.fsync(logs["events"]) + raise + write_all(logs["events"], encoded({"request": frame, "decision": grant}) + b"\n") + os.fsync(logs["events"]) + announce(Path(grant["path"]), grant["purpose"]) + reply = encoded(grant) + b"\n" + input_count += len(reply) + if input_count > LIMITS["input_bytes"]: + raise SafetyError("input_limit") + write_pipe(child.stdin.fileno(), reply, frame_deadline) + elif frame == {"type": "lock_busy"} and ready and protocol_result is None and case["id"] == "flock-a4" and held_lock is not None: + os.close(held_lock) + held_lock = None + reply = b'{"type":"lock_released"}\n' + input_count += len(reply) + if input_count > LIMITS["input_bytes"]: + raise SafetyError("input_limit") + write_pipe(child.stdin.fileno(), reply, frame_deadline) + elif frame.get("type") == "result" and protocol_result is None: + if not ready: + raise SafetyError("result_before_ready") + protocol_result = validate_result(frame, case, manifest["sources"]) + elif frame.get("type") == "ready" and not ready: + validate_ready(frame, child.pid) + ready = True + result["limit_readbacks"] = frame["limits"] + else: + raise SafetyError("unexpected_protocol_frame") + if len(frames) > LIMITS["frame_bytes"]: + raise SafetyError("protocol_frame_limit") + if frames: + raise SafetyError("truncated_protocol_frame") + remaining = max(0.001, start + LIMITS["wall_seconds"] - time.monotonic()) + result["exit_code"] = child.wait(timeout=remaining) + result["reaped"] = True + expected_exit = 73 if case["id"] == "flock-a4" else 0 + if result["exit_code"] != expected_exit or type(protocol_result) is not dict or protocol_result.get("passed") is not True or protocol_result.get("proof_scope") != PROOF_SCOPE: + raise SafetyError("case_nonterminal_or_failed") + if case["id"] == "flock-a4": + lock_fd = os.open(RUN_ROOT / "locks/contention-v1.lock", os.O_RDWR | os.O_NOFOLLOW) + try: + fcntl.flock(lock_fd, fcntl.LOCK_EX | fcntl.LOCK_NB) + finally: + os.close(lock_fd) + if not group_gone(expected.pgid): + result["descendant_containment"] = "unproven" + raise SafetyError("post_exit_group_containment_unproven") + result["passed"] = True + result["child_result"] = protocol_result + except (SafetyError, OSError, ValueError, subprocess.SubprocessError, psutil.Error) as error: + result["blocked"] = type(error).__name__ + ":" + str(error)[:256] + finally: + if not result["sensors"]: + result["sensors"] = [asdict(row) for row in records] + if child is None: + result["reaped"] = True + if held_lock is not None: + try: + os.close(held_lock) + except OSError as error: + result["blocked"] = "lock_close_failed:" + str(error)[:256] + result["passed"] = False + if child is not None and (not result["reaped"] or result["descendant_containment"] == "unproven"): + try: + terminate_owned(child, expected, members, psutil, result) + except (SafetyError, OSError, ValueError, subprocess.SubprocessError, psutil.Error) as error: + result["blocked"] = "identity_or_reap_unproven:" + str(error)[:256] + result["passed"] = False + result["wall_seconds"] = time.monotonic() - start + try: + selector.close() + except OSError as error: + result["blocked"] = "selector_close_failed:" + str(error)[:256] + result["passed"] = False + if child is not None: + for stream in (child.stdin, child.stdout, child.stderr): + if stream is not None: + try: + stream.close() + except OSError as error: + result["blocked"] = "pipe_close_failed:" + str(error)[:256] + result["passed"] = False + for fd in logs.values(): + try: + os.fsync(fd) + except OSError as error: + result["blocked"] = "evidence_sync_failed:" + str(error)[:256] + result["passed"] = False + finally: + try: + os.close(fd) + except OSError as error: + result["blocked"] = "evidence_close_failed:" + str(error)[:256] + result["passed"] = False + return result + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--manifest", required=True) + parser.add_argument("--execute-root-authorized", action="store_true") + args = parser.parse_args() + if not args.execute_root_authorized or sys.platform != "darwin" or not sys.flags.isolated or not sys.flags.no_site or not sys.flags.dont_write_bytecode: + raise SafetyError("root_isolated_native_execution_required") + sys.dont_write_bytecode = True + os.umask(0o077) + manifest_path = canonical_path(args.manifest, RUN_ROOT) + target, version = evidence_paths(manifest_path) + owned_path(RUN_ROOT, directory=True) + owned_path(RUN_ROOT / "home", directory=True) + manifest_bytes = read_bounded(manifest_path, 65536) + manifest = validate_manifest(json.loads(manifest_bytes)) + if (version != "v2" and any(case["id"] == "capture-revision" for case in manifest["cases"])) or (version == "v2" and [case["id"] for case in manifest["cases"]] != ["capture-revision"]): + raise SafetyError("unexpected_manifest_case_stage") + if digest(read_bounded(Path(__file__).resolve(), 262144, private=False)) != manifest["supervisor_sha256"]: + raise SafetyError("supervisor_hash_changed") + results: list[dict] = [] + setup_failure = None + # Reserve before runtime admission so a retained refusal cannot be replayed. + fd = new_log(target, "Retained Stage A results; native host/model/hard-cap acceptance remains unproven") + try: + try: + psutil = pinned_runtime() + pressure = PressureReader() + for case in manifest["cases"]: + result = execute_case(manifest, case, psutil, pressure, attempt_version=version) + results.append(result) + if not result["passed"] or not result["reaped"]: + break + except (SafetyError, OSError, ValueError, ImportError) as error: + setup_failure = type(error).__name__ + ":" + str(error)[:256] + data = encoded({"version": 1, "manifest_version": version, "manifest_sha256": digest(manifest_bytes), + "proof_scope": PROOF_SCOPE, "cases": results, "setup_failure": setup_failure, + "complete": setup_failure is None and len(results) == len(manifest["cases"]) and all(item["passed"] for item in results)}) + b"\n" + if len(data) > LIMITS["output_bytes"]: + raise SafetyError("results_byte_limit") + write_all(fd, data) + finally: + try: + os.fsync(fd) + finally: + os.close(fd) + return 0 if setup_failure is None and len(results) == len(manifest["cases"]) and all(item["passed"] for item in results) else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/native/mac-native-bootstrap-v1.py b/tests/native/mac-native-bootstrap-v1.py new file mode 100644 index 00000000..f2692ae1 --- /dev/null +++ b/tests/native/mac-native-bootstrap-v1.py @@ -0,0 +1,378 @@ +"""Fixed Stage A child. Importing definitions never launches native work.""" +from __future__ import annotations + +import hashlib +import json +import os +import resource +import select +import signal +import stat +import sys +import time +import types +from pathlib import Path + +LIMITS = {"wall_seconds": 5, "termination_seconds": 1, "cpu_soft": 2, "cpu_hard": 3, + "rss_abort_bytes": 67108864, "core_bytes": 0, "nofile": 64, + "fsize_bytes": 1048576, "input_bytes": 65536, "output_bytes": 262144, + "events": 32, "frame_bytes": 2048, "ack_ms": 100, "threads": 1} +RUN_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/native-mac-v1/run-0001") +SOURCE_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/worktrees/mac-native-acceptance-v1") +PROOF_SCOPE = "instrumented_unmodified_source_native_filesystem_lock_capture" +SOURCE_MODULES = ( + ("truememory._platform", "truememory/_platform.py"), + ("truememory.ingest.hooks._shared", "truememory/ingest/hooks/_shared.py"), + ("truememory.ingest.completion", "truememory/ingest/completion.py"), + ("truememory.ingest.transcript", "truememory/ingest/transcript.py"), + ("truememory.ingest.provenance", "truememory/ingest/provenance.py"), +) + + +class BootstrapError(ValueError): + pass + + +def encoded(value: object) -> bytes: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8") + + +class Protocol: + def __init__(self) -> None: + self.buffer = bytearray() + self.read_bytes = 0 + self.written_bytes = 0 + + def emit(self, value: object) -> None: + data = encoded(value) + b"\n" + if len(data) > LIMITS["frame_bytes"]: + raise BootstrapError("output_frame_limit") + self.written_bytes += len(data) + if self.written_bytes > LIMITS["output_bytes"]: + raise BootstrapError("output_limit") + while data: + count = os.write(1, data) + if count <= 0: + raise BootstrapError("output_no_progress") + data = data[count:] + + def receive(self, maximum: int, seconds: float) -> dict: + deadline = time.monotonic() + seconds + while b"\n" not in self.buffer: + remaining = deadline - time.monotonic() + if remaining <= 0 or not select.select([0], [], [], remaining)[0]: + raise BootstrapError("input_deadline") + chunk = os.read(0, min(4096, maximum + 1)) + if not chunk: + raise BootstrapError("input_eof") + self.read_bytes += len(chunk) + self.buffer.extend(chunk) + if self.read_bytes > LIMITS["input_bytes"] or len(self.buffer) > maximum: + raise BootstrapError("input_limit") + line, _, remaining = self.buffer.partition(b"\n") + self.buffer = bytearray(remaining) + if len(line) > maximum: + raise BootstrapError("input_frame_limit") + value = json.loads(line) + if type(value) is not dict: + raise BootstrapError("input_shape") + return value + + +def arm_limits() -> dict[str, list[int]]: + if sys.platform != "darwin" or not sys.flags.isolated or not sys.flags.no_site or not sys.flags.dont_write_bytecode: + raise BootstrapError("isolated_mac_child_required") + os.umask(0o077) + sys.dont_write_bytecode = True + expected = {"cpu": (2, 3), "core": (0, 0), "nofile": (64, 64), "fsize": (1048576, 1048576)} + kinds = {"cpu": resource.RLIMIT_CPU, "core": resource.RLIMIT_CORE, + "nofile": resource.RLIMIT_NOFILE, "fsize": resource.RLIMIT_FSIZE} + for name, limits in expected.items(): + current = resource.getrlimit(kinds[name]) + if any(old != resource.RLIM_INFINITY and old < new for old, new in zip(current, limits)): + raise BootstrapError("existing_limit_below_reviewed_limit") + resource.setrlimit(kinds[name], limits) + if resource.getrlimit(kinds[name]) != limits: + raise BootstrapError("limit_readback_failed") + signal.signal(signal.SIGALRM, signal.SIG_DFL) + signal.setitimer(signal.ITIMER_REAL, 5) + return {name: list(limits) for name, limits in expected.items()} + + +def compile_module(name: str, path: Path, checksum: str, *, package: bool = False) -> types.ModuleType: + if path.is_symlink() or path.resolve() != path: + raise BootstrapError("source_path_link") + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + try: + before = os.fstat(fd) + if not stat.S_ISREG(before.st_mode) or before.st_size > 262144: + raise BootstrapError("source_read_type_or_limit") + stream = os.fdopen(fd, "rb") + fd = None + with stream: + data = stream.read(262145) + after = os.fstat(stream.fileno()) + finally: + if fd is not None: + os.close(fd) + if (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns, before.st_ctime_ns) != (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns, after.st_ctime_ns): + raise BootstrapError("source_changed_during_read") + if len(data) > 262144 or hashlib.sha256(data).hexdigest() != checksum: + raise BootstrapError("source_hash_changed") + module = types.ModuleType(name) + module.__file__ = str(path) + module.__package__ = name if package else name.rpartition(".")[0] + if package: + module.__path__ = [str(path.parent)] + sys.modules[name] = module + exec(compile(data, str(path), "exec", dont_inherit=True), module.__dict__) + return module + + +class AuditGuard: + """Bounded telemetry/grants for fixed trusted source, not a security sandbox.""" + def __init__(self, protocol: Protocol, request: dict, policy_module: types.ModuleType) -> None: + self.protocol, self.request, self.policy_module = protocol, request, policy_module + self.case = request["case"] + self.sequence = 0 + self.activated = False + self.approved: dict[Path, int] = {} + self.known_fds: dict[int, tuple[int, int]] = {} + self.open_intents: list[tuple[Path, int]] = [] + self.source = Path(self.case["source"]) if self.case["source"] is not None else None + self.read_paths = {SOURCE_ROOT / relative for _, relative in SOURCE_MODULES} + self.read_paths.add(SOURCE_ROOT / "tests/native/mac-native-acceptance-v1.py") + if self.source is not None: + self.read_paths.add(self.source) + if self.case["id"] == "negative-a3": + self.read_paths.add(RUN_ROOT / "home/.claude/projects/native-acceptance-v1/negative-directory-v1.jsonl") + if self.case["previous"] is not None: + self.read_paths.add(policy_module.account_directory(self.case) / "captures" / (self.case["previous"]["key"] + ".jsonl")) + self.runtime_root = Path(sys.base_prefix) / "lib/python3.12" + + def grant(self, event: str, path: Path, *, flags: int, source: Path | None = None) -> None: + self.sequence += 1 + if self.sequence > LIMITS["events"]: + raise BootstrapError("mutation_event_limit") + self.protocol.emit({"type": "mutation", "version": 1, "seq": self.sequence, + "event": event, "path": str(path), "source": str(source) if source is not None else None, + "flags": flags}) + answer = self.protocol.receive(LIMITS["frame_bytes"], LIMITS["ack_ms"] / 1000) + if set(answer) != {"type", "version", "seq", "path", "purpose"} or answer["type"] != "grant" or type(answer["version"]) is not int or answer["version"] != 1 or type(answer["seq"]) is not int or answer["seq"] != self.sequence or answer["path"] != str(path) or type(answer["purpose"]) is not str or not answer["purpose"]: + raise BootstrapError("grant_mismatch") + if event == "open": + self.approved[path] = flags & os.O_ACCMODE + elif event == "os.rename" and source is not None: + if source not in self.approved: + raise BootstrapError("replace_source_not_approved") + self.approved[path] = self.approved.pop(source) + + def path(self, value: object) -> Path: + if not isinstance(value, (str, bytes)): + raise BootstrapError("path_argument_type") + if isinstance(value, bytes): + value = value.decode("utf-8") + if "\0" in value or not Path(value).is_absolute() or ".." in Path(value).parts: + raise BootstrapError("path_not_absolute") + path = Path(value) + if str(path) != value or path.resolve() != path or any(part.is_symlink() for part in (path, *path.parents)): + raise BootstrapError("path_not_canonical") + return path + + def readable(self, path: Path) -> bool: + if path in self.read_paths or path in self.approved: + return True + account = self.policy_module.account_directory(self.case) + if path in {account / "state.json", account / "state.lock"}: + return True + if path.is_relative_to(self.runtime_root) and "site-packages" not in path.parts: + return True + return False + + def descriptor(self, fd: int, mode: object) -> None: + import fcntl + # CPython FileIO reports r/w for an explicitly binary rb/wb fd wrapper. + if type(fd) is not int or fd in (0, 1, 2) or type(mode) is not str or mode not in ("r", "rb", "w", "wb"): + raise BootstrapError("unsupported_descriptor_wrapper") + info = os.fstat(fd) + identity = (info.st_dev, info.st_ino) + if not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid(): + raise BootstrapError("descriptor_not_regular_owned") + access = fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_ACCMODE + if not self.open_intents: + raise BootstrapError("descriptor_not_preapproved") + path, allowed = self.open_intents.pop() + current = path.lstat() + if path.is_symlink() or not stat.S_ISREG(current.st_mode) or current.st_uid != os.getuid() or (current.st_dev, current.st_ino) != identity: + raise BootstrapError("descriptor_intent_identity_mismatch") + if allowed != access or (mode.startswith("r") and access != os.O_RDONLY) or (mode.startswith("w") and access != os.O_WRONLY): + raise BootstrapError("descriptor_access_mismatch") + if len(self.known_fds | {fd: identity}) > LIMITS["events"]: + raise BootstrapError("descriptor_inventory_limit") + self.known_fds[fd] = identity + + def __call__(self, event: str, args: tuple[object, ...]) -> None: + if event == "truememory.native.audit_probe": + if args != ("fixed-stage-a-v1",): + raise BootstrapError("audit_probe_mismatch") + self.activated = True + return + if event == "open": + path, mode, flags = args + if type(flags) is not int: + raise BootstrapError("open_flags_type") + if type(path) is int: + self.descriptor(path, mode) + return + target = self.path(path) + if flags & (os.O_CREAT | os.O_WRONLY | os.O_RDWR | os.O_TRUNC | os.O_APPEND): + if not target.is_relative_to(RUN_ROOT): + raise BootstrapError("write_escape") + self.grant("open", target, flags=flags) + elif not self.readable(target): + raise BootstrapError("unplanned_read") + if mode is None: + # Only the latest approved raw open may supply a wrapper intent. + self.open_intents[:] = [(target, flags & os.O_ACCMODE)] + else: + self.open_intents.clear() + elif event == "os.mkdir": + path, mode, dir_fd = args + if dir_fd not in (-1, None) or type(mode) is not int: + raise BootstrapError("directory_descriptor_forbidden") + self.grant(event, self.path(path), flags=mode) + elif event == "os.rename": + source, destination, source_fd, destination_fd = args + if source_fd not in (-1, None) or destination_fd not in (-1, None): + raise BootstrapError("rename_descriptor_forbidden") + self.grant(event, self.path(destination), flags=0, source=self.path(source)) + elif event in ("os.listdir", "os.scandir"): + target = self.path(args[0]) + if not target.is_relative_to(RUN_ROOT) and not (target.is_relative_to(self.runtime_root) and "site-packages" not in target.parts): + raise BootstrapError("unplanned_directory_read") + elif event.startswith("socket.") or event in {"subprocess.Popen", "os.fork", "os.forkpty", "os.posix_spawn", "os.spawn", "os.system", "os.exec", "_thread.start_new_thread", "ctypes.dlopen", "sqlite3.connect", "sqlite3.load_extension", "sqlite3.enable_load_extension", "os.remove", "os.rmdir", "os.link", "os.symlink", "os.truncate", "os.chmod", "os.chown", "os.chflags", "resource.setrlimit", "sys.addaudithook"}: + raise BootstrapError("forbidden_native_operation:" + event) + elif event in ("os.kill", "os.killpg"): + if event != "os.kill" or args != (os.getpid(), 0): + raise BootstrapError("external_signal_forbidden") + elif event == "import": + name = args[0] + if type(name) is not str: + raise BootstrapError("import_name_type") + if name.split(".")[0] in {"torch", "transformers", "mlx", "numpy", "sentence_transformers", "requests", "httpx", "ctypes", "psutil"}: + raise BootstrapError("model_network_dependency_forbidden") + if name.startswith("truememory.") and name not in {item[0] for item in SOURCE_MODULES} and name not in {"truememory.ingest", "truememory.ingest.hooks"}: + raise BootstrapError("unplanned_production_import") + + +def require(condition: bool, reason: str) -> None: + if not condition: + raise BootstrapError(reason) + + +def execute_case(case: dict, modules: dict[str, types.ModuleType], protocol: Protocol, policy: types.ModuleType) -> dict: + completion = modules["truememory.ingest.completion"] + platform = modules["truememory._platform"] + transcript = modules["truememory.ingest.transcript"] + provenance = modules["truememory.ingest.provenance"] + kind = case["id"] + details: dict[str, object] = {} + if kind in {"capture-a1", "capture-a2", "capture-revision", "invalid-state"}: + try: + capture = completion.admit_capture(case["source"], case["session"], "native-acceptance-v1", case["database"], "claude", root=RUN_ROOT / "completion") + except completion.CompletionError as error: + if kind != "invalid-state" or str(error) != "invalid_retained_state": + raise + details["category"] = str(error) + else: + require(kind != "invalid-state", "invalid_state_was_admitted") + require(capture.key == policy.digest(policy.encoded(policy.capture_metadata(case))), "capture_key_mismatch") + completion.verify_capture(capture) + raw = capture.path.read_bytes() + require(hashlib.sha256(raw).hexdigest() == case["source_sha256"] and len(raw) == case["source_bytes"], "capture_bytes_mismatch") + require(capture.metadata == policy.capture_metadata(case), "capture_scope_mismatch") + require(capture.path.stat().st_mode & 0o077 == 0, "capture_not_private") + second = completion.admit_capture(case["source"], case["session"], "native-acceptance-v1", case["database"], "claude", root=RUN_ROOT / "completion") + require(second.key == capture.key and second.path == capture.path, "capture_not_idempotent") + if kind == "capture-revision": + old = case["previous"] + old_path = capture.directory / "captures" / (old["key"] + ".jsonl") + previous = old_path.read_bytes() + require(capture.key != old["key"] and hashlib.sha256(previous).hexdigest() == old["sha256"] and len(previous) == old["byte_count"], "revision_lost_previous_evidence") + details.update({"category": "immutable_capture_verified", "capture_key": capture.key, "source_sha256": case["source_sha256"], "byte_count": len(raw), "idempotent": True}) + elif kind == "negative-a3": + base = RUN_ROOT / "home/.claude/projects/native-acceptance-v1" + controls = [(case["source"], "unknown", "claude", "source_not_admitted"), + (case["source"], case["session"], "unknown", "source_not_admitted"), + (str(RUN_ROOT / "fixtures/negative-outside-v1.jsonl"), case["session"], "claude", "source_not_admitted"), + (str(base / "negative-source-link-v1.jsonl"), case["session"], "claude", "source_link"), + (str(base / "negative-directory-v1.jsonl"), case["session"], "claude", "source_not_regular")] + categories = [] + for source, session, host, expected in controls: + try: + completion.admit_capture(source, session, "native-acceptance-v1", case["database"], host, root=RUN_ROOT / "completion") + except completion.CompletionError as error: + require(str(error) == expected, "negative_category_mismatch") + categories.append(str(error)) + else: + raise BootstrapError("negative_source_admitted") + details.update({"category": "source_negatives_verified", "categories": categories}) + elif kind == "flock-a4": + lock = RUN_ROOT / "locks/contention-v1.lock" + require(platform.try_file_lock(lock) is None, "contention_not_observed") + protocol.emit({"type": "lock_busy"}) + require(protocol.receive(2048, 0.1) == {"type": "lock_released"}, "lock_release_protocol") + fd = platform.try_file_lock(lock) + require(type(fd) is int and fd >= 0, "released_lock_not_acquired") + details.update({"category": "native_contention_then_acquired", "post_exit_release_requires_parent": True}) + # Leave the actual acquired descriptor open: the parent proves exit release. + elif kind == "parser-a5": + parsed = transcript.parse_transcript_outcome(Path(case["source"])) + require(parsed.status == "complete" and parsed.file_version is not None and parsed.file_version.stable, "parser_scope_unproven") + require(parsed.file_version.sha256 == case["source_sha256"], "parser_version_mismatch") + require(any(message.timestamp for message in parsed.messages) and any(not message.timestamp for message in parsed.messages), "fixture_lacks_known_unknown_times") + formatted = transcript.format_for_extraction(parsed.messages) + context = provenance.source_context(formatted, case["session"]) + require(context.length == len(formatted) and context.digest == hashlib.sha256(formatted.encode("utf-8")).hexdigest(), "provenance_context_mismatch") + require(provenance._known_time("") is None and provenance._known_time("2020-01-01T00:00:00Z") is not None, "source_time_guard_mismatch") + details.update({"category": "parser_and_source_context_verified", "messages": len(parsed.messages), "known_unknown_time": True, + "scorer_extractor_store_proven": False}) + else: + raise BootstrapError("unsupported_case") + return details + + +def main() -> int: + readback = arm_limits() + protocol = Protocol() + request = protocol.receive(LIMITS["input_bytes"], 1) + required = {"version", "source_root", "run_root", "home", "sources", "limits", "case", "supervisor_sha256"} + require(set(request) == required and type(request["version"]) is int and request["version"] == 1, "request_shape") + require(request["source_root"] == str(SOURCE_ROOT) and request["run_root"] == str(RUN_ROOT) and request["home"] == str(RUN_ROOT / "home") and os.environ.get("HOME") == request["home"], "request_roots") + require(request["limits"] == LIMITS and all(type(number) is int for number in request["limits"].values()), "request_limits") + require(type(request["sources"]) is dict and set(request["sources"]) == {relative for _, relative in SOURCE_MODULES}, "source_pin_set") + policy = compile_module("mac_native_policy_v1", SOURCE_ROOT / "tests/native/mac-native-acceptance-v1.py", request["supervisor_sha256"]) + policy.validate_case(request["case"]) + for checksum in request["sources"].values(): + policy.hash_text(checksum) + guard = AuditGuard(protocol, request, policy) + sys.addaudithook(guard) + sys.audit("truememory.native.audit_probe", "fixed-stage-a-v1") + require(guard.activated, "audit_hook_not_installed") + protocol.emit({"type": "ready", "version": 1, "pid": os.getpid(), "limits": readback, "audit_installed": True}) + for name, relative in (("truememory", "truememory"), ("truememory.ingest", "truememory/ingest"), ("truememory.ingest.hooks", "truememory/ingest/hooks")): + module = types.ModuleType(name) + module.__path__ = [str(SOURCE_ROOT / relative)] + module.__package__ = name + sys.modules[name] = module + modules = {name: compile_module(name, SOURCE_ROOT / relative, request["sources"][relative]) for name, relative in SOURCE_MODULES} + details = execute_case(request["case"], modules, protocol, policy) + protocol.emit({"type": "result", "version": 1, "case": request["case"]["id"], "passed": True, + "proof_scope": PROOF_SCOPE, "import_mode": "isolated_source_module", "sources": request["sources"], "details": details}) + if request["case"]["id"] == "flock-a4": + os._exit(73) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/native/mac-native-bootstrap-v2.py b/tests/native/mac-native-bootstrap-v2.py new file mode 100644 index 00000000..1d3071ec --- /dev/null +++ b/tests/native/mac-native-bootstrap-v2.py @@ -0,0 +1,378 @@ +"""Fixed Stage A child. Importing definitions never launches native work.""" +from __future__ import annotations + +import hashlib +import json +import os +import resource +import select +import signal +import stat +import sys +import time +import types +from pathlib import Path + +LIMITS = {"wall_seconds": 5, "termination_seconds": 1, "cpu_soft": 2, "cpu_hard": 3, + "rss_abort_bytes": 67108864, "core_bytes": 0, "nofile": 64, + "fsize_bytes": 1048576, "input_bytes": 65536, "output_bytes": 262144, + "events": 32, "frame_bytes": 2048, "ack_ms": 100, "threads": 1} +RUN_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/native-mac-v1/run-0001") +SOURCE_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/worktrees/mac-native-acceptance-v1") +PROOF_SCOPE = "instrumented_unmodified_source_native_filesystem_lock_capture" +SOURCE_MODULES = ( + ("truememory._platform", "truememory/_platform.py"), + ("truememory.ingest.hooks._shared", "truememory/ingest/hooks/_shared.py"), + ("truememory.ingest.completion", "truememory/ingest/completion.py"), + ("truememory.ingest.transcript", "truememory/ingest/transcript.py"), + ("truememory.ingest.provenance", "truememory/ingest/provenance.py"), +) + + +class BootstrapError(ValueError): + pass + + +def encoded(value: object) -> bytes: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8") + + +class Protocol: + def __init__(self) -> None: + self.buffer = bytearray() + self.read_bytes = 0 + self.written_bytes = 0 + + def emit(self, value: object) -> None: + data = encoded(value) + b"\n" + if len(data) > LIMITS["frame_bytes"]: + raise BootstrapError("output_frame_limit") + self.written_bytes += len(data) + if self.written_bytes > LIMITS["output_bytes"]: + raise BootstrapError("output_limit") + while data: + count = os.write(1, data) + if count <= 0: + raise BootstrapError("output_no_progress") + data = data[count:] + + def receive(self, maximum: int, seconds: float) -> dict: + deadline = time.monotonic() + seconds + while b"\n" not in self.buffer: + remaining = deadline - time.monotonic() + if remaining <= 0 or not select.select([0], [], [], remaining)[0]: + raise BootstrapError("input_deadline") + chunk = os.read(0, min(4096, maximum + 1)) + if not chunk: + raise BootstrapError("input_eof") + self.read_bytes += len(chunk) + self.buffer.extend(chunk) + if self.read_bytes > LIMITS["input_bytes"] or len(self.buffer) > maximum: + raise BootstrapError("input_limit") + line, _, remaining = self.buffer.partition(b"\n") + self.buffer = bytearray(remaining) + if len(line) > maximum: + raise BootstrapError("input_frame_limit") + value = json.loads(line) + if type(value) is not dict: + raise BootstrapError("input_shape") + return value + + +def arm_limits() -> dict[str, list[int]]: + if sys.platform != "darwin" or not sys.flags.isolated or not sys.flags.no_site or not sys.flags.dont_write_bytecode: + raise BootstrapError("isolated_mac_child_required") + os.umask(0o077) + sys.dont_write_bytecode = True + expected = {"cpu": (2, 3), "core": (0, 0), "nofile": (64, 64), "fsize": (1048576, 1048576)} + kinds = {"cpu": resource.RLIMIT_CPU, "core": resource.RLIMIT_CORE, + "nofile": resource.RLIMIT_NOFILE, "fsize": resource.RLIMIT_FSIZE} + for name, limits in expected.items(): + current = resource.getrlimit(kinds[name]) + if any(old != resource.RLIM_INFINITY and old < new for old, new in zip(current, limits)): + raise BootstrapError("existing_limit_below_reviewed_limit") + resource.setrlimit(kinds[name], limits) + if resource.getrlimit(kinds[name]) != limits: + raise BootstrapError("limit_readback_failed") + signal.signal(signal.SIGALRM, signal.SIG_DFL) + signal.setitimer(signal.ITIMER_REAL, 5) + return {name: list(limits) for name, limits in expected.items()} + + +def compile_module(name: str, path: Path, checksum: str, *, package: bool = False) -> types.ModuleType: + if path.is_symlink() or path.resolve() != path: + raise BootstrapError("source_path_link") + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + try: + before = os.fstat(fd) + if not stat.S_ISREG(before.st_mode) or before.st_size > 262144: + raise BootstrapError("source_read_type_or_limit") + stream = os.fdopen(fd, "rb") + fd = None + with stream: + data = stream.read(262145) + after = os.fstat(stream.fileno()) + finally: + if fd is not None: + os.close(fd) + if (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns, before.st_ctime_ns) != (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns, after.st_ctime_ns): + raise BootstrapError("source_changed_during_read") + if len(data) > 262144 or hashlib.sha256(data).hexdigest() != checksum: + raise BootstrapError("source_hash_changed") + module = types.ModuleType(name) + module.__file__ = str(path) + module.__package__ = name if package else name.rpartition(".")[0] + if package: + module.__path__ = [str(path.parent)] + sys.modules[name] = module + exec(compile(data, str(path), "exec", dont_inherit=True), module.__dict__) + return module + + +class AuditGuard: + """Bounded telemetry/grants for fixed trusted source, not a security sandbox.""" + def __init__(self, protocol: Protocol, request: dict, policy_module: types.ModuleType) -> None: + self.protocol, self.request, self.policy_module = protocol, request, policy_module + self.case = request["case"] + self.sequence = 0 + self.activated = False + self.approved: dict[Path, int] = {} + self.known_fds: dict[int, tuple[int, int]] = {} + self.open_intents: list[tuple[Path, int]] = [] + self.source = Path(self.case["source"]) if self.case["source"] is not None else None + self.read_paths = {SOURCE_ROOT / relative for _, relative in SOURCE_MODULES} + self.read_paths.add(SOURCE_ROOT / "tests/native/mac-native-acceptance-v2.py") + if self.source is not None: + self.read_paths.add(self.source) + if self.case["id"] == "negative-a3": + self.read_paths.add(RUN_ROOT / "home/.claude/projects/native-acceptance-v1/negative-directory-v1.jsonl") + if self.case["previous"] is not None: + self.read_paths.add(policy_module.account_directory(self.case) / "captures" / (self.case["previous"]["key"] + ".jsonl")) + self.runtime_root = Path(sys.base_prefix) / "lib/python3.12" + + def grant(self, event: str, path: Path, *, flags: int, source: Path | None = None) -> None: + self.sequence += 1 + if self.sequence > LIMITS["events"]: + raise BootstrapError("mutation_event_limit") + self.protocol.emit({"type": "mutation", "version": 1, "seq": self.sequence, + "event": event, "path": str(path), "source": str(source) if source is not None else None, + "flags": flags}) + answer = self.protocol.receive(LIMITS["frame_bytes"], LIMITS["ack_ms"] / 1000) + if set(answer) != {"type", "version", "seq", "path", "purpose"} or answer["type"] != "grant" or type(answer["version"]) is not int or answer["version"] != 1 or type(answer["seq"]) is not int or answer["seq"] != self.sequence or answer["path"] != str(path) or type(answer["purpose"]) is not str or not answer["purpose"]: + raise BootstrapError("grant_mismatch") + if event == "open": + self.approved[path] = flags & os.O_ACCMODE + elif event == "os.rename" and source is not None: + if source not in self.approved: + raise BootstrapError("replace_source_not_approved") + self.approved[path] = self.approved.pop(source) + + def path(self, value: object) -> Path: + if not isinstance(value, (str, bytes)): + raise BootstrapError("path_argument_type") + if isinstance(value, bytes): + value = value.decode("utf-8") + if "\0" in value or not Path(value).is_absolute() or ".." in Path(value).parts: + raise BootstrapError("path_not_absolute") + path = Path(value) + if str(path) != value or path.resolve() != path or any(part.is_symlink() for part in (path, *path.parents)): + raise BootstrapError("path_not_canonical") + return path + + def readable(self, path: Path) -> bool: + if path in self.read_paths or path in self.approved: + return True + account = self.policy_module.account_directory(self.case) + if path in {account / "state.json", account / "state.lock"}: + return True + if path.is_relative_to(self.runtime_root) and "site-packages" not in path.parts: + return True + return False + + def descriptor(self, fd: int, mode: object) -> None: + import fcntl + # CPython FileIO reports r/w for an explicitly binary rb/wb fd wrapper. + if type(fd) is not int or fd in (0, 1, 2) or type(mode) is not str or mode not in ("r", "rb", "w", "wb"): + raise BootstrapError("unsupported_descriptor_wrapper") + info = os.fstat(fd) + identity = (info.st_dev, info.st_ino) + if not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid(): + raise BootstrapError("descriptor_not_regular_owned") + access = fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_ACCMODE + if not self.open_intents: + raise BootstrapError("descriptor_not_preapproved") + path, allowed = self.open_intents.pop() + current = path.lstat() + if path.is_symlink() or not stat.S_ISREG(current.st_mode) or current.st_uid != os.getuid() or (current.st_dev, current.st_ino) != identity: + raise BootstrapError("descriptor_intent_identity_mismatch") + if allowed != access or (mode.startswith("r") and access != os.O_RDONLY) or (mode.startswith("w") and access != os.O_WRONLY): + raise BootstrapError("descriptor_access_mismatch") + if len(self.known_fds | {fd: identity}) > LIMITS["events"]: + raise BootstrapError("descriptor_inventory_limit") + self.known_fds[fd] = identity + + def __call__(self, event: str, args: tuple[object, ...]) -> None: + if event == "truememory.native.audit_probe": + if args != ("fixed-stage-a-v1",): + raise BootstrapError("audit_probe_mismatch") + self.activated = True + return + if event == "open": + path, mode, flags = args + if type(flags) is not int: + raise BootstrapError("open_flags_type") + if type(path) is int: + self.descriptor(path, mode) + return + target = self.path(path) + if flags & (os.O_CREAT | os.O_WRONLY | os.O_RDWR | os.O_TRUNC | os.O_APPEND): + if not target.is_relative_to(RUN_ROOT): + raise BootstrapError("write_escape") + self.grant("open", target, flags=flags) + elif not self.readable(target): + raise BootstrapError("unplanned_read") + if mode is None: + # Only the latest approved raw open may supply a wrapper intent. + self.open_intents[:] = [(target, flags & os.O_ACCMODE)] + else: + self.open_intents.clear() + elif event == "os.mkdir": + path, mode, dir_fd = args + if dir_fd not in (-1, None) or type(mode) is not int: + raise BootstrapError("directory_descriptor_forbidden") + self.grant(event, self.path(path), flags=mode) + elif event == "os.rename": + source, destination, source_fd, destination_fd = args + if source_fd not in (-1, None) or destination_fd not in (-1, None): + raise BootstrapError("rename_descriptor_forbidden") + self.grant(event, self.path(destination), flags=0, source=self.path(source)) + elif event in ("os.listdir", "os.scandir"): + target = self.path(args[0]) + if not target.is_relative_to(RUN_ROOT) and not (target.is_relative_to(self.runtime_root) and "site-packages" not in target.parts): + raise BootstrapError("unplanned_directory_read") + elif event.startswith("socket.") or event in {"subprocess.Popen", "os.fork", "os.forkpty", "os.posix_spawn", "os.spawn", "os.system", "os.exec", "_thread.start_new_thread", "ctypes.dlopen", "sqlite3.connect", "sqlite3.load_extension", "sqlite3.enable_load_extension", "os.remove", "os.rmdir", "os.link", "os.symlink", "os.truncate", "os.chmod", "os.chown", "os.chflags", "resource.setrlimit", "sys.addaudithook"}: + raise BootstrapError("forbidden_native_operation:" + event) + elif event in ("os.kill", "os.killpg"): + if event != "os.kill" or args != (os.getpid(), 0): + raise BootstrapError("external_signal_forbidden") + elif event == "import": + name = args[0] + if type(name) is not str: + raise BootstrapError("import_name_type") + if name.split(".")[0] in {"torch", "transformers", "mlx", "numpy", "sentence_transformers", "requests", "httpx", "ctypes", "psutil"}: + raise BootstrapError("model_network_dependency_forbidden") + if name.startswith("truememory.") and name not in {item[0] for item in SOURCE_MODULES} and name not in {"truememory.ingest", "truememory.ingest.hooks"}: + raise BootstrapError("unplanned_production_import") + + +def require(condition: bool, reason: str) -> None: + if not condition: + raise BootstrapError(reason) + + +def execute_case(case: dict, modules: dict[str, types.ModuleType], protocol: Protocol, policy: types.ModuleType) -> dict: + completion = modules["truememory.ingest.completion"] + platform = modules["truememory._platform"] + transcript = modules["truememory.ingest.transcript"] + provenance = modules["truememory.ingest.provenance"] + kind = case["id"] + details: dict[str, object] = {} + if kind in {"capture-a1", "capture-a2", "capture-revision", "invalid-state"}: + try: + capture = completion.admit_capture(case["source"], case["session"], "native-acceptance-v1", case["database"], "claude", root=RUN_ROOT / "completion") + except completion.CompletionError as error: + if kind != "invalid-state" or str(error) != "invalid_retained_state": + raise + details["category"] = str(error) + else: + require(kind != "invalid-state", "invalid_state_was_admitted") + require(capture.key == policy.digest(policy.encoded(policy.capture_metadata(case))), "capture_key_mismatch") + completion.verify_capture(capture) + raw = capture.path.read_bytes() + require(hashlib.sha256(raw).hexdigest() == case["source_sha256"] and len(raw) == case["source_bytes"], "capture_bytes_mismatch") + require(capture.metadata == policy.capture_metadata(case), "capture_scope_mismatch") + require(capture.path.stat().st_mode & 0o077 == 0, "capture_not_private") + second = completion.admit_capture(case["source"], case["session"], "native-acceptance-v1", case["database"], "claude", root=RUN_ROOT / "completion") + require(second.key == capture.key and second.path == capture.path, "capture_not_idempotent") + if kind == "capture-revision": + old = case["previous"] + old_path = capture.directory / "captures" / (old["key"] + ".jsonl") + previous = old_path.read_bytes() + require(capture.key != old["key"] and hashlib.sha256(previous).hexdigest() == old["sha256"] and len(previous) == old["byte_count"], "revision_lost_previous_evidence") + details.update({"category": "immutable_capture_verified", "capture_key": capture.key, "source_sha256": case["source_sha256"], "byte_count": len(raw), "idempotent": True}) + elif kind == "negative-a3": + base = RUN_ROOT / "home/.claude/projects/native-acceptance-v1" + controls = [(case["source"], "unknown", "claude", "source_not_admitted"), + (case["source"], case["session"], "unknown", "source_not_admitted"), + (str(RUN_ROOT / "fixtures/negative-outside-v1.jsonl"), case["session"], "claude", "source_not_admitted"), + (str(base / "negative-source-link-v1.jsonl"), case["session"], "claude", "source_link"), + (str(base / "negative-directory-v1.jsonl"), case["session"], "claude", "source_not_regular")] + categories = [] + for source, session, host, expected in controls: + try: + completion.admit_capture(source, session, "native-acceptance-v1", case["database"], host, root=RUN_ROOT / "completion") + except completion.CompletionError as error: + require(str(error) == expected, "negative_category_mismatch") + categories.append(str(error)) + else: + raise BootstrapError("negative_source_admitted") + details.update({"category": "source_negatives_verified", "categories": categories}) + elif kind == "flock-a4": + lock = RUN_ROOT / "locks/contention-v1.lock" + require(platform.try_file_lock(lock) is None, "contention_not_observed") + protocol.emit({"type": "lock_busy"}) + require(protocol.receive(2048, 0.1) == {"type": "lock_released"}, "lock_release_protocol") + fd = platform.try_file_lock(lock) + require(type(fd) is int and fd >= 0, "released_lock_not_acquired") + details.update({"category": "native_contention_then_acquired", "post_exit_release_requires_parent": True}) + # Leave the actual acquired descriptor open: the parent proves exit release. + elif kind == "parser-a5": + parsed = transcript.parse_transcript_outcome(Path(case["source"])) + require(parsed.status == "complete" and parsed.file_version is not None and parsed.file_version.stable, "parser_scope_unproven") + require(parsed.file_version.sha256 == case["source_sha256"], "parser_version_mismatch") + require(any(message.timestamp for message in parsed.messages) and any(not message.timestamp for message in parsed.messages), "fixture_lacks_known_unknown_times") + formatted = transcript.format_for_extraction(parsed.messages) + context = provenance.source_context(formatted, case["session"]) + require(context.length == len(formatted) and context.digest == hashlib.sha256(formatted.encode("utf-8")).hexdigest(), "provenance_context_mismatch") + require(provenance._known_time("") is None and provenance._known_time("2020-01-01T00:00:00Z") is not None, "source_time_guard_mismatch") + details.update({"category": "parser_and_source_context_verified", "messages": len(parsed.messages), "known_unknown_time": True, + "scorer_extractor_store_proven": False}) + else: + raise BootstrapError("unsupported_case") + return details + + +def main() -> int: + readback = arm_limits() + protocol = Protocol() + request = protocol.receive(LIMITS["input_bytes"], 1) + required = {"version", "source_root", "run_root", "home", "sources", "limits", "case", "supervisor_sha256"} + require(set(request) == required and type(request["version"]) is int and request["version"] == 1, "request_shape") + require(request["source_root"] == str(SOURCE_ROOT) and request["run_root"] == str(RUN_ROOT) and request["home"] == str(RUN_ROOT / "home") and os.environ.get("HOME") == request["home"], "request_roots") + require(request["limits"] == LIMITS and all(type(number) is int for number in request["limits"].values()), "request_limits") + require(type(request["sources"]) is dict and set(request["sources"]) == {relative for _, relative in SOURCE_MODULES}, "source_pin_set") + policy = compile_module("mac_native_policy_v2", SOURCE_ROOT / "tests/native/mac-native-acceptance-v2.py", request["supervisor_sha256"]) + policy.validate_case(request["case"]) + for checksum in request["sources"].values(): + policy.hash_text(checksum) + guard = AuditGuard(protocol, request, policy) + sys.addaudithook(guard) + sys.audit("truememory.native.audit_probe", "fixed-stage-a-v1") + require(guard.activated, "audit_hook_not_installed") + protocol.emit({"type": "ready", "version": 1, "pid": os.getpid(), "limits": readback, "audit_installed": True}) + for name, relative in (("truememory", "truememory"), ("truememory.ingest", "truememory/ingest"), ("truememory.ingest.hooks", "truememory/ingest/hooks")): + module = types.ModuleType(name) + module.__path__ = [str(SOURCE_ROOT / relative)] + module.__package__ = name + sys.modules[name] = module + modules = {name: compile_module(name, SOURCE_ROOT / relative, request["sources"][relative]) for name, relative in SOURCE_MODULES} + details = execute_case(request["case"], modules, protocol, policy) + protocol.emit({"type": "result", "version": 1, "case": request["case"]["id"], "passed": True, + "proof_scope": PROOF_SCOPE, "import_mode": "isolated_source_module", "sources": request["sources"], "details": details}) + if request["case"]["id"] == "flock-a4": + os._exit(73) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/native/mac-native-bootstrap-v4.py b/tests/native/mac-native-bootstrap-v4.py new file mode 100644 index 00000000..b3bc1b94 --- /dev/null +++ b/tests/native/mac-native-bootstrap-v4.py @@ -0,0 +1,557 @@ +"""Fixed Stage A child. Importing definitions never launches native work.""" +from __future__ import annotations + +import hashlib +import json +import os +import resource +import select +import signal +import stat +import sys +import time +import types +from pathlib import Path + +LIMITS = {"wall_seconds": 5, "termination_seconds": 1, "cpu_soft": 2, "cpu_hard": 3, + "rss_abort_bytes": 67108864, "core_bytes": 0, "nofile": 64, + "fsize_bytes": 1048576, "input_bytes": 65536, "output_bytes": 262144, + "events": 32, "frame_bytes": 2048, "ack_ms": 100, "threads": 1} +RUN_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/native-mac-v1/run-0001") +SOURCE_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/worktrees/mac-native-acceptance-v1") +PROOF_SCOPE = "instrumented_unmodified_source_native_filesystem_lock_capture" +SOURCE_MODULES = ( + ("truememory._platform", "truememory/_platform.py"), + ("truememory.ingest.hooks._shared", "truememory/ingest/hooks/_shared.py"), + ("truememory.ingest.completion", "truememory/ingest/completion.py"), + ("truememory.ingest.transcript", "truememory/ingest/transcript.py"), + ("truememory.ingest.provenance", "truememory/ingest/provenance.py"), +) + + +class BootstrapError(ValueError): + pass + + +def encoded(value: object) -> bytes: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8") + + +class Protocol: + def __init__(self) -> None: + self.buffer = bytearray() + self.read_bytes = 0 + self.written_bytes = 0 + + def emit(self, value: object) -> None: + data = encoded(value) + b"\n" + if len(data) > LIMITS["frame_bytes"]: + raise BootstrapError("output_frame_limit") + self.written_bytes += len(data) + if self.written_bytes > LIMITS["output_bytes"]: + raise BootstrapError("output_limit") + while data: + count = os.write(1, data) + if count <= 0: + raise BootstrapError("output_no_progress") + data = data[count:] + + def receive(self, maximum: int, seconds: float) -> dict: + deadline = time.monotonic() + seconds + while b"\n" not in self.buffer: + remaining = deadline - time.monotonic() + if remaining <= 0 or not select.select([0], [], [], remaining)[0]: + raise BootstrapError("input_deadline") + chunk = os.read(0, min(4096, maximum + 1)) + if not chunk: + raise BootstrapError("input_eof") + self.read_bytes += len(chunk) + self.buffer.extend(chunk) + if self.read_bytes > LIMITS["input_bytes"] or len(self.buffer) > maximum: + raise BootstrapError("input_limit") + line, _, remaining = self.buffer.partition(b"\n") + self.buffer = bytearray(remaining) + if len(line) > maximum: + raise BootstrapError("input_frame_limit") + value = json.loads(line) + if type(value) is not dict: + raise BootstrapError("input_shape") + return value + + +def arm_limits() -> dict[str, list[int]]: + if sys.platform != "darwin" or not sys.flags.isolated or not sys.flags.no_site or not sys.flags.dont_write_bytecode: + raise BootstrapError("isolated_mac_child_required") + os.umask(0o077) + sys.dont_write_bytecode = True + expected = {"cpu": (2, 3), "core": (0, 0), "nofile": (64, 64), "fsize": (1048576, 1048576)} + kinds = {"cpu": resource.RLIMIT_CPU, "core": resource.RLIMIT_CORE, + "nofile": resource.RLIMIT_NOFILE, "fsize": resource.RLIMIT_FSIZE} + for name, limits in expected.items(): + current = resource.getrlimit(kinds[name]) + if any(old != resource.RLIM_INFINITY and old < new for old, new in zip(current, limits)): + raise BootstrapError("existing_limit_below_reviewed_limit") + resource.setrlimit(kinds[name], limits) + if resource.getrlimit(kinds[name]) != limits: + raise BootstrapError("limit_readback_failed") + signal.signal(signal.SIGALRM, signal.SIG_DFL) + signal.setitimer(signal.ITIMER_REAL, 5) + return {name: list(limits) for name, limits in expected.items()} + + +def compile_module(name: str, path: Path, checksum: str, *, package: bool = False) -> types.ModuleType: + if path.is_symlink() or path.resolve() != path: + raise BootstrapError("source_path_link") + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + try: + before = os.fstat(fd) + if not stat.S_ISREG(before.st_mode) or before.st_size > 262144: + raise BootstrapError("source_read_type_or_limit") + stream = os.fdopen(fd, "rb") + fd = None + with stream: + data = stream.read(262145) + after = os.fstat(stream.fileno()) + finally: + if fd is not None: + os.close(fd) + if (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns, before.st_ctime_ns) != (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns, after.st_ctime_ns): + raise BootstrapError("source_changed_during_read") + if len(data) > 262144 or hashlib.sha256(data).hexdigest() != checksum: + raise BootstrapError("source_hash_changed") + module = types.ModuleType(name) + module.__file__ = str(path) + module.__package__ = name if package else name.rpartition(".")[0] + if package: + module.__path__ = [str(path.parent)] + sys.modules[name] = module + exec(compile(data, str(path), "exec", dont_inherit=True), module.__dict__) + return module + + +class AuditGuard: + """Bounded telemetry/grants for fixed trusted source, not a security sandbox.""" + def __init__(self, protocol: Protocol, request: dict, policy_module: types.ModuleType) -> None: + self.protocol, self.request, self.policy_module = protocol, request, policy_module + self.case = request["case"] + self.sequence = 0 + self.activated = False + self.approved: dict[Path, int] = {} + self.known_fds: dict[int, tuple[int, int]] = {} + self.open_intents: list[tuple[Path, int]] = [] + self.directory_sync: DirectorySyncAdapter | None = None + self.source = Path(self.case["source"]) if self.case["source"] is not None else None + self.read_paths = {SOURCE_ROOT / relative for _, relative in SOURCE_MODULES} + self.read_paths.add(SOURCE_ROOT / "tests/native/mac-native-acceptance-v4.py") + if self.source is not None: + self.read_paths.add(self.source) + if self.case["id"] == "negative-a3": + self.read_paths.add(RUN_ROOT / "home/.claude/projects/native-acceptance-v1/negative-directory-v1.jsonl") + if self.case["previous"] is not None: + self.read_paths.add(policy_module.account_directory(self.case) / "captures" / (self.case["previous"]["key"] + ".jsonl")) + self.runtime_root = Path(sys.base_prefix) / "lib/python3.12" + + def grant(self, event: str, path: Path, *, flags: int, source: Path | None = None) -> None: + self.sequence += 1 + if self.sequence > LIMITS["events"]: + raise BootstrapError("mutation_event_limit") + self.protocol.emit({"type": "mutation", "version": 1, "seq": self.sequence, + "event": event, "path": str(path), "source": str(source) if source is not None else None, + "flags": flags}) + answer = self.protocol.receive(LIMITS["frame_bytes"], LIMITS["ack_ms"] / 1000) + if set(answer) != {"type", "version", "seq", "path", "purpose"} or answer["type"] != "grant" or type(answer["version"]) is not int or answer["version"] != 1 or type(answer["seq"]) is not int or answer["seq"] != self.sequence or answer["path"] != str(path) or type(answer["purpose"]) is not str or not answer["purpose"]: + raise BootstrapError("grant_mismatch") + if event == "open": + self.approved[path] = flags & os.O_ACCMODE + elif event == "os.rename" and source is not None: + if source not in self.approved: + raise BootstrapError("replace_source_not_approved") + self.approved[path] = self.approved.pop(source) + + def path(self, value: object) -> Path: + if not isinstance(value, (str, bytes)): + raise BootstrapError("path_argument_type") + if isinstance(value, bytes): + value = value.decode("utf-8") + if "\0" in value or not Path(value).is_absolute() or ".." in Path(value).parts: + raise BootstrapError("path_not_absolute") + path = Path(value) + if str(path) != value or path.resolve() != path or any(part.is_symlink() for part in (path, *path.parents)): + raise BootstrapError("path_not_canonical") + return path + + def readable(self, path: Path) -> bool: + if path in self.read_paths or path in self.approved: + return True + account = self.policy_module.account_directory(self.case) + if path in {account / "state.json", account / "state.lock"}: + return True + if path.is_relative_to(self.runtime_root) and "site-packages" not in path.parts: + return True + return False + + def descriptor(self, fd: int, mode: object) -> None: + import fcntl + # CPython FileIO reports r/w for an explicitly binary rb/wb fd wrapper. + if type(fd) is not int or fd in (0, 1, 2) or type(mode) is not str or mode not in ("r", "rb", "w", "wb"): + raise BootstrapError("unsupported_descriptor_wrapper") + info = os.fstat(fd) + identity = (info.st_dev, info.st_ino) + if not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid(): + raise BootstrapError("descriptor_not_regular_owned") + access = fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_ACCMODE + if not self.open_intents: + raise BootstrapError("descriptor_not_preapproved") + path, allowed = self.open_intents.pop() + current = path.lstat() + if path.is_symlink() or not stat.S_ISREG(current.st_mode) or current.st_uid != os.getuid() or (current.st_dev, current.st_ino) != identity: + raise BootstrapError("descriptor_intent_identity_mismatch") + if allowed != access or (mode.startswith("r") and access != os.O_RDONLY) or (mode.startswith("w") and access != os.O_WRONLY): + raise BootstrapError("descriptor_access_mismatch") + if len(self.known_fds | {fd: identity}) > LIMITS["events"]: + raise BootstrapError("descriptor_inventory_limit") + self.known_fds[fd] = identity + + def __call__(self, event: str, args: tuple[object, ...]) -> None: + if event == "truememory.native.audit_probe": + if args != ("fixed-stage-a-v1",): + raise BootstrapError("audit_probe_mismatch") + self.activated = True + return + if event == "open": + path, mode, flags = args + if type(flags) is not int: + raise BootstrapError("open_flags_type") + if type(path) is int: + self.descriptor(path, mode) + return + target = self.path(path) + if flags & getattr(os, "O_DIRECTORY", 0): + if self.directory_sync is None: + raise BootstrapError("directory_sync_adapter_missing") + self.directory_sync.audit_open(target, mode, flags) + return + if flags & (os.O_CREAT | os.O_WRONLY | os.O_RDWR | os.O_TRUNC | os.O_APPEND): + if not target.is_relative_to(RUN_ROOT): + raise BootstrapError("write_escape") + self.grant("open", target, flags=flags) + elif not self.readable(target): + raise BootstrapError("unplanned_read") + if mode is None: + # Only the latest approved raw open may supply a wrapper intent. + self.open_intents[:] = [(target, flags & os.O_ACCMODE)] + else: + self.open_intents.clear() + elif event == "os.mkdir": + path, mode, dir_fd = args + if dir_fd not in (-1, None) or type(mode) is not int: + raise BootstrapError("directory_descriptor_forbidden") + self.grant(event, self.path(path), flags=mode) + elif event == "os.rename": + source, destination, source_fd, destination_fd = args + if source_fd not in (-1, None) or destination_fd not in (-1, None): + raise BootstrapError("rename_descriptor_forbidden") + self.grant(event, self.path(destination), flags=0, source=self.path(source)) + elif event in ("os.listdir", "os.scandir"): + target = self.path(args[0]) + if not target.is_relative_to(RUN_ROOT) and not (target.is_relative_to(self.runtime_root) and "site-packages" not in target.parts): + raise BootstrapError("unplanned_directory_read") + elif event.startswith("socket.") or event in {"subprocess.Popen", "os.fork", "os.forkpty", "os.posix_spawn", "os.spawn", "os.system", "os.exec", "_thread.start_new_thread", "ctypes.dlopen", "sqlite3.connect", "sqlite3.load_extension", "sqlite3.enable_load_extension", "os.remove", "os.rmdir", "os.link", "os.symlink", "os.truncate", "os.chmod", "os.chown", "os.chflags", "resource.setrlimit", "sys.addaudithook"}: + raise BootstrapError("forbidden_native_operation:" + event) + elif event in ("os.kill", "os.killpg"): + if event != "os.kill" or args != (os.getpid(), 0): + raise BootstrapError("external_signal_forbidden") + elif event == "import": + name = args[0] + if type(name) is not str: + raise BootstrapError("import_name_type") + if name.split(".")[0] in {"torch", "transformers", "mlx", "numpy", "sentence_transformers", "requests", "httpx", "ctypes", "psutil"}: + raise BootstrapError("model_network_dependency_forbidden") + if name.startswith("truememory.") and name not in {item[0] for item in SOURCE_MODULES} and name not in {"truememory.ingest", "truememory.ingest.hooks"}: + raise BootstrapError("unplanned_production_import") + + +class DirectorySyncAdapter: + """Bind the actual source helper's directory handle, without rewriting it.""" + def __init__(self, guard: AuditGuard, completion: types.ModuleType) -> None: + self.guard = guard + self.completion = completion + self.function = completion._sync_directory + if (type(self.function) is not types.FunctionType + or self.function.__globals__ is not completion.__dict__ + or self.function.__code__.co_filename != str(SOURCE_ROOT / "truememory/ingest/completion.py")): + raise BootstrapError("directory_sync_source_identity") + self.raw_open, self.raw_fsync, self.raw_close = os.open, os.fsync, os.close + self.pending: tuple[Path, int, tuple[int, int]] | None = None + self.granted = False + self.bindings: dict[int, tuple[Path, tuple[int, int], int]] = {} + self.failed_open_bindings: dict[int, tuple[int, int, int, int] | None] = {} + self.failures: list[dict[str, object]] = [] + self.installed = False + + def source_caller(self, frame: types.FrameType) -> bool: + return frame.f_code is self.function.__code__ and frame.f_globals is self.completion.__dict__ + + def target(self, value: object, flags: object) -> tuple[Path, tuple[int, int]]: + directory_flag = getattr(os, "O_DIRECTORY", 0) + expected = os.O_RDONLY | directory_flag + if (os.name == "nt" or not directory_flag or type(flags) is not int + or flags & ~getattr(os, "O_CLOEXEC", 0) != expected + or self.guard.case["id"] not in {"capture-a1", "capture-a2", "capture-revision"}): + raise BootstrapError("unsupported_directory_sync_flags_or_case") + if isinstance(value, os.PathLike): + value = os.fspath(value) + path = self.guard.path(value) + account = self.guard.policy_module.account_directory(self.guard.case) + if path not in {account, account / "captures"}: + raise BootstrapError("unplanned_directory_sync_target") + info = self.guard.policy_module.owned_path(path, directory=True) + if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() or info.st_mode & 0o077: + raise BootstrapError("directory_sync_target_not_private_owned") + return path, (info.st_dev, info.st_ino) + + def audit_open(self, path: Path, mode: object, flags: int) -> None: + if self.pending is None or mode is not None or self.granted: + raise BootstrapError("directory_sync_not_in_source_open") + target, expected_flags, expected_identity = self.pending + current, identity = self.target(path, flags) + if current != target or flags & ~getattr(os, "O_CLOEXEC", 0) != expected_flags or identity != expected_identity: + raise BootstrapError("directory_sync_preopen_changed") + self.guard.grant("open", target, flags=flags) + # Directory grants do not authorize ordinary reads or fdopen wrappers. + self.guard.approved.pop(target, None) + self.granted = True + + def verify(self, fd: int, path: Path, identity: tuple[int, int], *, check_path: bool) -> os.stat_result: + import fcntl + if type(fd) is not int or fd < 3: + raise BootstrapError("directory_sync_descriptor_type") + info = os.fstat(fd) + if (not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() + or info.st_mode & 0o077 or (info.st_dev, info.st_ino) != identity + or fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_ACCMODE != os.O_RDONLY): + raise BootstrapError("directory_sync_descriptor_changed") + if check_path: + current = self.guard.policy_module.owned_path(path, directory=True) + if (not stat.S_ISDIR(current.st_mode) or current.st_uid != os.getuid() + or current.st_mode & 0o077 or (current.st_dev, current.st_ino) != identity): + raise BootstrapError("directory_sync_path_changed") + return info + + def failure(self, fd: int, reason: str, *, closed: bool) -> None: + if len(self.failures) >= LIMITS["events"]: + raise BootstrapError("directory_sync_failure_inventory") + self.failures.append({"fd": fd, "reason": reason, "closed": closed}) + + def close_failed_open(self, fd: int, observed: os.stat_result | None, error: BaseException) -> None: + if fd in self.bindings: + self.failure(fd, "returned_descriptor_bound_conflict:" + type(error).__name__, closed=False) + return + if len(self.failed_open_bindings) >= LIMITS["events"]: + raise BootstrapError("directory_sync_failed_binding_inventory") + if observed is None: + self.failed_open_bindings[fd] = None + self.failure(fd, "returned_descriptor_identity_unavailable:" + type(error).__name__, closed=False) + return + try: + current = os.fstat(fd) + if ((current.st_dev, current.st_ino, stat.S_IFMT(current.st_mode), current.st_uid) + != (observed.st_dev, observed.st_ino, stat.S_IFMT(observed.st_mode), observed.st_uid)): + raise BootstrapError("failed_open_descriptor_reused") + self.raw_close(fd) + except (OSError, BootstrapError) as close_error: + self.failed_open_bindings[fd] = (observed.st_dev, observed.st_ino, stat.S_IFMT(observed.st_mode), observed.st_uid) + self.failure(fd, "returned_descriptor_close_unproven:" + type(close_error).__name__, closed=False) + raise BootstrapError("directory_sync_failed_open_close_unproven") from close_error + self.failure(fd, type(error).__name__, closed=True) + + def open(self, path: object, flags: int, mode: int = 0o777, *, dir_fd: int | None = None) -> int: + if not self.source_caller(sys._getframe(1)): + if type(flags) is int and flags & getattr(os, "O_DIRECTORY", 0): + raise BootstrapError("directory_sync_foreign_caller") + if dir_fd is None: + return self.raw_open(path, flags, mode) + return self.raw_open(path, flags, mode, dir_fd=dir_fd) + if type(mode) is not int or mode != 0o777 or dir_fd is not None or self.pending is not None: + raise BootstrapError("directory_sync_open_arguments") + target, identity = self.target(path, flags) + if len(self.bindings) >= LIMITS["events"]: + raise BootstrapError("directory_sync_binding_inventory") + self.pending = (target, flags & ~getattr(os, "O_CLOEXEC", 0), identity) + self.granted = False + fd = None + observed = None + try: + fd = self.raw_open(path, flags, mode) + if type(fd) is not int or fd < 3: + raise BootstrapError("directory_sync_open_return") + observed = os.fstat(fd) + if not self.granted: + raise BootstrapError("directory_sync_grant_missing") + self.verify(fd, target, identity, check_path=True) + if fd in self.bindings: + raise BootstrapError("directory_sync_returned_fd_reused") + self.bindings[fd] = (target, identity, self.guard.sequence) + return fd + except (OSError, BootstrapError) as error: + if type(fd) is int and fd >= 3: + self.close_failed_open(fd, observed, error) + raise + finally: + self.pending = None + self.granted = False + + def fsync(self, fd: int) -> None: + if not self.source_caller(sys._getframe(1)): + if fd in self.bindings: + raise BootstrapError("directory_sync_foreign_fsync") + self.raw_fsync(fd) + return + if type(fd) is not int or fd not in self.bindings: + raise BootstrapError("directory_sync_fsync_unbound") + path, identity, sequence = self.bindings[fd] + if not 1 <= sequence <= self.guard.sequence <= LIMITS["events"]: + raise BootstrapError("directory_sync_grant_binding") + self.verify(fd, path, identity, check_path=True) + self.raw_fsync(fd) + + def close(self, fd: int) -> None: + if not self.source_caller(sys._getframe(1)): + if fd in self.bindings: + raise BootstrapError("directory_sync_foreign_close") + self.raw_close(fd) + return + if type(fd) is not int or fd not in self.bindings: + raise BootstrapError("directory_sync_close_unbound") + path, identity, _ = self.bindings[fd] + try: + # A replaced directory path still permits closing our original handle. + self.verify(fd, path, identity, check_path=False) + self.raw_close(fd) + except (OSError, BootstrapError) as error: + self.failure(fd, "bound_descriptor_close_unproven:" + type(error).__name__, closed=False) + raise + del self.bindings[fd] + + def install(self) -> None: + if self.installed or self.guard.directory_sync is not None: + raise BootstrapError("directory_sync_adapter_already_installed") + self.guard.directory_sync = self + os.open, os.fsync, os.close = self.open, self.fsync, self.close + self.installed = True + + +def require(condition: bool, reason: str) -> None: + if not condition: + raise BootstrapError(reason) + + +def execute_case(case: dict, modules: dict[str, types.ModuleType], protocol: Protocol, policy: types.ModuleType) -> dict: + completion = modules["truememory.ingest.completion"] + platform = modules["truememory._platform"] + transcript = modules["truememory.ingest.transcript"] + provenance = modules["truememory.ingest.provenance"] + kind = case["id"] + details: dict[str, object] = {} + if kind in {"capture-a1", "capture-a2", "capture-revision", "invalid-state"}: + try: + capture = completion.admit_capture(case["source"], case["session"], "native-acceptance-v1", case["database"], "claude", root=RUN_ROOT / "completion") + except completion.CompletionError as error: + if kind != "invalid-state" or str(error) != "invalid_retained_state": + raise + details["category"] = str(error) + else: + require(kind != "invalid-state", "invalid_state_was_admitted") + require(capture.key == policy.digest(policy.encoded(policy.capture_metadata(case))), "capture_key_mismatch") + completion.verify_capture(capture) + raw = capture.path.read_bytes() + require(hashlib.sha256(raw).hexdigest() == case["source_sha256"] and len(raw) == case["source_bytes"], "capture_bytes_mismatch") + require(capture.metadata == policy.capture_metadata(case), "capture_scope_mismatch") + require(capture.path.stat().st_mode & 0o077 == 0, "capture_not_private") + second = completion.admit_capture(case["source"], case["session"], "native-acceptance-v1", case["database"], "claude", root=RUN_ROOT / "completion") + require(second.key == capture.key and second.path == capture.path, "capture_not_idempotent") + if kind == "capture-revision": + old = case["previous"] + old_path = capture.directory / "captures" / (old["key"] + ".jsonl") + previous = old_path.read_bytes() + require(capture.key != old["key"] and hashlib.sha256(previous).hexdigest() == old["sha256"] and len(previous) == old["byte_count"], "revision_lost_previous_evidence") + details.update({"category": "immutable_capture_verified", "capture_key": capture.key, "source_sha256": case["source_sha256"], "byte_count": len(raw), "idempotent": True}) + elif kind == "negative-a3": + base = RUN_ROOT / "home/.claude/projects/native-acceptance-v1" + controls = [(case["source"], "unknown", "claude", "source_not_admitted"), + (case["source"], case["session"], "unknown", "source_not_admitted"), + (str(RUN_ROOT / "fixtures/negative-outside-v1.jsonl"), case["session"], "claude", "source_not_admitted"), + (str(base / "negative-source-link-v1.jsonl"), case["session"], "claude", "source_link"), + (str(base / "negative-directory-v1.jsonl"), case["session"], "claude", "source_not_regular")] + categories = [] + for source, session, host, expected in controls: + try: + completion.admit_capture(source, session, "native-acceptance-v1", case["database"], host, root=RUN_ROOT / "completion") + except completion.CompletionError as error: + require(str(error) == expected, "negative_category_mismatch") + categories.append(str(error)) + else: + raise BootstrapError("negative_source_admitted") + details.update({"category": "source_negatives_verified", "categories": categories}) + elif kind == "flock-a4": + lock = RUN_ROOT / "locks/contention-v1.lock" + require(platform.try_file_lock(lock) is None, "contention_not_observed") + protocol.emit({"type": "lock_busy"}) + require(protocol.receive(2048, 0.1) == {"type": "lock_released"}, "lock_release_protocol") + fd = platform.try_file_lock(lock) + require(type(fd) is int and fd >= 0, "released_lock_not_acquired") + details.update({"category": "native_contention_then_acquired", "post_exit_release_requires_parent": True}) + # Leave the actual acquired descriptor open: the parent proves exit release. + elif kind == "parser-a5": + parsed = transcript.parse_transcript_outcome(Path(case["source"])) + require(parsed.status == "complete" and parsed.file_version is not None and parsed.file_version.stable, "parser_scope_unproven") + require(parsed.file_version.sha256 == case["source_sha256"], "parser_version_mismatch") + require(any(message.timestamp for message in parsed.messages) and any(not message.timestamp for message in parsed.messages), "fixture_lacks_known_unknown_times") + formatted = transcript.format_for_extraction(parsed.messages) + context = provenance.source_context(formatted, case["session"]) + require(context.length == len(formatted) and context.digest == hashlib.sha256(formatted.encode("utf-8")).hexdigest(), "provenance_context_mismatch") + require(provenance._known_time("") is None and provenance._known_time("2020-01-01T00:00:00Z") is not None, "source_time_guard_mismatch") + details.update({"category": "parser_and_source_context_verified", "messages": len(parsed.messages), "known_unknown_time": True, + "scorer_extractor_store_proven": False}) + else: + raise BootstrapError("unsupported_case") + return details + + +def main() -> int: + readback = arm_limits() + protocol = Protocol() + request = protocol.receive(LIMITS["input_bytes"], 1) + required = {"version", "source_root", "run_root", "home", "sources", "limits", "case", "supervisor_sha256"} + require(set(request) == required and type(request["version"]) is int and request["version"] == 1, "request_shape") + require(request["source_root"] == str(SOURCE_ROOT) and request["run_root"] == str(RUN_ROOT) and request["home"] == str(RUN_ROOT / "home") and os.environ.get("HOME") == request["home"], "request_roots") + require(request["limits"] == LIMITS and all(type(number) is int for number in request["limits"].values()), "request_limits") + require(type(request["sources"]) is dict and set(request["sources"]) == {relative for _, relative in SOURCE_MODULES}, "source_pin_set") + policy = compile_module("mac_native_policy_v4", SOURCE_ROOT / "tests/native/mac-native-acceptance-v4.py", request["supervisor_sha256"]) + policy.validate_case(request["case"]) + for checksum in request["sources"].values(): + policy.hash_text(checksum) + guard = AuditGuard(protocol, request, policy) + sys.addaudithook(guard) + sys.audit("truememory.native.audit_probe", "fixed-stage-a-v1") + require(guard.activated, "audit_hook_not_installed") + protocol.emit({"type": "ready", "version": 1, "pid": os.getpid(), "limits": readback, "audit_installed": True}) + for name, relative in (("truememory", "truememory"), ("truememory.ingest", "truememory/ingest"), ("truememory.ingest.hooks", "truememory/ingest/hooks")): + module = types.ModuleType(name) + module.__path__ = [str(SOURCE_ROOT / relative)] + module.__package__ = name + sys.modules[name] = module + modules = {name: compile_module(name, SOURCE_ROOT / relative, request["sources"][relative]) for name, relative in SOURCE_MODULES} + directory_sync = DirectorySyncAdapter(guard, modules["truememory.ingest.completion"]) + directory_sync.install() + details = execute_case(request["case"], modules, protocol, policy) + require(not directory_sync.bindings and not directory_sync.failed_open_bindings and not directory_sync.failures, "directory_sync_nonterminal") + protocol.emit({"type": "result", "version": 1, "case": request["case"]["id"], "passed": True, + "proof_scope": PROOF_SCOPE, "import_mode": "isolated_source_module", "sources": request["sources"], "details": details}) + if request["case"]["id"] == "flock-a4": + os._exit(73) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/native/mac-native-bootstrap-v6.py b/tests/native/mac-native-bootstrap-v6.py new file mode 100644 index 00000000..8051687c --- /dev/null +++ b/tests/native/mac-native-bootstrap-v6.py @@ -0,0 +1,918 @@ +"""Fixed Stage A child. Importing definitions never launches native work.""" +from __future__ import annotations + +import hashlib +import io +import json +import os +import resource +import select +import signal +import stat +import sys +import time +import types +from pathlib import Path + +LIMITS = {"wall_seconds": 5, "termination_seconds": 1, "cpu_soft": 2, "cpu_hard": 3, + "rss_abort_bytes": 67108864, "core_bytes": 0, "nofile": 64, + "fsize_bytes": 1048576, "input_bytes": 65536, "output_bytes": 262144, + "events": 32, "frame_bytes": 2048, "ack_ms": 100, "threads": 1} +RUN_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/native-mac-v1/run-0001") +SOURCE_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/worktrees/mac-native-acceptance-v1") +PROOF_SCOPE = "instrumented_unmodified_source_native_filesystem_lock_capture" +SOURCE_MODULES = ( + ("truememory._platform", "truememory/_platform.py"), + ("truememory.ingest.hooks._shared", "truememory/ingest/hooks/_shared.py"), + ("truememory.ingest.completion", "truememory/ingest/completion.py"), + ("truememory.ingest.transcript", "truememory/ingest/transcript.py"), + ("truememory.ingest.provenance", "truememory/ingest/provenance.py"), +) + + +class BootstrapError(ValueError): + pass + + +def encoded(value: object) -> bytes: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8") + + +class Protocol: + def __init__(self) -> None: + self.buffer = bytearray() + self.read_bytes = 0 + self.written_bytes = 0 + + def emit(self, value: object) -> None: + data = encoded(value) + b"\n" + if len(data) > LIMITS["frame_bytes"]: + raise BootstrapError("output_frame_limit") + self.written_bytes += len(data) + if self.written_bytes > LIMITS["output_bytes"]: + raise BootstrapError("output_limit") + while data: + count = os.write(1, data) + if count <= 0: + raise BootstrapError("output_no_progress") + data = data[count:] + + def receive(self, maximum: int, seconds: float) -> dict: + deadline = time.monotonic() + seconds + while b"\n" not in self.buffer: + remaining = deadline - time.monotonic() + if remaining <= 0 or not select.select([0], [], [], remaining)[0]: + raise BootstrapError("input_deadline") + chunk = os.read(0, min(4096, maximum + 1)) + if not chunk: + raise BootstrapError("input_eof") + self.read_bytes += len(chunk) + self.buffer.extend(chunk) + if self.read_bytes > LIMITS["input_bytes"] or len(self.buffer) > maximum: + raise BootstrapError("input_limit") + line, _, remaining = self.buffer.partition(b"\n") + self.buffer = bytearray(remaining) + if len(line) > maximum: + raise BootstrapError("input_frame_limit") + value = json.loads(line) + if type(value) is not dict: + raise BootstrapError("input_shape") + return value + + +def arm_limits() -> dict[str, list[int]]: + if sys.platform != "darwin" or not sys.flags.isolated or not sys.flags.no_site or not sys.flags.dont_write_bytecode: + raise BootstrapError("isolated_mac_child_required") + os.umask(0o077) + sys.dont_write_bytecode = True + expected = {"cpu": (2, 3), "core": (0, 0), "nofile": (64, 64), "fsize": (1048576, 1048576)} + kinds = {"cpu": resource.RLIMIT_CPU, "core": resource.RLIMIT_CORE, + "nofile": resource.RLIMIT_NOFILE, "fsize": resource.RLIMIT_FSIZE} + for name, limits in expected.items(): + current = resource.getrlimit(kinds[name]) + if any(old != resource.RLIM_INFINITY and old < new for old, new in zip(current, limits)): + raise BootstrapError("existing_limit_below_reviewed_limit") + resource.setrlimit(kinds[name], limits) + if resource.getrlimit(kinds[name]) != limits: + raise BootstrapError("limit_readback_failed") + signal.signal(signal.SIGALRM, signal.SIG_DFL) + signal.setitimer(signal.ITIMER_REAL, 5) + return {name: list(limits) for name, limits in expected.items()} + + +def compile_module(name: str, path: Path, checksum: str, *, package: bool = False) -> types.ModuleType: + if path.is_symlink() or path.resolve() != path: + raise BootstrapError("source_path_link") + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + try: + before = os.fstat(fd) + if not stat.S_ISREG(before.st_mode) or before.st_size > 262144: + raise BootstrapError("source_read_type_or_limit") + stream = os.fdopen(fd, "rb") + fd = None + with stream: + data = stream.read(262145) + after = os.fstat(stream.fileno()) + finally: + if fd is not None: + os.close(fd) + if (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns, before.st_ctime_ns) != (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns, after.st_ctime_ns): + raise BootstrapError("source_changed_during_read") + if len(data) > 262144 or hashlib.sha256(data).hexdigest() != checksum: + raise BootstrapError("source_hash_changed") + module = types.ModuleType(name) + module.__file__ = str(path) + module.__package__ = name if package else name.rpartition(".")[0] + if package: + module.__path__ = [str(path.parent)] + sys.modules[name] = module + exec(compile(data, str(path), "exec", dont_inherit=True), module.__dict__) + return module + + +class AuditGuard: + """Bounded telemetry/grants for fixed trusted source, not a security sandbox.""" + def __init__(self, protocol: Protocol, request: dict, policy_module: types.ModuleType) -> None: + self.protocol, self.request, self.policy_module = protocol, request, policy_module + self.case = request["case"] + self.sequence = 0 + self.activated = False + self.approved: dict[Path, int] = {} + self.known_fds: dict[int, tuple[int, int]] = {} + self.open_intents: list[tuple[Path, int]] = [] + self.directory_sync: DirectorySyncAdapter | None = None + self.capture_reads: RetainedCaptureReadAdapter | None = None + self.source = Path(self.case["source"]) if self.case["source"] is not None else None + self.read_paths = {SOURCE_ROOT / relative for _, relative in SOURCE_MODULES} + self.read_paths.add(SOURCE_ROOT / "tests/native/mac-native-acceptance-v6.py") + if self.source is not None: + self.read_paths.add(self.source) + if self.case["id"] == "negative-a3": + self.read_paths.add(RUN_ROOT / "home/.claude/projects/native-acceptance-v1/negative-directory-v1.jsonl") + if self.case["previous"] is not None: + self.read_paths.add(policy_module.account_directory(self.case) / "captures" / (self.case["previous"]["key"] + ".jsonl")) + self.runtime_root = Path(sys.base_prefix) / "lib/python3.12" + + def grant(self, event: str, path: Path, *, flags: int, source: Path | None = None) -> None: + self.sequence += 1 + if self.sequence > LIMITS["events"]: + raise BootstrapError("mutation_event_limit") + self.protocol.emit({"type": "mutation", "version": 1, "seq": self.sequence, + "event": event, "path": str(path), "source": str(source) if source is not None else None, + "flags": flags}) + answer = self.protocol.receive(LIMITS["frame_bytes"], LIMITS["ack_ms"] / 1000) + if set(answer) != {"type", "version", "seq", "path", "purpose"} or answer["type"] != "grant" or type(answer["version"]) is not int or answer["version"] != 1 or type(answer["seq"]) is not int or answer["seq"] != self.sequence or answer["path"] != str(path) or type(answer["purpose"]) is not str or not answer["purpose"]: + raise BootstrapError("grant_mismatch") + if event == "open": + self.approved[path] = flags & os.O_ACCMODE + elif event == "os.rename" and source is not None: + if source not in self.approved: + raise BootstrapError("replace_source_not_approved") + self.approved[path] = self.approved.pop(source) + + def path(self, value: object) -> Path: + if not isinstance(value, (str, bytes)): + raise BootstrapError("path_argument_type") + if isinstance(value, bytes): + value = value.decode("utf-8") + if "\0" in value or not Path(value).is_absolute() or ".." in Path(value).parts: + raise BootstrapError("path_not_absolute") + path = Path(value) + if str(path) != value or path.resolve() != path or any(part.is_symlink() for part in (path, *path.parents)): + raise BootstrapError("path_not_canonical") + return path + + def readable(self, path: Path) -> bool: + if self.capture_reads is not None and self.capture_reads.capture_path(path) is not None: + return False + if path in self.read_paths or path in self.approved: + return True + account = self.policy_module.account_directory(self.case) + if path in {account / "state.json", account / "state.lock"}: + return True + if path.is_relative_to(self.runtime_root) and "site-packages" not in path.parts: + return True + return False + + def descriptor(self, fd: int, mode: object) -> None: + import fcntl + if self.capture_reads is not None and self.open_intents and self.capture_reads.capture_path(self.open_intents[-1][0]) is not None: + self.capture_reads.descriptor(fd, mode) + # CPython FileIO reports r/w for an explicitly binary rb/wb fd wrapper. + if type(fd) is not int or fd in (0, 1, 2) or type(mode) is not str or mode not in ("r", "rb", "w", "wb"): + raise BootstrapError("unsupported_descriptor_wrapper") + info = os.fstat(fd) + identity = (info.st_dev, info.st_ino) + if not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid(): + raise BootstrapError("descriptor_not_regular_owned") + access = fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_ACCMODE + if not self.open_intents: + raise BootstrapError("descriptor_not_preapproved") + path, allowed = self.open_intents.pop() + current = path.lstat() + if path.is_symlink() or not stat.S_ISREG(current.st_mode) or current.st_uid != os.getuid() or (current.st_dev, current.st_ino) != identity: + raise BootstrapError("descriptor_intent_identity_mismatch") + if allowed != access or (mode.startswith("r") and access != os.O_RDONLY) or (mode.startswith("w") and access != os.O_WRONLY): + raise BootstrapError("descriptor_access_mismatch") + if len(self.known_fds | {fd: identity}) > LIMITS["events"]: + raise BootstrapError("descriptor_inventory_limit") + self.known_fds[fd] = identity + + def __call__(self, event: str, args: tuple[object, ...]) -> None: + if event == "truememory.native.audit_probe": + if args != ("fixed-stage-a-v1",): + raise BootstrapError("audit_probe_mismatch") + self.activated = True + return + if event == "open": + path, mode, flags = args + if type(flags) is not int: + raise BootstrapError("open_flags_type") + if type(path) is int: + self.descriptor(path, mode) + return + target = self.path(path) + if flags & getattr(os, "O_DIRECTORY", 0): + if self.directory_sync is None: + raise BootstrapError("directory_sync_adapter_missing") + self.directory_sync.audit_open(target, mode, flags) + return + if (self.capture_reads is not None and self.capture_reads.capture_path(target) is not None + and not flags & (os.O_CREAT | os.O_WRONLY | os.O_RDWR | os.O_TRUNC | os.O_APPEND)): + self.capture_reads.audit_open(target, mode, flags) + if mode is None: + self.open_intents[:] = [(target, os.O_RDONLY)] + else: + self.open_intents.clear() + return + if flags & (os.O_CREAT | os.O_WRONLY | os.O_RDWR | os.O_TRUNC | os.O_APPEND): + if not target.is_relative_to(RUN_ROOT): + raise BootstrapError("write_escape") + self.grant("open", target, flags=flags) + elif not self.readable(target): + raise BootstrapError("unplanned_read") + if mode is None: + # Only the latest approved raw open may supply a wrapper intent. + self.open_intents[:] = [(target, flags & os.O_ACCMODE)] + else: + self.open_intents.clear() + elif event == "os.mkdir": + path, mode, dir_fd = args + if dir_fd not in (-1, None) or type(mode) is not int: + raise BootstrapError("directory_descriptor_forbidden") + self.grant(event, self.path(path), flags=mode) + elif event == "os.rename": + source, destination, source_fd, destination_fd = args + if source_fd not in (-1, None) or destination_fd not in (-1, None): + raise BootstrapError("rename_descriptor_forbidden") + self.grant(event, self.path(destination), flags=0, source=self.path(source)) + elif event in ("os.listdir", "os.scandir"): + target = self.path(args[0]) + if not target.is_relative_to(RUN_ROOT) and not (target.is_relative_to(self.runtime_root) and "site-packages" not in target.parts): + raise BootstrapError("unplanned_directory_read") + elif event.startswith("socket.") or event in {"subprocess.Popen", "os.fork", "os.forkpty", "os.posix_spawn", "os.spawn", "os.system", "os.exec", "_thread.start_new_thread", "ctypes.dlopen", "sqlite3.connect", "sqlite3.load_extension", "sqlite3.enable_load_extension", "os.remove", "os.rmdir", "os.link", "os.symlink", "os.truncate", "os.chmod", "os.chown", "os.chflags", "resource.setrlimit", "sys.addaudithook"}: + raise BootstrapError("forbidden_native_operation:" + event) + elif event in ("os.kill", "os.killpg"): + if event != "os.kill" or args != (os.getpid(), 0): + raise BootstrapError("external_signal_forbidden") + elif event == "import": + name = args[0] + if type(name) is not str: + raise BootstrapError("import_name_type") + if name.split(".")[0] in {"torch", "transformers", "mlx", "numpy", "sentence_transformers", "requests", "httpx", "ctypes", "psutil"}: + raise BootstrapError("model_network_dependency_forbidden") + if name.startswith("truememory.") and name not in {item[0] for item in SOURCE_MODULES} and name not in {"truememory.ingest", "truememory.ingest.hooks"}: + raise BootstrapError("unplanned_production_import") + + +class DirectorySyncAdapter: + """Bind the actual source helper's directory handle, without rewriting it.""" + def __init__(self, guard: AuditGuard, completion: types.ModuleType) -> None: + self.guard = guard + self.completion = completion + self.function = completion._sync_directory + if (type(self.function) is not types.FunctionType + or self.function.__globals__ is not completion.__dict__ + or self.function.__code__.co_filename != str(SOURCE_ROOT / "truememory/ingest/completion.py")): + raise BootstrapError("directory_sync_source_identity") + self.raw_open, self.raw_fsync, self.raw_close = os.open, os.fsync, os.close + self.pending: tuple[Path, int, tuple[int, int]] | None = None + self.granted = False + self.bindings: dict[int, tuple[Path, tuple[int, int], int]] = {} + self.failed_open_bindings: dict[int, tuple[int, int, int, int] | None] = {} + self.failures: list[dict[str, object]] = [] + self.installed = False + + def source_caller(self, frame: types.FrameType) -> bool: + return frame.f_code is self.function.__code__ and frame.f_globals is self.completion.__dict__ + + def target(self, value: object, flags: object) -> tuple[Path, tuple[int, int]]: + directory_flag = getattr(os, "O_DIRECTORY", 0) + expected = os.O_RDONLY | directory_flag + if (os.name == "nt" or not directory_flag or type(flags) is not int + or flags & ~getattr(os, "O_CLOEXEC", 0) != expected + or self.guard.case["id"] not in {"capture-a1", "capture-a2", "capture-revision"}): + raise BootstrapError("unsupported_directory_sync_flags_or_case") + if isinstance(value, os.PathLike): + value = os.fspath(value) + path = self.guard.path(value) + account = self.guard.policy_module.account_directory(self.guard.case) + if path not in {account, account / "captures"}: + raise BootstrapError("unplanned_directory_sync_target") + info = self.guard.policy_module.owned_path(path, directory=True) + if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() or info.st_mode & 0o077: + raise BootstrapError("directory_sync_target_not_private_owned") + return path, (info.st_dev, info.st_ino) + + def audit_open(self, path: Path, mode: object, flags: int) -> None: + if self.pending is None or mode is not None or self.granted: + raise BootstrapError("directory_sync_not_in_source_open") + target, expected_flags, expected_identity = self.pending + current, identity = self.target(path, flags) + if current != target or flags & ~getattr(os, "O_CLOEXEC", 0) != expected_flags or identity != expected_identity: + raise BootstrapError("directory_sync_preopen_changed") + self.guard.grant("open", target, flags=flags) + # Directory grants do not authorize ordinary reads or fdopen wrappers. + self.guard.approved.pop(target, None) + self.granted = True + + def verify(self, fd: int, path: Path, identity: tuple[int, int], *, check_path: bool) -> os.stat_result: + import fcntl + if type(fd) is not int or fd < 3: + raise BootstrapError("directory_sync_descriptor_type") + info = os.fstat(fd) + if (not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() + or info.st_mode & 0o077 or (info.st_dev, info.st_ino) != identity + or fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_ACCMODE != os.O_RDONLY): + raise BootstrapError("directory_sync_descriptor_changed") + if check_path: + current = self.guard.policy_module.owned_path(path, directory=True) + if (not stat.S_ISDIR(current.st_mode) or current.st_uid != os.getuid() + or current.st_mode & 0o077 or (current.st_dev, current.st_ino) != identity): + raise BootstrapError("directory_sync_path_changed") + return info + + def failure(self, fd: int, reason: str, *, closed: bool) -> None: + if len(self.failures) >= LIMITS["events"]: + raise BootstrapError("directory_sync_failure_inventory") + self.failures.append({"fd": fd, "reason": reason, "closed": closed}) + + def close_failed_open(self, fd: int, observed: os.stat_result | None, error: BaseException) -> None: + if fd in self.bindings: + self.failure(fd, "returned_descriptor_bound_conflict:" + type(error).__name__, closed=False) + return + if len(self.failed_open_bindings) >= LIMITS["events"]: + raise BootstrapError("directory_sync_failed_binding_inventory") + if observed is None: + self.failed_open_bindings[fd] = None + self.failure(fd, "returned_descriptor_identity_unavailable:" + type(error).__name__, closed=False) + return + try: + current = os.fstat(fd) + if ((current.st_dev, current.st_ino, stat.S_IFMT(current.st_mode), current.st_uid) + != (observed.st_dev, observed.st_ino, stat.S_IFMT(observed.st_mode), observed.st_uid)): + raise BootstrapError("failed_open_descriptor_reused") + self.raw_close(fd) + except (OSError, BootstrapError) as close_error: + self.failed_open_bindings[fd] = (observed.st_dev, observed.st_ino, stat.S_IFMT(observed.st_mode), observed.st_uid) + self.failure(fd, "returned_descriptor_close_unproven:" + type(close_error).__name__, closed=False) + raise BootstrapError("directory_sync_failed_open_close_unproven") from close_error + self.failure(fd, type(error).__name__, closed=True) + + def open(self, path: object, flags: int, mode: int = 0o777, *, dir_fd: int | None = None) -> int: + if not self.source_caller(sys._getframe(1)): + if type(flags) is int and flags & getattr(os, "O_DIRECTORY", 0): + raise BootstrapError("directory_sync_foreign_caller") + if dir_fd is None: + return self.raw_open(path, flags, mode) + return self.raw_open(path, flags, mode, dir_fd=dir_fd) + if type(mode) is not int or mode != 0o777 or dir_fd is not None or self.pending is not None: + raise BootstrapError("directory_sync_open_arguments") + target, identity = self.target(path, flags) + if len(self.bindings) >= LIMITS["events"]: + raise BootstrapError("directory_sync_binding_inventory") + self.pending = (target, flags & ~getattr(os, "O_CLOEXEC", 0), identity) + self.granted = False + fd = None + observed = None + try: + fd = self.raw_open(path, flags, mode) + if type(fd) is not int or fd < 3: + raise BootstrapError("directory_sync_open_return") + observed = os.fstat(fd) + if not self.granted: + raise BootstrapError("directory_sync_grant_missing") + self.verify(fd, target, identity, check_path=True) + if fd in self.bindings: + raise BootstrapError("directory_sync_returned_fd_reused") + self.bindings[fd] = (target, identity, self.guard.sequence) + return fd + except (OSError, BootstrapError) as error: + if type(fd) is int and fd >= 3: + self.close_failed_open(fd, observed, error) + raise + finally: + self.pending = None + self.granted = False + + def fsync(self, fd: int) -> None: + if not self.source_caller(sys._getframe(1)): + if fd in self.bindings: + raise BootstrapError("directory_sync_foreign_fsync") + self.raw_fsync(fd) + return + if type(fd) is not int or fd not in self.bindings: + raise BootstrapError("directory_sync_fsync_unbound") + path, identity, sequence = self.bindings[fd] + if not 1 <= sequence <= self.guard.sequence <= LIMITS["events"]: + raise BootstrapError("directory_sync_grant_binding") + self.verify(fd, path, identity, check_path=True) + self.raw_fsync(fd) + + def close(self, fd: int) -> None: + if not self.source_caller(sys._getframe(1)): + if fd in self.bindings: + raise BootstrapError("directory_sync_foreign_close") + self.raw_close(fd) + return + if type(fd) is not int or fd not in self.bindings: + raise BootstrapError("directory_sync_close_unbound") + path, identity, _ = self.bindings[fd] + try: + # A replaced directory path still permits closing our original handle. + self.verify(fd, path, identity, check_path=False) + self.raw_close(fd) + except (OSError, BootstrapError) as error: + self.failure(fd, "bound_descriptor_close_unproven:" + type(error).__name__, closed=False) + raise + del self.bindings[fd] + + def install(self) -> None: + if self.installed or self.guard.directory_sync is not None: + raise BootstrapError("directory_sync_adapter_already_installed") + self.guard.directory_sync = self + os.open, os.fsync, os.close = self.open, self.fsync, self.close + self.installed = True + + +class RetainedCaptureReadAdapter: + """Verify finite synthetic reads before unmodified producers receive handles.""" + def __init__(self, guard: AuditGuard, completion: types.ModuleType) -> None: + self.guard, self.completion = guard, completion + self.catalog = guard.policy_module.capture_read_catalog(guard.case) + self.functions = {name: getattr(completion, name) for name in + ("_open_private", "_capture_inventory", "_read_private", "_immutable", "verify_capture")} + for function in self.functions.values(): + if (type(function) is not types.FunctionType or function.__globals__ is not completion.__dict__ + or function.__code__.co_filename != str(SOURCE_ROOT / "truememory/ingest/completion.py")): + raise BootstrapError("capture_read_source_identity") + directory = guard.directory_sync + if type(directory) is not DirectorySyncAdapter or directory.installed is not True: + raise BootstrapError("capture_read_directory_chain_missing") + self.directory = directory + self.raw_open, self.raw_close = directory.raw_open, directory.raw_close + self.raw_io_open = io.open + self.pending: tuple[Path, str | None, int, tuple[int, ...]] | None = None + self.pending_sequence = 0 + self.wrapper_fd: int | None = None + self.bindings: dict[int, tuple[Path, tuple[int, ...], int]] = {} + self.handles: list[tuple[io.BufferedReader, Path, tuple[int, ...], int]] = [] + self.failed_handles: list[object] = [] + self.failures: list[dict[str, object]] = [] + self.installed = False + + def capture_path(self, value: object) -> Path | None: + if isinstance(value, os.PathLike): + value = os.fspath(value) + if not isinstance(value, (str, bytes)): + return None + if isinstance(value, bytes): + value = value.decode("utf-8") + path = Path(value) + if path.parent != self.guard.policy_module.account_directory(self.guard.case) / "captures": + return None + return self.guard.path(value) + + def actual(self, frame: types.FrameType, name: str) -> bool: + function = self.functions[name] + return frame.f_code is function.__code__ and frame.f_globals is self.completion.__dict__ + + def producer_frame(self, frame: types.FrameType, method: str) -> types.FrameType: + if type(self.directory) is not DirectorySyncAdapter or self.guard.directory_sync is not self.directory: + raise BootstrapError("capture_read_directory_chain_changed") + function = getattr(DirectorySyncAdapter, method) + delegate = getattr(self.directory, "raw_" + method) + expected_delegate = getattr(RetainedCaptureReadAdapter, method) + if (frame.f_code is function.__code__ and frame.f_globals is function.__globals__ + and frame.f_locals.get("self") is self.directory and frame.f_back is not None + and self.guard.directory_sync is self.directory and type(delegate) is types.MethodType + and delegate.__self__ is self and delegate.__func__ is expected_delegate): + return frame.f_back + return frame + + def source_context(self, frame: types.FrameType, path: Path) -> None: + frame = self.producer_frame(frame, "open") + if path not in self.catalog or not self.actual(frame, "_open_private") or frame.f_back is None: + raise BootstrapError("capture_read_foreign_source") + caller = frame.f_back + if self.actual(caller, "_capture_inventory"): + count = caller.f_locals.get("count") + if (caller.f_locals.get("directory") != path.parent or type(count) is not int + or not 1 <= count <= len(self.catalog)): + raise BootstrapError("capture_inventory_scope") + return + current = self.guard.policy_module.account_directory(self.guard.case) / "captures" / ( + self.guard.policy_module.digest(self.guard.policy_module.encoded(self.guard.policy_module.capture_metadata(self.guard.case))) + ".jsonl") + if (path != current or not self.actual(caller, "_read_private") or caller.f_back is None + or not any(self.actual(caller.f_back, name) for name in ("_immutable", "verify_capture"))): + raise BootstrapError("capture_read_consumer_scope") + + def worker_context(self, frame: types.FrameType, path: Path) -> None: + opened = Path.open + reader = Path.read_bytes + worker = execute_case + middle = frame.f_back + outer = middle.f_back if middle is not None else None + if (path not in self.catalog or frame.f_code is not opened.__code__ + or frame.f_globals is not opened.__globals__ or middle is None + or middle.f_code is not reader.__code__ or middle.f_globals is not reader.__globals__ + or outer is None or outer.f_code is not worker.__code__ + or outer.f_globals is not worker.__globals__ or outer.f_locals.get("case") is not self.guard.case): + raise BootstrapError("capture_read_foreign_worker") + + def target(self, path: Path) -> tuple[int, ...]: + if os.name == "nt" or path not in self.catalog: + raise BootstrapError("unplanned_capture_read_target") + self.guard.policy_module.owned_path(path.parent.parent, directory=True) + self.guard.policy_module.owned_path(path.parent, directory=True) + info = self.guard.policy_module.owned_path(path) + size, _ = self.catalog[path] + if (not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() or info.st_mode & 0o077 + or info.st_nlink != 1 or info.st_size != size): + raise BootstrapError("capture_read_target_identity") + return tuple(self.guard.policy_module.file_identity(info)) + + def audit_open(self, path: Path, mode: object, flags: int) -> None: + if self.pending is None or self.pending_sequence: + raise BootstrapError("capture_read_not_in_verified_open") + target, expected_mode, expected_flags, identity = self.pending + if (path != target or mode != expected_mode or type(flags) is not int + or flags & ~getattr(os, "O_CLOEXEC", 0) != expected_flags or self.target(path) != identity): + raise BootstrapError("capture_read_preopen_changed") + self.guard.grant("open", path, flags=flags) + # A read grant is consumed by this open, never a general readable path. + self.guard.approved.pop(path, None) + self.pending_sequence = self.guard.sequence + + def verify(self, fd: int, path: Path, identity: tuple[int, ...]) -> os.stat_result: + import fcntl + if type(fd) is not int or fd < 3 or path not in self.catalog: + raise BootstrapError("capture_read_descriptor_type") + before = os.fstat(fd) + if (not stat.S_ISREG(before.st_mode) or before.st_uid != os.getuid() + or before.st_mode & 0o077 or before.st_nlink != 1 + or tuple(self.guard.policy_module.file_identity(before)) != identity + or self.target(path) != identity + or fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_ACCMODE != os.O_RDONLY): + raise BootstrapError("capture_read_descriptor_identity") + offset_before = os.lseek(fd, 0, os.SEEK_CUR) + size, expected_hash = self.catalog[path] + checksum = hashlib.sha256() + offset = 0 + while offset <= size: + chunk = os.pread(fd, size + 1 - offset, offset) + if not chunk: + break + checksum.update(chunk) + offset += len(chunk) + after = os.fstat(fd) + if (offset != size or checksum.hexdigest() != expected_hash + or tuple(self.guard.policy_module.file_identity(after)) != identity or self.target(path) != identity + or not stat.S_ISREG(after.st_mode) or after.st_uid != os.getuid() + or after.st_mode & 0o077 or after.st_nlink != 1 + or fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_ACCMODE != os.O_RDONLY + or os.lseek(fd, 0, os.SEEK_CUR) != offset_before): + raise BootstrapError("capture_read_changed_eof_or_hash") + return after + + def conflict(self, fd: int) -> bool: + if fd in self.bindings or fd in self.directory.bindings: + return True + return any(not handle.closed and handle.fileno() == fd for handle, _, _, _ in self.handles) + + def failure(self, fd: int, reason: str) -> None: + if len(self.failures) >= LIMITS["events"]: + raise BootstrapError("capture_read_failure_inventory") + self.failures.append({"fd": fd, "reason": reason, "closed": False}) + + def close_failed_open(self, fd: int, path: Path, identity: tuple[int, ...], error: BaseException) -> None: + import fcntl + if self.conflict(fd): + self.failure(fd, "returned_descriptor_bound_conflict:" + type(error).__name__) + return + try: + current = os.fstat(fd) + if (tuple(self.guard.policy_module.file_identity(current)) != identity + or not stat.S_ISREG(current.st_mode) or current.st_uid != os.getuid() + or current.st_mode & 0o077 or current.st_nlink != 1 + or fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_ACCMODE != os.O_RDONLY): + raise BootstrapError("capture_read_failed_descriptor_foreign") + self.raw_close(fd) + except (OSError, BootstrapError) as close_error: + self.failure(fd, "returned_descriptor_close_unproven:" + type(close_error).__name__) + raise BootstrapError("capture_read_failed_open_close_unproven") from close_error + + def open(self, path: object, flags: int, mode: int = 0o777, *, dir_fd: int | None = None) -> int: + target = self.capture_path(path) + if target is None: + if dir_fd is None: + return self.raw_open(path, flags, mode) + return self.raw_open(path, flags, mode, dir_fd=dir_fd) + if type(flags) is not int: + raise BootstrapError("capture_read_flags_type") + if flags & (os.O_CREAT | os.O_WRONLY | os.O_RDWR | os.O_TRUNC | os.O_APPEND): + caller = self.producer_frame(sys._getframe(1), "open") + owned_exclusive = (target in self.catalog and self.actual(caller, "_open_private") + and caller.f_back is not None and self.actual(caller.f_back, "_immutable") + and flags & ~getattr(os, "O_CLOEXEC", 0) == os.O_CREAT | os.O_EXCL | os.O_WRONLY | os.O_NOFOLLOW + and type(mode) is int and mode == 0o600 and dir_fd is None) + try: + if dir_fd is None: + return self.raw_open(path, flags, mode) + return self.raw_open(path, flags, mode, dir_fd=dir_fd) + except OSError: + if owned_exclusive: + self.guard.approved.pop(target, None) + if self.guard.open_intents and self.guard.open_intents[-1] == (target, os.O_WRONLY): + self.guard.open_intents.clear() + raise + self.source_context(sys._getframe(1), target) + if (dir_fd is not None or type(mode) is not int or mode != 0o600 or self.pending is not None + or flags & ~getattr(os, "O_CLOEXEC", 0) != os.O_RDONLY | os.O_NOFOLLOW): + raise BootstrapError("capture_read_open_arguments") + identity = self.target(target) + if len(self.bindings) >= LIMITS["events"]: + raise BootstrapError("capture_read_binding_inventory") + self.pending = (target, None, flags & ~getattr(os, "O_CLOEXEC", 0), identity) + self.pending_sequence = 0 + fd = None + try: + fd = self.raw_open(path, flags, mode) + if type(fd) is not int or fd < 3 or not self.pending_sequence: + raise BootstrapError("capture_read_open_return_or_grant") + if self.conflict(fd): + raise BootstrapError("capture_read_returned_live_descriptor") + self.verify(fd, target, identity) + self.bindings[fd] = (target, identity, self.pending_sequence) + return fd + except (OSError, BootstrapError) as error: + if type(fd) is int and fd >= 3: + self.close_failed_open(fd, target, identity, error) + if self.guard.open_intents and self.guard.open_intents[-1] == (target, os.O_RDONLY): + self.guard.open_intents.clear() + raise + finally: + self.pending = None + self.pending_sequence = 0 + + def descriptor(self, fd: int, mode: object) -> None: + caller = sys._getframe(1) + audit = caller.f_back + wrapper = audit.f_back if audit is not None else None + if (caller.f_code is not AuditGuard.descriptor.__code__ or caller.f_globals is not AuditGuard.descriptor.__globals__ + or caller.f_locals.get("self") is not self.guard or audit is None + or audit.f_code is not AuditGuard.__call__.__code__ or audit.f_globals is not AuditGuard.__call__.__globals__ + or audit.f_locals.get("self") is not self.guard or wrapper is None + or wrapper.f_code is not type(self).io_open.__code__ or wrapper.f_globals is not type(self).io_open.__globals__ + or wrapper.f_locals.get("self") is not self): + raise BootstrapError("capture_read_foreign_wrapper_frame") + if (type(fd) is not int or fd not in self.bindings or self.wrapper_fd != fd + or type(mode) is not str or mode not in ("r", "rb")): + raise BootstrapError("capture_read_wrapper_not_owned") + path, identity, sequence = self.bindings[fd] + if not 1 <= sequence <= self.guard.sequence <= LIMITS["events"]: + raise BootstrapError("capture_read_wrapper_grant") + self.verify(fd, path, identity) + + def io_open(self, file: object, mode: str = "r", buffering: int = -1, + encoding: str | None = None, errors: str | None = None, newline: str | None = None, + closefd: bool = True, opener: object = None) -> io.IOBase: + target = self.capture_path(file) + integer_binding = type(file) is int and file in self.bindings + if target is None and not integer_binding: + return self.raw_io_open(file, mode, buffering, encoding, errors, newline, closefd, opener) + if (mode != "rb" or type(mode) is not str or type(buffering) is not int or buffering != -1 + or encoding is not None or errors is not None or newline is not None + or closefd is not True or opener is not None or self.pending is not None or self.wrapper_fd is not None): + raise BootstrapError("capture_read_io_arguments") + if integer_binding: + caller = sys._getframe(1) + if (caller.f_code is not os.fdopen.__code__ or caller.f_globals is not os.fdopen.__globals__ + or caller.f_back is None or not self.actual(caller.f_back, "_read_private")): + raise BootstrapError("capture_read_foreign_fdopen") + fd = file + target, identity, _ = self.bindings[fd] + self.verify(fd, target, identity) + self.wrapper_fd = fd + else: + self.worker_context(sys._getframe(1), target) + identity = self.target(target) + self.pending = (target, "r", os.O_RDONLY, identity) + self.pending_sequence = 0 + handle = None + try: + handle = self.raw_io_open(file, mode, buffering, encoding, errors, newline, closefd, opener) + if (type(handle) is not io.BufferedReader or type(handle.raw) is not io.FileIO + or handle.raw.closefd is not True): + raise BootstrapError("capture_read_io_handle_type") + returned = handle.fileno() + if type(returned) is not int or returned < 3 or handle.raw.fileno() != returned: + raise BootstrapError("capture_read_io_handle_descriptor") + if (integer_binding and returned != fd) or (not integer_binding and (not self.pending_sequence or self.conflict(returned))): + raise BootstrapError("capture_read_io_return_or_grant") + self.verify(returned, target, identity) + if len(self.handles) >= LIMITS["events"]: + raise BootstrapError("capture_read_handle_inventory") + self.handles.append((handle, target, identity, returned)) + if integer_binding: + del self.bindings[fd] + return handle + except (OSError, BootstrapError) as error: + if handle is not None: + if len(self.failed_handles) >= LIMITS["events"]: + raise BootstrapError("capture_read_failed_handle_inventory") from error + self.failed_handles.append(handle) + self.failure(file if type(file) is int else -1, "io_transfer_unproven:" + type(error).__name__) + raise + finally: + self.pending = None + self.pending_sequence = 0 + self.wrapper_fd = None + + def close(self, fd: int) -> None: + import fcntl + if type(fd) is not int or fd not in self.bindings: + if type(fd) is int and self.conflict(fd): + frame = sys._getframe(1) + directory_close = DirectorySyncAdapter.close + if (type(self.directory) is not DirectorySyncAdapter or self.guard.directory_sync is not self.directory + or fd not in self.directory.bindings or frame.f_code is not directory_close.__code__ + or frame.f_globals is not directory_close.__globals__ + or frame.f_locals.get("self") is not self.directory or frame.f_back is None + or not self.directory.source_caller(frame.f_back)): + self.failure(fd, "foreign_close_live_bound_descriptor") + raise BootstrapError("capture_read_foreign_live_close") + self.raw_close(fd) + return + caller = self.producer_frame(sys._getframe(1), "close") + if not any(self.actual(caller, name) for name in ("_capture_inventory", "_open_private")): + raise BootstrapError("capture_read_foreign_close") + path, identity, _ = self.bindings[fd] + try: + # A changed pathname cannot authorize closing a reused descriptor. + current = os.fstat(fd) + if (tuple(self.guard.policy_module.file_identity(current)) != identity + or not stat.S_ISREG(current.st_mode) or current.st_uid != os.getuid() + or current.st_mode & 0o077 or current.st_nlink != 1 + or fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_ACCMODE != os.O_RDONLY): + raise BootstrapError("capture_read_close_identity") + self.raw_close(fd) + except (OSError, BootstrapError) as error: + self.failure(fd, "owned_close_unproven:" + type(error).__name__) + raise + del self.bindings[fd] + + def terminal(self) -> None: + if (self.pending is not None or self.pending_sequence or self.wrapper_fd is not None + or self.bindings or self.failed_handles or self.failures + or any(type(handle) is not io.BufferedReader or type(handle.raw) is not io.FileIO + or handle.raw.closefd is not True or not handle.closed or not handle.raw.closed + for handle, _, _, _ in self.handles)): + raise BootstrapError("capture_read_nonterminal") + + def install(self) -> None: + if self.installed or self.guard.capture_reads is not None: + raise BootstrapError("capture_read_adapter_already_installed") + self.guard.capture_reads = self + self.directory.raw_open, self.directory.raw_close = self.open, self.close + io.open = self.io_open + self.installed = True + + +def require(condition: bool, reason: str) -> None: + if not condition: + raise BootstrapError(reason) + + +def execute_case(case: dict, modules: dict[str, types.ModuleType], protocol: Protocol, policy: types.ModuleType) -> dict: + completion = modules["truememory.ingest.completion"] + platform = modules["truememory._platform"] + transcript = modules["truememory.ingest.transcript"] + provenance = modules["truememory.ingest.provenance"] + kind = case["id"] + details: dict[str, object] = {} + if kind in {"capture-a1", "capture-a2", "capture-revision", "invalid-state"}: + try: + capture = completion.admit_capture(case["source"], case["session"], "native-acceptance-v1", case["database"], "claude", root=RUN_ROOT / "completion") + except completion.CompletionError as error: + if kind != "invalid-state" or str(error) != "invalid_retained_state": + raise + details["category"] = str(error) + else: + require(kind != "invalid-state", "invalid_state_was_admitted") + require(capture.key == policy.digest(policy.encoded(policy.capture_metadata(case))), "capture_key_mismatch") + completion.verify_capture(capture) + raw = capture.path.read_bytes() + require(hashlib.sha256(raw).hexdigest() == case["source_sha256"] and len(raw) == case["source_bytes"], "capture_bytes_mismatch") + require(capture.metadata == policy.capture_metadata(case), "capture_scope_mismatch") + require(capture.path.stat().st_mode & 0o077 == 0, "capture_not_private") + second = completion.admit_capture(case["source"], case["session"], "native-acceptance-v1", case["database"], "claude", root=RUN_ROOT / "completion") + require(second.key == capture.key and second.path == capture.path, "capture_not_idempotent") + if kind == "capture-revision": + old = case["previous"] + old_path = capture.directory / "captures" / (old["key"] + ".jsonl") + previous = old_path.read_bytes() + require(capture.key != old["key"] and hashlib.sha256(previous).hexdigest() == old["sha256"] and len(previous) == old["byte_count"], "revision_lost_previous_evidence") + details.update({"category": "immutable_capture_verified", "capture_key": capture.key, "source_sha256": case["source_sha256"], "byte_count": len(raw), "idempotent": True}) + elif kind == "negative-a3": + base = RUN_ROOT / "home/.claude/projects/native-acceptance-v1" + controls = [(case["source"], "unknown", "claude", "source_not_admitted"), + (case["source"], case["session"], "unknown", "source_not_admitted"), + (str(RUN_ROOT / "fixtures/negative-outside-v1.jsonl"), case["session"], "claude", "source_not_admitted"), + (str(base / "negative-source-link-v1.jsonl"), case["session"], "claude", "source_link"), + (str(base / "negative-directory-v1.jsonl"), case["session"], "claude", "source_not_regular")] + categories = [] + for source, session, host, expected in controls: + try: + completion.admit_capture(source, session, "native-acceptance-v1", case["database"], host, root=RUN_ROOT / "completion") + except completion.CompletionError as error: + require(str(error) == expected, "negative_category_mismatch") + categories.append(str(error)) + else: + raise BootstrapError("negative_source_admitted") + details.update({"category": "source_negatives_verified", "categories": categories}) + elif kind == "flock-a4": + lock = RUN_ROOT / "locks/contention-v1.lock" + require(platform.try_file_lock(lock) is None, "contention_not_observed") + protocol.emit({"type": "lock_busy"}) + require(protocol.receive(2048, 0.1) == {"type": "lock_released"}, "lock_release_protocol") + fd = platform.try_file_lock(lock) + require(type(fd) is int and fd >= 0, "released_lock_not_acquired") + details.update({"category": "native_contention_then_acquired", "post_exit_release_requires_parent": True}) + # Leave the actual acquired descriptor open: the parent proves exit release. + elif kind == "parser-a5": + parsed = transcript.parse_transcript_outcome(Path(case["source"])) + require(parsed.status == "complete" and parsed.file_version is not None and parsed.file_version.stable, "parser_scope_unproven") + require(parsed.file_version.sha256 == case["source_sha256"], "parser_version_mismatch") + require(any(message.timestamp for message in parsed.messages) and any(not message.timestamp for message in parsed.messages), "fixture_lacks_known_unknown_times") + formatted = transcript.format_for_extraction(parsed.messages) + context = provenance.source_context(formatted, case["session"]) + require(context.length == len(formatted) and context.digest == hashlib.sha256(formatted.encode("utf-8")).hexdigest(), "provenance_context_mismatch") + require(provenance._known_time("") is None and provenance._known_time("2020-01-01T00:00:00Z") is not None, "source_time_guard_mismatch") + details.update({"category": "parser_and_source_context_verified", "messages": len(parsed.messages), "known_unknown_time": True, + "scorer_extractor_store_proven": False}) + else: + raise BootstrapError("unsupported_case") + return details + + +def main() -> int: + readback = arm_limits() + protocol = Protocol() + request = protocol.receive(LIMITS["input_bytes"], 1) + required = {"version", "source_root", "run_root", "home", "sources", "limits", "case", "supervisor_sha256"} + require(set(request) == required and type(request["version"]) is int and request["version"] == 1, "request_shape") + require(request["source_root"] == str(SOURCE_ROOT) and request["run_root"] == str(RUN_ROOT) and request["home"] == str(RUN_ROOT / "home") and os.environ.get("HOME") == request["home"], "request_roots") + require(request["limits"] == LIMITS and all(type(number) is int for number in request["limits"].values()), "request_limits") + require(type(request["sources"]) is dict and set(request["sources"]) == {relative for _, relative in SOURCE_MODULES}, "source_pin_set") + policy = compile_module("mac_native_policy_v6", SOURCE_ROOT / "tests/native/mac-native-acceptance-v6.py", request["supervisor_sha256"]) + policy.validate_case(request["case"]) + for checksum in request["sources"].values(): + policy.hash_text(checksum) + guard = AuditGuard(protocol, request, policy) + sys.addaudithook(guard) + sys.audit("truememory.native.audit_probe", "fixed-stage-a-v1") + require(guard.activated, "audit_hook_not_installed") + protocol.emit({"type": "ready", "version": 1, "pid": os.getpid(), "limits": readback, "audit_installed": True}) + for name, relative in (("truememory", "truememory"), ("truememory.ingest", "truememory/ingest"), ("truememory.ingest.hooks", "truememory/ingest/hooks")): + module = types.ModuleType(name) + module.__path__ = [str(SOURCE_ROOT / relative)] + module.__package__ = name + sys.modules[name] = module + modules = {name: compile_module(name, SOURCE_ROOT / relative, request["sources"][relative]) for name, relative in SOURCE_MODULES} + directory_sync = DirectorySyncAdapter(guard, modules["truememory.ingest.completion"]) + directory_sync.install() + capture_reads = RetainedCaptureReadAdapter(guard, modules["truememory.ingest.completion"]) + capture_reads.install() + details = execute_case(request["case"], modules, protocol, policy) + capture_reads.terminal() + require(not directory_sync.bindings and not directory_sync.failed_open_bindings and not directory_sync.failures, "directory_sync_nonterminal") + protocol.emit({"type": "result", "version": 1, "case": request["case"]["id"], "passed": True, + "proof_scope": PROOF_SCOPE, "import_mode": "isolated_source_module", "sources": request["sources"], "details": details}) + if request["case"]["id"] == "flock-a4": + os._exit(73) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/native/mac-native-bootstrap-v7.py b/tests/native/mac-native-bootstrap-v7.py new file mode 100644 index 00000000..29f3a41f --- /dev/null +++ b/tests/native/mac-native-bootstrap-v7.py @@ -0,0 +1,918 @@ +"""Fixed Stage A child. Importing definitions never launches native work.""" +from __future__ import annotations + +import hashlib +import io +import json +import os +import resource +import select +import signal +import stat +import sys +import time +import types +from pathlib import Path + +LIMITS = {"wall_seconds": 5, "termination_seconds": 1, "cpu_soft": 2, "cpu_hard": 3, + "rss_abort_bytes": 67108864, "core_bytes": 0, "nofile": 64, + "fsize_bytes": 1048576, "input_bytes": 65536, "output_bytes": 262144, + "events": 32, "frame_bytes": 2048, "ack_ms": 100, "threads": 1} +RUN_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/native-mac-v1/run-0003") +SOURCE_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/worktrees/mac-native-acceptance-v1") +PROOF_SCOPE = "instrumented_unmodified_source_native_filesystem_lock_capture" +SOURCE_MODULES = ( + ("truememory._platform", "truememory/_platform.py"), + ("truememory.ingest.hooks._shared", "truememory/ingest/hooks/_shared.py"), + ("truememory.ingest.completion", "truememory/ingest/completion.py"), + ("truememory.ingest.transcript", "truememory/ingest/transcript.py"), + ("truememory.ingest.provenance", "truememory/ingest/provenance.py"), +) + + +class BootstrapError(ValueError): + pass + + +def encoded(value: object) -> bytes: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8") + + +class Protocol: + def __init__(self) -> None: + self.buffer = bytearray() + self.read_bytes = 0 + self.written_bytes = 0 + + def emit(self, value: object) -> None: + data = encoded(value) + b"\n" + if len(data) > LIMITS["frame_bytes"]: + raise BootstrapError("output_frame_limit") + self.written_bytes += len(data) + if self.written_bytes > LIMITS["output_bytes"]: + raise BootstrapError("output_limit") + while data: + count = os.write(1, data) + if count <= 0: + raise BootstrapError("output_no_progress") + data = data[count:] + + def receive(self, maximum: int, seconds: float) -> dict: + deadline = time.monotonic() + seconds + while b"\n" not in self.buffer: + remaining = deadline - time.monotonic() + if remaining <= 0 or not select.select([0], [], [], remaining)[0]: + raise BootstrapError("input_deadline") + chunk = os.read(0, min(4096, maximum + 1)) + if not chunk: + raise BootstrapError("input_eof") + self.read_bytes += len(chunk) + self.buffer.extend(chunk) + if self.read_bytes > LIMITS["input_bytes"] or len(self.buffer) > maximum: + raise BootstrapError("input_limit") + line, _, remaining = self.buffer.partition(b"\n") + self.buffer = bytearray(remaining) + if len(line) > maximum: + raise BootstrapError("input_frame_limit") + value = json.loads(line) + if type(value) is not dict: + raise BootstrapError("input_shape") + return value + + +def arm_limits() -> dict[str, list[int]]: + if sys.platform != "darwin" or not sys.flags.isolated or not sys.flags.no_site or not sys.flags.dont_write_bytecode: + raise BootstrapError("isolated_mac_child_required") + os.umask(0o077) + sys.dont_write_bytecode = True + expected = {"cpu": (2, 3), "core": (0, 0), "nofile": (64, 64), "fsize": (1048576, 1048576)} + kinds = {"cpu": resource.RLIMIT_CPU, "core": resource.RLIMIT_CORE, + "nofile": resource.RLIMIT_NOFILE, "fsize": resource.RLIMIT_FSIZE} + for name, limits in expected.items(): + current = resource.getrlimit(kinds[name]) + if any(old != resource.RLIM_INFINITY and old < new for old, new in zip(current, limits)): + raise BootstrapError("existing_limit_below_reviewed_limit") + resource.setrlimit(kinds[name], limits) + if resource.getrlimit(kinds[name]) != limits: + raise BootstrapError("limit_readback_failed") + signal.signal(signal.SIGALRM, signal.SIG_DFL) + signal.setitimer(signal.ITIMER_REAL, 5) + return {name: list(limits) for name, limits in expected.items()} + + +def compile_module(name: str, path: Path, checksum: str, *, package: bool = False) -> types.ModuleType: + if path.is_symlink() or path.resolve() != path: + raise BootstrapError("source_path_link") + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + try: + before = os.fstat(fd) + if not stat.S_ISREG(before.st_mode) or before.st_size > 262144: + raise BootstrapError("source_read_type_or_limit") + stream = os.fdopen(fd, "rb") + fd = None + with stream: + data = stream.read(262145) + after = os.fstat(stream.fileno()) + finally: + if fd is not None: + os.close(fd) + if (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns, before.st_ctime_ns) != (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns, after.st_ctime_ns): + raise BootstrapError("source_changed_during_read") + if len(data) > 262144 or hashlib.sha256(data).hexdigest() != checksum: + raise BootstrapError("source_hash_changed") + module = types.ModuleType(name) + module.__file__ = str(path) + module.__package__ = name if package else name.rpartition(".")[0] + if package: + module.__path__ = [str(path.parent)] + sys.modules[name] = module + exec(compile(data, str(path), "exec", dont_inherit=True), module.__dict__) + return module + + +class AuditGuard: + """Bounded telemetry/grants for fixed trusted source, not a security sandbox.""" + def __init__(self, protocol: Protocol, request: dict, policy_module: types.ModuleType) -> None: + self.protocol, self.request, self.policy_module = protocol, request, policy_module + self.case = request["case"] + self.sequence = 0 + self.activated = False + self.approved: dict[Path, int] = {} + self.known_fds: dict[int, tuple[int, int]] = {} + self.open_intents: list[tuple[Path, int]] = [] + self.directory_sync: DirectorySyncAdapter | None = None + self.capture_reads: RetainedCaptureReadAdapter | None = None + self.source = Path(self.case["source"]) if self.case["source"] is not None else None + self.read_paths = {SOURCE_ROOT / relative for _, relative in SOURCE_MODULES} + self.read_paths.add(SOURCE_ROOT / "tests/native/mac-native-acceptance-v7.py") + if self.source is not None: + self.read_paths.add(self.source) + if self.case["id"] == "negative-a3": + self.read_paths.add(RUN_ROOT / "home/.claude/projects/native-acceptance-v1/negative-directory-v1.jsonl") + if self.case["previous"] is not None: + self.read_paths.add(policy_module.account_directory(self.case) / "captures" / (self.case["previous"]["key"] + ".jsonl")) + self.runtime_root = Path(sys.base_prefix) / "lib/python3.12" + + def grant(self, event: str, path: Path, *, flags: int, source: Path | None = None) -> None: + self.sequence += 1 + if self.sequence > LIMITS["events"]: + raise BootstrapError("mutation_event_limit") + self.protocol.emit({"type": "mutation", "version": 1, "seq": self.sequence, + "event": event, "path": str(path), "source": str(source) if source is not None else None, + "flags": flags}) + answer = self.protocol.receive(LIMITS["frame_bytes"], LIMITS["ack_ms"] / 1000) + if set(answer) != {"type", "version", "seq", "path", "purpose"} or answer["type"] != "grant" or type(answer["version"]) is not int or answer["version"] != 1 or type(answer["seq"]) is not int or answer["seq"] != self.sequence or answer["path"] != str(path) or type(answer["purpose"]) is not str or not answer["purpose"]: + raise BootstrapError("grant_mismatch") + if event == "open": + self.approved[path] = flags & os.O_ACCMODE + elif event == "os.rename" and source is not None: + if source not in self.approved: + raise BootstrapError("replace_source_not_approved") + self.approved[path] = self.approved.pop(source) + + def path(self, value: object) -> Path: + if not isinstance(value, (str, bytes)): + raise BootstrapError("path_argument_type") + if isinstance(value, bytes): + value = value.decode("utf-8") + if "\0" in value or not Path(value).is_absolute() or ".." in Path(value).parts: + raise BootstrapError("path_not_absolute") + path = Path(value) + if str(path) != value or path.resolve() != path or any(part.is_symlink() for part in (path, *path.parents)): + raise BootstrapError("path_not_canonical") + return path + + def readable(self, path: Path) -> bool: + if self.capture_reads is not None and self.capture_reads.capture_path(path) is not None: + return False + if path in self.read_paths or path in self.approved: + return True + account = self.policy_module.account_directory(self.case) + if path in {account / "state.json", account / "state.lock"}: + return True + if path.is_relative_to(self.runtime_root) and "site-packages" not in path.parts: + return True + return False + + def descriptor(self, fd: int, mode: object) -> None: + import fcntl + if self.capture_reads is not None and self.open_intents and self.capture_reads.capture_path(self.open_intents[-1][0]) is not None and self.open_intents[-1][1] == os.O_RDONLY: + self.capture_reads.descriptor(fd, mode) + # CPython FileIO reports r/w for an explicitly binary rb/wb fd wrapper. + if type(fd) is not int or fd in (0, 1, 2) or type(mode) is not str or mode not in ("r", "rb", "w", "wb"): + raise BootstrapError("unsupported_descriptor_wrapper") + info = os.fstat(fd) + identity = (info.st_dev, info.st_ino) + if not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid(): + raise BootstrapError("descriptor_not_regular_owned") + access = fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_ACCMODE + if not self.open_intents: + raise BootstrapError("descriptor_not_preapproved") + path, allowed = self.open_intents.pop() + current = path.lstat() + if path.is_symlink() or not stat.S_ISREG(current.st_mode) or current.st_uid != os.getuid() or (current.st_dev, current.st_ino) != identity: + raise BootstrapError("descriptor_intent_identity_mismatch") + if allowed != access or (mode.startswith("r") and access != os.O_RDONLY) or (mode.startswith("w") and access != os.O_WRONLY): + raise BootstrapError("descriptor_access_mismatch") + if len(self.known_fds | {fd: identity}) > LIMITS["events"]: + raise BootstrapError("descriptor_inventory_limit") + self.known_fds[fd] = identity + + def __call__(self, event: str, args: tuple[object, ...]) -> None: + if event == "truememory.native.audit_probe": + if args != ("fixed-stage-a-v1",): + raise BootstrapError("audit_probe_mismatch") + self.activated = True + return + if event == "open": + path, mode, flags = args + if type(flags) is not int: + raise BootstrapError("open_flags_type") + if type(path) is int: + self.descriptor(path, mode) + return + target = self.path(path) + if flags & getattr(os, "O_DIRECTORY", 0): + if self.directory_sync is None: + raise BootstrapError("directory_sync_adapter_missing") + self.directory_sync.audit_open(target, mode, flags) + return + if (self.capture_reads is not None and self.capture_reads.capture_path(target) is not None + and not flags & (os.O_CREAT | os.O_WRONLY | os.O_RDWR | os.O_TRUNC | os.O_APPEND)): + self.capture_reads.audit_open(target, mode, flags) + if mode is None: + self.open_intents[:] = [(target, os.O_RDONLY)] + else: + self.open_intents.clear() + return + if flags & (os.O_CREAT | os.O_WRONLY | os.O_RDWR | os.O_TRUNC | os.O_APPEND): + if not target.is_relative_to(RUN_ROOT): + raise BootstrapError("write_escape") + self.grant("open", target, flags=flags) + elif not self.readable(target): + raise BootstrapError("unplanned_read") + if mode is None: + # Only the latest approved raw open may supply a wrapper intent. + self.open_intents[:] = [(target, flags & os.O_ACCMODE)] + else: + self.open_intents.clear() + elif event == "os.mkdir": + path, mode, dir_fd = args + if dir_fd not in (-1, None) or type(mode) is not int: + raise BootstrapError("directory_descriptor_forbidden") + self.grant(event, self.path(path), flags=mode) + elif event == "os.rename": + source, destination, source_fd, destination_fd = args + if source_fd not in (-1, None) or destination_fd not in (-1, None): + raise BootstrapError("rename_descriptor_forbidden") + self.grant(event, self.path(destination), flags=0, source=self.path(source)) + elif event in ("os.listdir", "os.scandir"): + target = self.path(args[0]) + if not target.is_relative_to(RUN_ROOT) and not (target.is_relative_to(self.runtime_root) and "site-packages" not in target.parts): + raise BootstrapError("unplanned_directory_read") + elif event.startswith("socket.") or event in {"subprocess.Popen", "os.fork", "os.forkpty", "os.posix_spawn", "os.spawn", "os.system", "os.exec", "_thread.start_new_thread", "ctypes.dlopen", "sqlite3.connect", "sqlite3.load_extension", "sqlite3.enable_load_extension", "os.remove", "os.rmdir", "os.link", "os.symlink", "os.truncate", "os.chmod", "os.chown", "os.chflags", "resource.setrlimit", "sys.addaudithook"}: + raise BootstrapError("forbidden_native_operation:" + event) + elif event in ("os.kill", "os.killpg"): + if event != "os.kill" or args != (os.getpid(), 0): + raise BootstrapError("external_signal_forbidden") + elif event == "import": + name = args[0] + if type(name) is not str: + raise BootstrapError("import_name_type") + if name.split(".")[0] in {"torch", "transformers", "mlx", "numpy", "sentence_transformers", "requests", "httpx", "ctypes", "psutil"}: + raise BootstrapError("model_network_dependency_forbidden") + if name.startswith("truememory.") and name not in {item[0] for item in SOURCE_MODULES} and name not in {"truememory.ingest", "truememory.ingest.hooks"}: + raise BootstrapError("unplanned_production_import") + + +class DirectorySyncAdapter: + """Bind the actual source helper's directory handle, without rewriting it.""" + def __init__(self, guard: AuditGuard, completion: types.ModuleType) -> None: + self.guard = guard + self.completion = completion + self.function = completion._sync_directory + if (type(self.function) is not types.FunctionType + or self.function.__globals__ is not completion.__dict__ + or self.function.__code__.co_filename != str(SOURCE_ROOT / "truememory/ingest/completion.py")): + raise BootstrapError("directory_sync_source_identity") + self.raw_open, self.raw_fsync, self.raw_close = os.open, os.fsync, os.close + self.pending: tuple[Path, int, tuple[int, int]] | None = None + self.granted = False + self.bindings: dict[int, tuple[Path, tuple[int, int], int]] = {} + self.failed_open_bindings: dict[int, tuple[int, int, int, int] | None] = {} + self.failures: list[dict[str, object]] = [] + self.installed = False + + def source_caller(self, frame: types.FrameType) -> bool: + return frame.f_code is self.function.__code__ and frame.f_globals is self.completion.__dict__ + + def target(self, value: object, flags: object) -> tuple[Path, tuple[int, int]]: + directory_flag = getattr(os, "O_DIRECTORY", 0) + expected = os.O_RDONLY | directory_flag + if (os.name == "nt" or not directory_flag or type(flags) is not int + or flags & ~getattr(os, "O_CLOEXEC", 0) != expected + or self.guard.case["id"] not in {"capture-a1", "capture-a2", "capture-revision"}): + raise BootstrapError("unsupported_directory_sync_flags_or_case") + if isinstance(value, os.PathLike): + value = os.fspath(value) + path = self.guard.path(value) + account = self.guard.policy_module.account_directory(self.guard.case) + if path not in {account, account / "captures"}: + raise BootstrapError("unplanned_directory_sync_target") + info = self.guard.policy_module.owned_path(path, directory=True) + if not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() or info.st_mode & 0o077: + raise BootstrapError("directory_sync_target_not_private_owned") + return path, (info.st_dev, info.st_ino) + + def audit_open(self, path: Path, mode: object, flags: int) -> None: + if self.pending is None or mode is not None or self.granted: + raise BootstrapError("directory_sync_not_in_source_open") + target, expected_flags, expected_identity = self.pending + current, identity = self.target(path, flags) + if current != target or flags & ~getattr(os, "O_CLOEXEC", 0) != expected_flags or identity != expected_identity: + raise BootstrapError("directory_sync_preopen_changed") + self.guard.grant("open", target, flags=flags) + # Directory grants do not authorize ordinary reads or fdopen wrappers. + self.guard.approved.pop(target, None) + self.granted = True + + def verify(self, fd: int, path: Path, identity: tuple[int, int], *, check_path: bool) -> os.stat_result: + import fcntl + if type(fd) is not int or fd < 3: + raise BootstrapError("directory_sync_descriptor_type") + info = os.fstat(fd) + if (not stat.S_ISDIR(info.st_mode) or info.st_uid != os.getuid() + or info.st_mode & 0o077 or (info.st_dev, info.st_ino) != identity + or fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_ACCMODE != os.O_RDONLY): + raise BootstrapError("directory_sync_descriptor_changed") + if check_path: + current = self.guard.policy_module.owned_path(path, directory=True) + if (not stat.S_ISDIR(current.st_mode) or current.st_uid != os.getuid() + or current.st_mode & 0o077 or (current.st_dev, current.st_ino) != identity): + raise BootstrapError("directory_sync_path_changed") + return info + + def failure(self, fd: int, reason: str, *, closed: bool) -> None: + if len(self.failures) >= LIMITS["events"]: + raise BootstrapError("directory_sync_failure_inventory") + self.failures.append({"fd": fd, "reason": reason, "closed": closed}) + + def close_failed_open(self, fd: int, observed: os.stat_result | None, error: BaseException) -> None: + if fd in self.bindings: + self.failure(fd, "returned_descriptor_bound_conflict:" + type(error).__name__, closed=False) + return + if len(self.failed_open_bindings) >= LIMITS["events"]: + raise BootstrapError("directory_sync_failed_binding_inventory") + if observed is None: + self.failed_open_bindings[fd] = None + self.failure(fd, "returned_descriptor_identity_unavailable:" + type(error).__name__, closed=False) + return + try: + current = os.fstat(fd) + if ((current.st_dev, current.st_ino, stat.S_IFMT(current.st_mode), current.st_uid) + != (observed.st_dev, observed.st_ino, stat.S_IFMT(observed.st_mode), observed.st_uid)): + raise BootstrapError("failed_open_descriptor_reused") + self.raw_close(fd) + except (OSError, BootstrapError) as close_error: + self.failed_open_bindings[fd] = (observed.st_dev, observed.st_ino, stat.S_IFMT(observed.st_mode), observed.st_uid) + self.failure(fd, "returned_descriptor_close_unproven:" + type(close_error).__name__, closed=False) + raise BootstrapError("directory_sync_failed_open_close_unproven") from close_error + self.failure(fd, type(error).__name__, closed=True) + + def open(self, path: object, flags: int, mode: int = 0o777, *, dir_fd: int | None = None) -> int: + if not self.source_caller(sys._getframe(1)): + if type(flags) is int and flags & getattr(os, "O_DIRECTORY", 0): + raise BootstrapError("directory_sync_foreign_caller") + if dir_fd is None: + return self.raw_open(path, flags, mode) + return self.raw_open(path, flags, mode, dir_fd=dir_fd) + if type(mode) is not int or mode != 0o777 or dir_fd is not None or self.pending is not None: + raise BootstrapError("directory_sync_open_arguments") + target, identity = self.target(path, flags) + if len(self.bindings) >= LIMITS["events"]: + raise BootstrapError("directory_sync_binding_inventory") + self.pending = (target, flags & ~getattr(os, "O_CLOEXEC", 0), identity) + self.granted = False + fd = None + observed = None + try: + fd = self.raw_open(path, flags, mode) + if type(fd) is not int or fd < 3: + raise BootstrapError("directory_sync_open_return") + observed = os.fstat(fd) + if not self.granted: + raise BootstrapError("directory_sync_grant_missing") + self.verify(fd, target, identity, check_path=True) + if fd in self.bindings: + raise BootstrapError("directory_sync_returned_fd_reused") + self.bindings[fd] = (target, identity, self.guard.sequence) + return fd + except (OSError, BootstrapError) as error: + if type(fd) is int and fd >= 3: + self.close_failed_open(fd, observed, error) + raise + finally: + self.pending = None + self.granted = False + + def fsync(self, fd: int) -> None: + if not self.source_caller(sys._getframe(1)): + if fd in self.bindings: + raise BootstrapError("directory_sync_foreign_fsync") + self.raw_fsync(fd) + return + if type(fd) is not int or fd not in self.bindings: + raise BootstrapError("directory_sync_fsync_unbound") + path, identity, sequence = self.bindings[fd] + if not 1 <= sequence <= self.guard.sequence <= LIMITS["events"]: + raise BootstrapError("directory_sync_grant_binding") + self.verify(fd, path, identity, check_path=True) + self.raw_fsync(fd) + + def close(self, fd: int) -> None: + if not self.source_caller(sys._getframe(1)): + if fd in self.bindings: + raise BootstrapError("directory_sync_foreign_close") + self.raw_close(fd) + return + if type(fd) is not int or fd not in self.bindings: + raise BootstrapError("directory_sync_close_unbound") + path, identity, _ = self.bindings[fd] + try: + # A replaced directory path still permits closing our original handle. + self.verify(fd, path, identity, check_path=False) + self.raw_close(fd) + except (OSError, BootstrapError) as error: + self.failure(fd, "bound_descriptor_close_unproven:" + type(error).__name__, closed=False) + raise + del self.bindings[fd] + + def install(self) -> None: + if self.installed or self.guard.directory_sync is not None: + raise BootstrapError("directory_sync_adapter_already_installed") + self.guard.directory_sync = self + os.open, os.fsync, os.close = self.open, self.fsync, self.close + self.installed = True + + +class RetainedCaptureReadAdapter: + """Verify finite synthetic reads before unmodified producers receive handles.""" + def __init__(self, guard: AuditGuard, completion: types.ModuleType) -> None: + self.guard, self.completion = guard, completion + self.catalog = guard.policy_module.capture_read_catalog(guard.case) + self.functions = {name: getattr(completion, name) for name in + ("_open_private", "_capture_inventory", "_read_private", "_immutable", "verify_capture")} + for function in self.functions.values(): + if (type(function) is not types.FunctionType or function.__globals__ is not completion.__dict__ + or function.__code__.co_filename != str(SOURCE_ROOT / "truememory/ingest/completion.py")): + raise BootstrapError("capture_read_source_identity") + directory = guard.directory_sync + if type(directory) is not DirectorySyncAdapter or directory.installed is not True: + raise BootstrapError("capture_read_directory_chain_missing") + self.directory = directory + self.raw_open, self.raw_close = directory.raw_open, directory.raw_close + self.raw_io_open = io.open + self.pending: tuple[Path, str | None, int, tuple[int, ...]] | None = None + self.pending_sequence = 0 + self.wrapper_fd: int | None = None + self.bindings: dict[int, tuple[Path, tuple[int, ...], int]] = {} + self.handles: list[tuple[io.BufferedReader, Path, tuple[int, ...], int]] = [] + self.failed_handles: list[object] = [] + self.failures: list[dict[str, object]] = [] + self.installed = False + + def capture_path(self, value: object) -> Path | None: + if isinstance(value, os.PathLike): + value = os.fspath(value) + if not isinstance(value, (str, bytes)): + return None + if isinstance(value, bytes): + value = value.decode("utf-8") + path = Path(value) + if path.parent != self.guard.policy_module.account_directory(self.guard.case) / "captures": + return None + return self.guard.path(value) + + def actual(self, frame: types.FrameType, name: str) -> bool: + function = self.functions[name] + return frame.f_code is function.__code__ and frame.f_globals is self.completion.__dict__ + + def producer_frame(self, frame: types.FrameType, method: str) -> types.FrameType: + if type(self.directory) is not DirectorySyncAdapter or self.guard.directory_sync is not self.directory: + raise BootstrapError("capture_read_directory_chain_changed") + function = getattr(DirectorySyncAdapter, method) + delegate = getattr(self.directory, "raw_" + method) + expected_delegate = getattr(RetainedCaptureReadAdapter, method) + if (frame.f_code is function.__code__ and frame.f_globals is function.__globals__ + and frame.f_locals.get("self") is self.directory and frame.f_back is not None + and self.guard.directory_sync is self.directory and type(delegate) is types.MethodType + and delegate.__self__ is self and delegate.__func__ is expected_delegate): + return frame.f_back + return frame + + def source_context(self, frame: types.FrameType, path: Path) -> None: + frame = self.producer_frame(frame, "open") + if path not in self.catalog or not self.actual(frame, "_open_private") or frame.f_back is None: + raise BootstrapError("capture_read_foreign_source") + caller = frame.f_back + if self.actual(caller, "_capture_inventory"): + count = caller.f_locals.get("count") + if (caller.f_locals.get("directory") != path.parent or type(count) is not int + or not 1 <= count <= len(self.catalog)): + raise BootstrapError("capture_inventory_scope") + return + current = self.guard.policy_module.account_directory(self.guard.case) / "captures" / ( + self.guard.policy_module.digest(self.guard.policy_module.encoded(self.guard.policy_module.capture_metadata(self.guard.case))) + ".jsonl") + if (path != current or not self.actual(caller, "_read_private") or caller.f_back is None + or not any(self.actual(caller.f_back, name) for name in ("_immutable", "verify_capture"))): + raise BootstrapError("capture_read_consumer_scope") + + def worker_context(self, frame: types.FrameType, path: Path) -> None: + opened = Path.open + reader = Path.read_bytes + worker = execute_case + middle = frame.f_back + outer = middle.f_back if middle is not None else None + if (path not in self.catalog or frame.f_code is not opened.__code__ + or frame.f_globals is not opened.__globals__ or middle is None + or middle.f_code is not reader.__code__ or middle.f_globals is not reader.__globals__ + or outer is None or outer.f_code is not worker.__code__ + or outer.f_globals is not worker.__globals__ or outer.f_locals.get("case") is not self.guard.case): + raise BootstrapError("capture_read_foreign_worker") + + def target(self, path: Path) -> tuple[int, ...]: + if os.name == "nt" or path not in self.catalog: + raise BootstrapError("unplanned_capture_read_target") + self.guard.policy_module.owned_path(path.parent.parent, directory=True) + self.guard.policy_module.owned_path(path.parent, directory=True) + info = self.guard.policy_module.owned_path(path) + size, _ = self.catalog[path] + if (not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid() or info.st_mode & 0o077 + or info.st_nlink != 1 or info.st_size != size): + raise BootstrapError("capture_read_target_identity") + return tuple(self.guard.policy_module.file_identity(info)) + + def audit_open(self, path: Path, mode: object, flags: int) -> None: + if self.pending is None or self.pending_sequence: + raise BootstrapError("capture_read_not_in_verified_open") + target, expected_mode, expected_flags, identity = self.pending + if (path != target or mode != expected_mode or type(flags) is not int + or flags & ~getattr(os, "O_CLOEXEC", 0) != expected_flags or self.target(path) != identity): + raise BootstrapError("capture_read_preopen_changed") + self.guard.grant("open", path, flags=flags) + # A read grant is consumed by this open, never a general readable path. + self.guard.approved.pop(path, None) + self.pending_sequence = self.guard.sequence + + def verify(self, fd: int, path: Path, identity: tuple[int, ...]) -> os.stat_result: + import fcntl + if type(fd) is not int or fd < 3 or path not in self.catalog: + raise BootstrapError("capture_read_descriptor_type") + before = os.fstat(fd) + if (not stat.S_ISREG(before.st_mode) or before.st_uid != os.getuid() + or before.st_mode & 0o077 or before.st_nlink != 1 + or tuple(self.guard.policy_module.file_identity(before)) != identity + or self.target(path) != identity + or fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_ACCMODE != os.O_RDONLY): + raise BootstrapError("capture_read_descriptor_identity") + offset_before = os.lseek(fd, 0, os.SEEK_CUR) + size, expected_hash = self.catalog[path] + checksum = hashlib.sha256() + offset = 0 + while offset <= size: + chunk = os.pread(fd, size + 1 - offset, offset) + if not chunk: + break + checksum.update(chunk) + offset += len(chunk) + after = os.fstat(fd) + if (offset != size or checksum.hexdigest() != expected_hash + or tuple(self.guard.policy_module.file_identity(after)) != identity or self.target(path) != identity + or not stat.S_ISREG(after.st_mode) or after.st_uid != os.getuid() + or after.st_mode & 0o077 or after.st_nlink != 1 + or fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_ACCMODE != os.O_RDONLY + or os.lseek(fd, 0, os.SEEK_CUR) != offset_before): + raise BootstrapError("capture_read_changed_eof_or_hash") + return after + + def conflict(self, fd: int) -> bool: + if fd in self.bindings or fd in self.directory.bindings: + return True + return any(not handle.closed and handle.fileno() == fd for handle, _, _, _ in self.handles) + + def failure(self, fd: int, reason: str) -> None: + if len(self.failures) >= LIMITS["events"]: + raise BootstrapError("capture_read_failure_inventory") + self.failures.append({"fd": fd, "reason": reason, "closed": False}) + + def close_failed_open(self, fd: int, path: Path, identity: tuple[int, ...], error: BaseException) -> None: + import fcntl + if self.conflict(fd): + self.failure(fd, "returned_descriptor_bound_conflict:" + type(error).__name__) + return + try: + current = os.fstat(fd) + if (tuple(self.guard.policy_module.file_identity(current)) != identity + or not stat.S_ISREG(current.st_mode) or current.st_uid != os.getuid() + or current.st_mode & 0o077 or current.st_nlink != 1 + or fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_ACCMODE != os.O_RDONLY): + raise BootstrapError("capture_read_failed_descriptor_foreign") + self.raw_close(fd) + except (OSError, BootstrapError) as close_error: + self.failure(fd, "returned_descriptor_close_unproven:" + type(close_error).__name__) + raise BootstrapError("capture_read_failed_open_close_unproven") from close_error + + def open(self, path: object, flags: int, mode: int = 0o777, *, dir_fd: int | None = None) -> int: + target = self.capture_path(path) + if target is None: + if dir_fd is None: + return self.raw_open(path, flags, mode) + return self.raw_open(path, flags, mode, dir_fd=dir_fd) + if type(flags) is not int: + raise BootstrapError("capture_read_flags_type") + if flags & (os.O_CREAT | os.O_WRONLY | os.O_RDWR | os.O_TRUNC | os.O_APPEND): + caller = self.producer_frame(sys._getframe(1), "open") + owned_exclusive = (target in self.catalog and self.actual(caller, "_open_private") + and caller.f_back is not None and self.actual(caller.f_back, "_immutable") + and flags & ~getattr(os, "O_CLOEXEC", 0) == os.O_CREAT | os.O_EXCL | os.O_WRONLY | os.O_NOFOLLOW + and type(mode) is int and mode == 0o600 and dir_fd is None) + try: + if dir_fd is None: + return self.raw_open(path, flags, mode) + return self.raw_open(path, flags, mode, dir_fd=dir_fd) + except OSError: + if owned_exclusive: + self.guard.approved.pop(target, None) + if self.guard.open_intents and self.guard.open_intents[-1] == (target, os.O_WRONLY): + self.guard.open_intents.clear() + raise + self.source_context(sys._getframe(1), target) + if (dir_fd is not None or type(mode) is not int or mode != 0o600 or self.pending is not None + or flags & ~getattr(os, "O_CLOEXEC", 0) != os.O_RDONLY | os.O_NOFOLLOW): + raise BootstrapError("capture_read_open_arguments") + identity = self.target(target) + if len(self.bindings) >= LIMITS["events"]: + raise BootstrapError("capture_read_binding_inventory") + self.pending = (target, None, flags & ~getattr(os, "O_CLOEXEC", 0), identity) + self.pending_sequence = 0 + fd = None + try: + fd = self.raw_open(path, flags, mode) + if type(fd) is not int or fd < 3 or not self.pending_sequence: + raise BootstrapError("capture_read_open_return_or_grant") + if self.conflict(fd): + raise BootstrapError("capture_read_returned_live_descriptor") + self.verify(fd, target, identity) + self.bindings[fd] = (target, identity, self.pending_sequence) + return fd + except (OSError, BootstrapError) as error: + if type(fd) is int and fd >= 3: + self.close_failed_open(fd, target, identity, error) + if self.guard.open_intents and self.guard.open_intents[-1] == (target, os.O_RDONLY): + self.guard.open_intents.clear() + raise + finally: + self.pending = None + self.pending_sequence = 0 + + def descriptor(self, fd: int, mode: object) -> None: + caller = sys._getframe(1) + audit = caller.f_back + wrapper = audit.f_back if audit is not None else None + if (caller.f_code is not AuditGuard.descriptor.__code__ or caller.f_globals is not AuditGuard.descriptor.__globals__ + or caller.f_locals.get("self") is not self.guard or audit is None + or audit.f_code is not AuditGuard.__call__.__code__ or audit.f_globals is not AuditGuard.__call__.__globals__ + or audit.f_locals.get("self") is not self.guard or wrapper is None + or wrapper.f_code is not type(self).io_open.__code__ or wrapper.f_globals is not type(self).io_open.__globals__ + or wrapper.f_locals.get("self") is not self): + raise BootstrapError("capture_read_foreign_wrapper_frame") + if (type(fd) is not int or fd not in self.bindings or self.wrapper_fd != fd + or type(mode) is not str or mode not in ("r", "rb")): + raise BootstrapError("capture_read_wrapper_not_owned") + path, identity, sequence = self.bindings[fd] + if not 1 <= sequence <= self.guard.sequence <= LIMITS["events"]: + raise BootstrapError("capture_read_wrapper_grant") + self.verify(fd, path, identity) + + def io_open(self, file: object, mode: str = "r", buffering: int = -1, + encoding: str | None = None, errors: str | None = None, newline: str | None = None, + closefd: bool = True, opener: object = None) -> io.IOBase: + target = self.capture_path(file) + integer_binding = type(file) is int and file in self.bindings + if target is None and not integer_binding: + return self.raw_io_open(file, mode, buffering, encoding, errors, newline, closefd, opener) + if (mode != "rb" or type(mode) is not str or type(buffering) is not int or buffering != -1 + or encoding is not None or errors is not None or newline is not None + or closefd is not True or opener is not None or self.pending is not None or self.wrapper_fd is not None): + raise BootstrapError("capture_read_io_arguments") + if integer_binding: + caller = sys._getframe(1) + if (caller.f_code is not os.fdopen.__code__ or caller.f_globals is not os.fdopen.__globals__ + or caller.f_back is None or not self.actual(caller.f_back, "_read_private")): + raise BootstrapError("capture_read_foreign_fdopen") + fd = file + target, identity, _ = self.bindings[fd] + self.verify(fd, target, identity) + self.wrapper_fd = fd + else: + self.worker_context(sys._getframe(1), target) + identity = self.target(target) + self.pending = (target, "r", os.O_RDONLY, identity) + self.pending_sequence = 0 + handle = None + try: + handle = self.raw_io_open(file, mode, buffering, encoding, errors, newline, closefd, opener) + if (type(handle) is not io.BufferedReader or type(handle.raw) is not io.FileIO + or handle.raw.closefd is not True): + raise BootstrapError("capture_read_io_handle_type") + returned = handle.fileno() + if type(returned) is not int or returned < 3 or handle.raw.fileno() != returned: + raise BootstrapError("capture_read_io_handle_descriptor") + if (integer_binding and returned != fd) or (not integer_binding and (not self.pending_sequence or self.conflict(returned))): + raise BootstrapError("capture_read_io_return_or_grant") + self.verify(returned, target, identity) + if len(self.handles) >= LIMITS["events"]: + raise BootstrapError("capture_read_handle_inventory") + self.handles.append((handle, target, identity, returned)) + if integer_binding: + del self.bindings[fd] + return handle + except (OSError, BootstrapError) as error: + if handle is not None: + if len(self.failed_handles) >= LIMITS["events"]: + raise BootstrapError("capture_read_failed_handle_inventory") from error + self.failed_handles.append(handle) + self.failure(file if type(file) is int else -1, "io_transfer_unproven:" + type(error).__name__) + raise + finally: + self.pending = None + self.pending_sequence = 0 + self.wrapper_fd = None + + def close(self, fd: int) -> None: + import fcntl + if type(fd) is not int or fd not in self.bindings: + if type(fd) is int and self.conflict(fd): + frame = sys._getframe(1) + directory_close = DirectorySyncAdapter.close + if (type(self.directory) is not DirectorySyncAdapter or self.guard.directory_sync is not self.directory + or fd not in self.directory.bindings or frame.f_code is not directory_close.__code__ + or frame.f_globals is not directory_close.__globals__ + or frame.f_locals.get("self") is not self.directory or frame.f_back is None + or not self.directory.source_caller(frame.f_back)): + self.failure(fd, "foreign_close_live_bound_descriptor") + raise BootstrapError("capture_read_foreign_live_close") + self.raw_close(fd) + return + caller = self.producer_frame(sys._getframe(1), "close") + if not any(self.actual(caller, name) for name in ("_capture_inventory", "_open_private")): + raise BootstrapError("capture_read_foreign_close") + path, identity, _ = self.bindings[fd] + try: + # A changed pathname cannot authorize closing a reused descriptor. + current = os.fstat(fd) + if (tuple(self.guard.policy_module.file_identity(current)) != identity + or not stat.S_ISREG(current.st_mode) or current.st_uid != os.getuid() + or current.st_mode & 0o077 or current.st_nlink != 1 + or fcntl.fcntl(fd, fcntl.F_GETFL) & os.O_ACCMODE != os.O_RDONLY): + raise BootstrapError("capture_read_close_identity") + self.raw_close(fd) + except (OSError, BootstrapError) as error: + self.failure(fd, "owned_close_unproven:" + type(error).__name__) + raise + del self.bindings[fd] + + def terminal(self) -> None: + if (self.pending is not None or self.pending_sequence or self.wrapper_fd is not None + or self.bindings or self.failed_handles or self.failures + or any(type(handle) is not io.BufferedReader or type(handle.raw) is not io.FileIO + or handle.raw.closefd is not True or not handle.closed or not handle.raw.closed + for handle, _, _, _ in self.handles)): + raise BootstrapError("capture_read_nonterminal") + + def install(self) -> None: + if self.installed or self.guard.capture_reads is not None: + raise BootstrapError("capture_read_adapter_already_installed") + self.guard.capture_reads = self + self.directory.raw_open, self.directory.raw_close = self.open, self.close + io.open = self.io_open + self.installed = True + + +def require(condition: bool, reason: str) -> None: + if not condition: + raise BootstrapError(reason) + + +def execute_case(case: dict, modules: dict[str, types.ModuleType], protocol: Protocol, policy: types.ModuleType) -> dict: + completion = modules["truememory.ingest.completion"] + platform = modules["truememory._platform"] + transcript = modules["truememory.ingest.transcript"] + provenance = modules["truememory.ingest.provenance"] + kind = case["id"] + details: dict[str, object] = {} + if kind in {"capture-a1", "capture-a2", "capture-revision", "invalid-state"}: + try: + capture = completion.admit_capture(case["source"], case["session"], "native-acceptance-v1", case["database"], "claude", root=RUN_ROOT / "completion") + except completion.CompletionError as error: + if kind != "invalid-state" or str(error) != "invalid_retained_state": + raise + details["category"] = str(error) + else: + require(kind != "invalid-state", "invalid_state_was_admitted") + require(capture.key == policy.digest(policy.encoded(policy.capture_metadata(case))), "capture_key_mismatch") + completion.verify_capture(capture) + raw = capture.path.read_bytes() + require(hashlib.sha256(raw).hexdigest() == case["source_sha256"] and len(raw) == case["source_bytes"], "capture_bytes_mismatch") + require(capture.metadata == policy.capture_metadata(case), "capture_scope_mismatch") + require(capture.path.stat().st_mode & 0o077 == 0, "capture_not_private") + second = completion.admit_capture(case["source"], case["session"], "native-acceptance-v1", case["database"], "claude", root=RUN_ROOT / "completion") + require(second.key == capture.key and second.path == capture.path, "capture_not_idempotent") + if kind == "capture-revision": + old = case["previous"] + old_path = capture.directory / "captures" / (old["key"] + ".jsonl") + previous = old_path.read_bytes() + require(capture.key != old["key"] and hashlib.sha256(previous).hexdigest() == old["sha256"] and len(previous) == old["byte_count"], "revision_lost_previous_evidence") + details.update({"category": "immutable_capture_verified", "capture_key": capture.key, "source_sha256": case["source_sha256"], "byte_count": len(raw), "idempotent": True}) + elif kind == "negative-a3": + base = RUN_ROOT / "home/.claude/projects/native-acceptance-v1" + controls = [(case["source"], "unknown", "claude", "source_not_admitted"), + (case["source"], case["session"], "unknown", "source_not_admitted"), + (str(RUN_ROOT / "fixtures/negative-outside-v1.jsonl"), case["session"], "claude", "source_not_admitted"), + (str(base / "negative-source-link-v1.jsonl"), case["session"], "claude", "source_link"), + (str(base / "negative-directory-v1.jsonl"), case["session"], "claude", "source_not_regular")] + categories = [] + for source, session, host, expected in controls: + try: + completion.admit_capture(source, session, "native-acceptance-v1", case["database"], host, root=RUN_ROOT / "completion") + except completion.CompletionError as error: + require(str(error) == expected, "negative_category_mismatch") + categories.append(str(error)) + else: + raise BootstrapError("negative_source_admitted") + details.update({"category": "source_negatives_verified", "categories": categories}) + elif kind == "flock-a4": + lock = RUN_ROOT / "locks/contention-v1.lock" + require(platform.try_file_lock(lock) is None, "contention_not_observed") + protocol.emit({"type": "lock_busy"}) + require(protocol.receive(2048, 0.1) == {"type": "lock_released"}, "lock_release_protocol") + fd = platform.try_file_lock(lock) + require(type(fd) is int and fd >= 0, "released_lock_not_acquired") + details.update({"category": "native_contention_then_acquired", "post_exit_release_requires_parent": True}) + # Leave the actual acquired descriptor open: the parent proves exit release. + elif kind == "parser-a5": + parsed = transcript.parse_transcript_outcome(Path(case["source"])) + require(parsed.status == "complete" and parsed.file_version is not None and parsed.file_version.stable, "parser_scope_unproven") + require(parsed.file_version.sha256 == case["source_sha256"], "parser_version_mismatch") + require(any(message.timestamp for message in parsed.messages) and any(not message.timestamp for message in parsed.messages), "fixture_lacks_known_unknown_times") + formatted = transcript.format_for_extraction(parsed.messages) + context = provenance.source_context(formatted, case["session"]) + require(context.length == len(formatted) and context.digest == hashlib.sha256(formatted.encode("utf-8")).hexdigest(), "provenance_context_mismatch") + require(provenance._known_time("") is None and provenance._known_time("2020-01-01T00:00:00Z") is not None, "source_time_guard_mismatch") + details.update({"category": "parser_and_source_context_verified", "messages": len(parsed.messages), "known_unknown_time": True, + "scorer_extractor_store_proven": False}) + else: + raise BootstrapError("unsupported_case") + return details + + +def main() -> int: + readback = arm_limits() + protocol = Protocol() + request = protocol.receive(LIMITS["input_bytes"], 1) + required = {"version", "source_root", "run_root", "home", "sources", "limits", "case", "supervisor_sha256"} + require(set(request) == required and type(request["version"]) is int and request["version"] == 1, "request_shape") + require(request["source_root"] == str(SOURCE_ROOT) and request["run_root"] == str(RUN_ROOT) and request["home"] == str(RUN_ROOT / "home") and os.environ.get("HOME") == request["home"], "request_roots") + require(request["limits"] == LIMITS and all(type(number) is int for number in request["limits"].values()), "request_limits") + require(type(request["sources"]) is dict and set(request["sources"]) == {relative for _, relative in SOURCE_MODULES}, "source_pin_set") + policy = compile_module("mac_native_policy_v7", SOURCE_ROOT / "tests/native/mac-native-acceptance-v7.py", request["supervisor_sha256"]) + policy.validate_case(request["case"]) + for checksum in request["sources"].values(): + policy.hash_text(checksum) + guard = AuditGuard(protocol, request, policy) + sys.addaudithook(guard) + sys.audit("truememory.native.audit_probe", "fixed-stage-a-v1") + require(guard.activated, "audit_hook_not_installed") + protocol.emit({"type": "ready", "version": 1, "pid": os.getpid(), "limits": readback, "audit_installed": True}) + for name, relative in (("truememory", "truememory"), ("truememory.ingest", "truememory/ingest"), ("truememory.ingest.hooks", "truememory/ingest/hooks")): + module = types.ModuleType(name) + module.__path__ = [str(SOURCE_ROOT / relative)] + module.__package__ = name + sys.modules[name] = module + modules = {name: compile_module(name, SOURCE_ROOT / relative, request["sources"][relative]) for name, relative in SOURCE_MODULES} + directory_sync = DirectorySyncAdapter(guard, modules["truememory.ingest.completion"]) + directory_sync.install() + capture_reads = RetainedCaptureReadAdapter(guard, modules["truememory.ingest.completion"]) + capture_reads.install() + details = execute_case(request["case"], modules, protocol, policy) + capture_reads.terminal() + require(not directory_sync.bindings and not directory_sync.failed_open_bindings and not directory_sync.failures, "directory_sync_nonterminal") + protocol.emit({"type": "result", "version": 1, "case": request["case"]["id"], "passed": True, + "proof_scope": PROOF_SCOPE, "import_mode": "isolated_source_module", "sources": request["sources"], "details": details}) + if request["case"]["id"] == "flock-a4": + os._exit(73) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/native/mac-native-package-acceptance-v3.py b/tests/native/mac-native-package-acceptance-v3.py new file mode 100644 index 00000000..ff2dcccc --- /dev/null +++ b/tests/native/mac-native-package-acceptance-v3.py @@ -0,0 +1,1053 @@ +"""ROOT-run Stage B supervisor. Importing definitions never starts native work. + +This instrumented full-source harness proves one owned AF_UNIX package ping. +It does not certify installed packages, host dispatch, models or a physical cap. +""" +from __future__ import annotations + +import argparse +import hashlib +import importlib.abc +import importlib.machinery +import importlib.util +import json +import math +import os +import re +import select +import selectors +import signal +import stat +import subprocess +import sys +import time +from dataclasses import asdict, dataclass +from pathlib import Path +from types import ModuleType + +RUN_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/native-mac-v1/run-0002") +HARNESS_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/worktrees/mac-native-acceptance-v1") +SOURCE_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/worktrees/whole-process-memory-297-v1") +RUNTIME_PACKET = Path("/Users/j/dev/truememory-maintenance-v1/.work/reports/mac-native-package-runtime-pins-v2.json") +RUNTIME_PACKET_SHA = "9c7ed6eda037c200530fbb913352006886c312d4011dac5e82d1805d3ead69aa" +PROOF_SCOPE = "instrumented_unmodified_full_package_source_af_unix_ping" +SOURCE_HASHES = {'truememory/__init__.py': '00d3ab14540092b63b449d14533b17292018ae4a69cd3f3ef1345e1a02b54d98', 'truememory/_platform.py': '0688763a6fc26ef8db2b87866aabd5a2373a18f4ee735859cc6d595147e310c0', 'truememory/client.py': 'f020ac930617cd5d4848775ddd7390a61665e0fbdc62d51ccaf19bccdeeb3a33', 'truememory/clustering.py': 'ea9c2dbb7d083217983bb962f696e7bfcf08359ba5969f916fa63241baa535a5', 'truememory/consolidation.py': 'dcff2bf4c8f80315453b5078cdae575e4414adf879b69ef8f7983e72478fd474', 'truememory/embedding_target.py': 'da61626141f1ca5587eaa2fa3c27381f80834e79bc32351b5d7a3c20e86c7ddc', 'truememory/engine.py': '898f2b8dd79e8ca18154f8e4b3ef0cbfe785c27b543719f975f4a1f2cf0a010c', 'truememory/fts_search.py': '9c954bd2513ea8e3c82b33e0fbc311cd5b7346d12ee70da011df3f4244dc4389', 'truememory/hybrid.py': '17b281113b2927c6e63088c0d48dc34c2681a9943e1d4f4aa4d0a70d1c36dd17', 'truememory/hyde.py': '127e4d1c01767e13c0d963d6878f01a7329bb749b4510e831ef51c9c879276e4', 'truememory/maintenance.py': '413ffbc08f92ba3ed331cab5e0aebd67ba2cadf2e5400f8b5c09c47f80521819', 'truememory/memory_budget.py': '2d2578554d1c029c5205ac1fd27511f30e3d372a1178bf88cb7b81b0501409a4', 'truememory/model_client.py': 'a8a9332ab7cf532c98594f4f7455642f00d6235d9ca8a4552a75c68a49b7d0f3', 'truememory/model_server.py': '424986a108d0ecd9e984ed77e352fba36305bf9aa3d79517de8fd1cfa4f8701b', 'truememory/personality.py': '4a55a594360435bd56152738af061988b73e3374c3e8757e934c0b88cf4cd2af', 'truememory/personality_style_vec.py': 'f294e67b46ce4473b2cf15a49c80fc348de27f5236834dfbec832fc092c5362e', 'truememory/predictive.py': '88ae1a2f8436b305855a5884e3ac5ebe435106808533dc0bc563d2054d374c41', 'truememory/query_classifier.py': '52f1f3bfe5a75f45eb5aef389b490953422bf1d8fb03c5181bff816440bf2476', 'truememory/rebuild_source.py': 'e46639a50d35b711bc0c0b0de9e7a90652b1386b25356c14d095551673c7bd76', 'truememory/reranker.py': 'c02740b54246ca9f52fd6df90d424f18d3417c36734346ede128c91d3c66e90d', 'truememory/salience.py': 'a005256ecc20c44ad7d4825a11fafa2c2b5951a762450dfce9fd80ec7e12940e', 'truememory/storage.py': 'adc89850a897a2f9b478c8a23ad62fa438d7a214405ea21df886e029cdf4481a', 'truememory/temporal.py': '52e98b3924573db6a209ef27d0318e254e5c976a7a65d75762f901d712505a65', 'truememory/tier_config.py': 'a8e690000c74f7629690c67fb99e34d4bd87f02b78795fb68ec2009f18348489', 'truememory/tier_switch/__init__.py': '2333416f4985986b9fcf6558478f5c3cf9f75c277961c14902162f8f11ea1705', 'truememory/tier_switch/activation.py': '24b820b0ff129fb42b9c13544e42b7c4b2ec8397b427d23b61c60c3e8ff19d47', 'truememory/tier_switch/cache.py': '77bc24c176763629f83d199cc588eca9a9e7432e36e29584a2bd3d475ac314b4', 'truememory/tier_switch/job.py': 'c1def76a9510c3cd600ec11737ee068d71cedae2bc980048fba6c70bdee7a1f0', 'truememory/tier_switch/manager.py': '160a61ff2b2a4b2a7fdeb3c7fa2442ce161f5b1a426735ea3cac53829b6b8c4b', 'truememory/tier_switch/runtime.py': '861893308506c0189e50eea2d21a9da43b2c7be0eb90c56f8fd62e6bb40dc5be', 'truememory/tier_switch/sensors.py': '6471176f75ab5ddce3aa523340a6567ec24f9243c3551f4203baa8ea2f15b55b', 'truememory/tier_switch/serving.py': '25c1524a02affc127e170c16ceee0f61c2b3b221e28c5a19e9fb347aec57bef7', 'truememory/tier_switch/source.py': 'f8ffe9cd5890c5e6520ca52cfe20bad49cb789d2222e29fffc4300bdbc64f227', 'truememory/tier_switch/state_machine.py': '39ccf8d3e944728f7bf18e6efe10d0e8f9db625f9d812220554dd980a932d1b9', 'truememory/tier_switch/throttler.py': '3416ad7681a22b2478e9e4375041f7d2d364b474a81255103e11abe79d5b5f32', 'truememory/tier_switch/worker.py': '8bae8daf54ab06c0693df46578e340cbde874dda0dccfdb5bcfc2a30d3081ef7', 'truememory/vector_search.py': '2acd58a0dd858cc6a3a8c69117c1666f5e74d166ed9aeb57b81da4da45804739'} +SOURCE_FILES = tuple(SOURCE_HASHES) +SOURCE_BYTES = {'truememory/__init__.py': 6932, 'truememory/_platform.py': 5947, 'truememory/client.py': 13639, 'truememory/clustering.py': 22412, 'truememory/consolidation.py': 62951, 'truememory/embedding_target.py': 5225, 'truememory/engine.py': 174355, 'truememory/fts_search.py': 10285, 'truememory/hybrid.py': 12551, 'truememory/hyde.py': 8143, 'truememory/maintenance.py': 137455, 'truememory/memory_budget.py': 41094, 'truememory/model_client.py': 34164, 'truememory/model_server.py': 100943, 'truememory/personality.py': 68823, 'truememory/personality_style_vec.py': 21995, 'truememory/predictive.py': 18593, 'truememory/query_classifier.py': 5045, 'truememory/rebuild_source.py': 19160, 'truememory/reranker.py': 25401, 'truememory/salience.py': 20238, 'truememory/storage.py': 68710, 'truememory/temporal.py': 49851, 'truememory/tier_config.py': 6988, 'truememory/tier_switch/__init__.py': 319, 'truememory/tier_switch/activation.py': 33404, 'truememory/tier_switch/cache.py': 7779, 'truememory/tier_switch/job.py': 25028, 'truememory/tier_switch/manager.py': 47952, 'truememory/tier_switch/runtime.py': 38531, 'truememory/tier_switch/sensors.py': 4336, 'truememory/tier_switch/serving.py': 18642, 'truememory/tier_switch/source.py': 28269, 'truememory/tier_switch/state_machine.py': 5509, 'truememory/tier_switch/throttler.py': 14364, 'truememory/tier_switch/worker.py': 27782, 'truememory/vector_search.py': 96703} +DATA_HASHES = {'truememory/data/l3_weights.json': '5d91643f843b4639375a4324aa87cbd09815a4242b11f3fe4138bae641b2d5b8'} +SOURCE_COMMIT = 'aeecf25f78279629789d98a29bbd8bf817233391' +SOURCE_MAP_SHA = 'e1cbacd6cdfa558110647572b614d0d3983413af3dc988530a30b56d6a06cbd6' +DATA_MAP_SHA = '19d20af55c695fe0952cfc26e4fd339037ac3bbfe05fab4b14bb1ae814ca4ea2' +CASES = frozenset({"package-ping"}) +B_OS_THREAD_MAX = 4 +INTERPRETER_PACKET = RUNTIME_PACKET.with_name("mac-native-interpreter-runtime-v1.json") +INTERPRETER_PACKET_SHA = "73a77a325ba667ff84d395c8ecddd9d21d63be27914c4586c6d5e65e2ca55596" +LINKAGE_PACKET = RUNTIME_PACKET.with_name("mac-native-interpreter-linkage-v1.json") +LINKAGE_PACKET_SHA = "762d535cdb9f021729fe101c654966e52a9d8da285d5fe77bc0b1db815d43843" +ENDPOINT = RUN_ROOT / "home/.truememory/model.sock" +TERMINAL_ACK = {"type":"result_ack","version":1,"case":"package-ping"} +CACHE_KEYS = frozenset({"vector_model","reranker_model","server_embed_state","server_reranker","server_fast_encoder"}) +UNKNOWN_STAGES = ('client frame receive', 'client request measure', 'client request serialize', 'client response materialize', 'frame receive', 'request materialize', 'response metadata', 'response serialize') +CAPABILITY = {'evidence_sha256': [], 'limit_bytes': None, 'metric': 'none', 'physical_hard_cap': False, 'reason': 'No native enforcement witness has been accepted', 'scope': None, 'status': 'unsupported'} +LIMITS = {"wall_seconds": 5, "termination_seconds": 1, "cpu_soft": 2, "cpu_hard": 3, + "rss_abort_bytes": 67108864, "core_bytes": 0, "nofile": 64, + "fsize_bytes": 1048576, "input_bytes": 65536, "output_bytes": 262144, + "events": 32, "frame_bytes": 2048, "ack_ms": 100, "threads": 1} +FREE_MIN = 268435456 +MAX_INT = (1 << 63) - 1 +_SHA = re.compile(r"[0-9a-f]{64}\Z") +_PENDING = re.compile(r"state-[0-9a-f]{32}\.pending\Z") +_ATTEMPT = re.compile(r"v(?:[1-9]|[1-9][0-9])\Z") +_announcement_bytes = 0 + + +class SafetyError(ValueError): + pass + + +def integer(value: object, *, minimum: int = 0, maximum: int = MAX_INT) -> int: + if type(value) is not int or not minimum <= value <= maximum: + raise SafetyError("invalid_integer") + return value + + +def exact_keys(value: object, keys: set[str]) -> dict: + if type(value) is not dict or set(value) != keys: + raise SafetyError("invalid_fields") + return value + + +def encoded(value: object) -> bytes: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8") + + +def digest(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def hash_text(value: object) -> str: + if type(value) is not str or _SHA.fullmatch(value) is None: + raise SafetyError("invalid_hash") + return value + + +def canonical_path(value: object, root: Path) -> Path: + if type(value) is not str or not value or "\0" in value or len(value.encode("utf-8")) > 1024: + raise SafetyError("invalid_path") + path = Path(value) + if not path.is_absolute() or ".." in path.parts or str(path) != value: + raise SafetyError("noncanonical_path") + if path != root and not path.is_relative_to(root): + raise SafetyError("path_escape") + return path + + +def owned_path(path: Path, *, directory: bool = False) -> os.stat_result: + if path.resolve() != path or any(item.is_symlink() for item in (path, *path.parents)): + raise SafetyError("path_link") + info = path.lstat() + if info.st_uid != os.getuid() or info.st_mode & 0o077: + raise SafetyError("path_not_private_owned") + if directory: + if not stat.S_ISDIR(info.st_mode): + raise SafetyError("path_not_directory") + elif not stat.S_ISREG(info.st_mode) or info.st_nlink != 1: + raise SafetyError("path_not_regular_single_link") + return info + + +def read_bounded(path: Path, maximum: int, *, private: bool = True) -> bytes: + if private: + owned_path(path) + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + try: + before = os.fstat(fd) + if not stat.S_ISREG(before.st_mode) or before.st_size > maximum: + raise SafetyError("read_limit_or_type") + if private and (before.st_uid != os.getuid() or before.st_mode & 0o077 or before.st_nlink != 1): + raise SafetyError("read_not_private") + chunks: list[bytes] = [] + remaining = maximum + 1 + while remaining: + chunk = os.read(fd, min(remaining, 65536)) + if not chunk: + break + chunks.append(chunk) + remaining -= len(chunk) + after = os.fstat(fd) + data = b"".join(chunks) + if file_identity(before) != file_identity(after) or len(data) > maximum or len(data) != after.st_size: + raise SafetyError("read_changed_or_limit") + return data + finally: + os.close(fd) + + +def file_identity(info: os.stat_result) -> list[int]: + return [info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns, info.st_ctime_ns] + + +@dataclass(frozen=True) +class Sensor: + monotonic_ns: int + pressure: int + free_bytes: int + swap_used_bytes: int + + +def valid_sensor(value: object) -> Sensor: + row = exact_keys(value, {"monotonic_ns", "pressure", "free_bytes", "swap_used_bytes"}) + integer(row["monotonic_ns"], minimum=1) + integer(row["pressure"], minimum=1, maximum=4) + integer(row["free_bytes"]) + integer(row["swap_used_bytes"]) + if row["pressure"] not in (1, 2, 4): + raise SafetyError("unknown_pressure_abi") + return Sensor(**row) + + +def admission(samples: list[Sensor], now_ns: int) -> None: + integer(now_ns, minimum=1) + if len(samples) < 3: + raise SafetyError("sensor_count") + previous = None + for sample in samples: + valid_sensor(asdict(sample)) + if sample.pressure != 1 or sample.free_bytes < FREE_MIN: + raise SafetyError("pressure_or_headroom") + if previous is not None and (sample.monotonic_ns <= previous.monotonic_ns or sample.swap_used_bytes > previous.swap_used_bytes): + raise SafetyError("unordered_or_growing_swap") + previous = sample + if samples[-1].monotonic_ns - samples[0].monotonic_ns < 2_000_000_000: + raise SafetyError("sensor_window") + if not 0 <= now_ns - samples[-1].monotonic_ns <= 500_000_000: + raise SafetyError("sensor_stale") + + +@dataclass(frozen=True) +class Identity: + pid: int + uid: int + birth_ns: int + pgid: int + + +def same_owned_identity(expected: Identity, actual: Identity, worker_uid: int) -> bool: + try: + for row in (expected, actual): + integer(row.pid, minimum=1) + integer(row.uid) + integer(row.birth_ns, minimum=1) + integer(row.pgid, minimum=1) + integer(worker_uid) + except SafetyError: + return False + return expected == actual and actual.uid == worker_uid and actual.pgid == actual.pid + + + + + + + + +def case_contract(value: object) -> dict[str, object]: + row = exact_keys(value, {"id"}) + if type(row["id"]) is not str or row["id"] != "package-ping": + raise SafetyError("unsupported_B_case") + return row + + + +def validate_manifest(value: object) -> dict[str, object]: + row = exact_keys(value, {"version", "stage", "synthetic", "root_execution_go", "run_root", "home", "source_root", "source_commit", "sources", "sources_sha256", "data", "data_sha256", "runtime_packet_sha256", "interpreter_packet_sha256", "supervisor_sha256", "bootstrap_sha256", "limits", "cases"}) + if type(row["version"]) is not int or row["version"] != 1 or row["stage"] != "B" or row["synthetic"] is not True or row["root_execution_go"] is not True: + raise SafetyError("manifest_authority_or_version") + if any(type(row[name]) is not str for name in ("stage","run_root","home","source_root","source_commit","sources_sha256","data_sha256","runtime_packet_sha256","interpreter_packet_sha256")): + raise SafetyError("manifest_string_type") + if row["run_root"] != str(RUN_ROOT) or row["home"] != str(RUN_ROOT / "home") or row["source_root"] != str(SOURCE_ROOT) or row["source_commit"] != SOURCE_COMMIT: + raise SafetyError("unexpected_root_or_source") + if type(row["sources"]) is not dict or row["sources"] != SOURCE_HASHES or digest(encoded(row["sources"])) != SOURCE_MAP_SHA or row["sources_sha256"] != SOURCE_MAP_SHA: + raise SafetyError("source_map_mismatch") + if type(row["data"]) is not dict or row["data"] != DATA_HASHES or digest(encoded(row["data"])) != DATA_MAP_SHA or row["data_sha256"] != DATA_MAP_SHA: + raise SafetyError("data_map_mismatch") + if row["runtime_packet_sha256"] != RUNTIME_PACKET_SHA or row["interpreter_packet_sha256"] != INTERPRETER_PACKET_SHA: + raise SafetyError("runtime_packet_mismatch") + hash_text(row["supervisor_sha256"]) + hash_text(row["bootstrap_sha256"]) + if type(row["limits"]) is not dict or row["limits"] != LIMITS or any(type(v) is not int for v in row["limits"].values()): + raise SafetyError("limit_change") + if type(row["cases"]) is not list or len(row["cases"]) != 1: + raise SafetyError("case_count") + case_contract(row["cases"][0]) + return row + + + +def validate_ready(value: object, pid: int) -> dict: + row = exact_keys(value, {"type", "version", "pid", "limits", "audit_installed"}) + integer(pid, minimum=1) + if row["type"] != "ready" or type(row["version"]) is not int or row["version"] != 1 or type(row["pid"]) is not int or row["pid"] != pid or row["audit_installed"] is not True: + raise SafetyError("limits_ready_mismatch") + expected = {"cpu": [2, 3], "core": [0, 0], "nofile": [64, 64], "fsize": [1048576, 1048576]} + limits = exact_keys(row["limits"], set(expected)) + for name, pair in limits.items(): + if type(pair) is not list or len(pair) != 2 or any(type(number) is not int for number in pair) or pair != expected[name]: + raise SafetyError("limits_ready_mismatch") + return row + + +def result_contract(value: object, manifest: dict[str, object], child_pid: int, worker_uid: int) -> dict[str, object]: + integer(child_pid, minimum=1) + integer(worker_uid) + validate_manifest(manifest) + row = exact_keys(value, {"type", "version", "case", "passed", "proof_scope", "import_mode", "source_commit", "sources_sha256", "source_count", "data_sha256", "runtime_packet_sha256", "interpreter_packet_sha256", "details"}) + expected = {"type": "result", "version": 1, "case": "package-ping", "passed": True, "proof_scope": PROOF_SCOPE, "import_mode": "full_source_package", "source_commit": SOURCE_COMMIT, "sources_sha256": SOURCE_MAP_SHA, "source_count": 37, "data_sha256": DATA_MAP_SHA, "runtime_packet_sha256": RUNTIME_PACKET_SHA, "interpreter_packet_sha256": INTERPRETER_PACKET_SHA} + for name, expected_value in expected.items(): + if type(row[name]) is not type(expected_value) or row[name] != expected_value: + raise SafetyError("result_scope_mismatch") + detail = exact_keys(row["details"], {"package_version", "initializer_executed", "paths_exact", "ping", "timeout_ms", "source_data_loaded", "neural_imports", "caches_before", "caches_after", "socket_identity", "socket_uid", "socket_mode", "pid_file", "lock_identity", "bind_lock_held", "registry"}) + if detail["package_version"] != "0.7.6.2" or type(detail["package_version"]) is not str or detail["neural_imports"] != [] or type(detail["neural_imports"]) is not list: + raise SafetyError("package_or_neural_state") + for name in ("initializer_executed", "paths_exact", "source_data_loaded", "bind_lock_held"): + if detail[name] is not True: + raise SafetyError("source_observation_missing") + ping = exact_keys(detail["ping"], {"ok", "protocol"}) + if ping["ok"] is not True or type(ping["protocol"]) is not int or ping["protocol"] != 1 or type(detail["timeout_ms"]) is not int or detail["timeout_ms"] != 200: + raise SafetyError("ping_mismatch") + for name in ("caches_before", "caches_after"): + cache = exact_keys(detail[name], set(CACHE_KEYS)) + if any(value is not None for value in cache.values()): + raise SafetyError("neural_cache_present") + for name in ("socket_identity", "lock_identity"): + identity = exact_keys(detail[name], {"device", "inode"}) + integer(identity["device"]) + integer(identity["inode"], minimum=1) + if integer(detail["socket_uid"]) != worker_uid or integer(detail["socket_mode"]) != 0o600: + raise SafetyError("socket_owner_mode") + pid = exact_keys(detail["pid_file"], {"pid", "device", "inode", "mode"}) + if integer(pid["pid"], minimum=1) != child_pid or integer(pid["mode"]) != 0o600: + raise SafetyError("pid_file_mismatch") + integer(pid["device"]) + integer(pid["inode"], minimum=1) + registry = exact_keys(detail["registry"], {"pid", "metric", "effective_limit_bytes", "owner_count", "closing_owner_count", "lease_count", "reserved_known_bytes", "high_water_known_bytes", "unprofiled_stages", "coverage_truncated", "physical_hard_cap", "os_enforcement"}) + if integer(registry["pid"], minimum=1) != child_pid or (type(registry["metric"]) is not str or registry["metric"] != "process_rss_sampled"): + raise SafetyError("registry_identity") + limit = integer(registry["effective_limit_bytes"], minimum=1, maximum=8 * 1024**3) + if integer(registry["owner_count"]) != 1 or any(integer(registry[name]) != 0 for name in ("closing_owner_count", "lease_count", "reserved_known_bytes")): + raise SafetyError("registry_nonterminal") + integer(registry["high_water_known_bytes"], maximum=limit) + unknown = registry["unprofiled_stages"] + if type(unknown) is not list or tuple(unknown) != UNKNOWN_STAGES or any(type(item) is not str for item in unknown): + raise SafetyError("unknown_coverage_mismatch") + if registry["coverage_truncated"] is not False or registry["physical_hard_cap"] is not False: + raise SafetyError("unsupported_coverage_claim") + capability = exact_keys(registry["os_enforcement"], set(CAPABILITY)) + if capability != CAPABILITY or any(type(capability[name]) is not type(expected_value) for name, expected_value in CAPABILITY.items()): + raise SafetyError("unsupported_enforcement_claim") + if len(encoded(row)) + 1 > LIMITS["frame_bytes"]: + raise SafetyError("result_frame_limit") + return row + + + +def validate_attempt_version(value: object) -> str: + if type(value) is not str or _ATTEMPT.fullmatch(value) is None: + raise SafetyError("invalid_attempt_version") + return value + + +def evidence_paths(manifest_path: Path) -> tuple[Path, str]: + name = manifest_path.name + if manifest_path.parent != RUN_ROOT or not name.startswith("manifest-") or not name.endswith(".json"): + raise SafetyError("unexpected_manifest_path") + version = validate_attempt_version(name[len("manifest-"):-len(".json")]) + return RUN_ROOT / ("results-" + version + ".json"), version + + +class MutationPolicy: + def __init__(self, case: dict[str, object]) -> None: + case_contract(case) + self.sequence = 0 + self.socket_objects = 0 + self.model_dir = RUN_ROOT / "home/.truememory" + self.lock = self.model_dir / "model_server.lock" + self.pid = self.model_dir / "model_server.pid" + self.bound = False + self.connected = False + + def decide(self, value: object) -> dict[str, object]: + row = exact_keys(value, {"type", "version", "seq", "event", "path", "source", "flags"}) + if row["type"] != "mutation" or type(row["version"]) is not int or row["version"] != 1 or row["source"] is not None: + raise SafetyError("mutation_shape") + seq = integer(row["seq"], minimum=1, maximum=LIMITS["events"]) + if seq != self.sequence + 1: + raise SafetyError("mutation_sequence") + path = canonical_path(row["path"], RUN_ROOT) + flags = integer(row["flags"], maximum=(1 << 31) - 1) + event = row["event"] + if event == "os.mkdir" and path == self.model_dir and flags == 0o777: + purpose = "Fresh private synthetic model-server directory under child umask077" + elif event == "os.chmod" and ((path == self.model_dir and flags == 0o700) or (path == ENDPOINT and flags == 0o600 and self.bound)): + purpose = "Owned synthetic endpoint permissions" + elif event == "open" and path in (self.lock, self.pid): + ordinary = flags & ~getattr(os, "O_CLOEXEC", 0) + expected = os.O_CREAT | (os.O_RDWR if path == self.lock else os.O_WRONLY | os.O_TRUNC) + if ordinary != expected: + raise SafetyError("producer_open_flags") + purpose = "Fresh retained bind lock" if path == self.lock else "Fresh owned PID digits without newline" + elif event == "socket.__new__" and path == ENDPOINT and flags == 0 and self.socket_objects < 3: + self.socket_objects += 1 + purpose = "Owned AF_UNIX listener/client/accepted socket wrapper" + elif event == "socket.bind" and path == ENDPOINT and flags == 0 and not self.bound: + self.bound = True + purpose = "Bind fresh private AF_UNIX endpoint" + elif event == "socket.connect" and path == ENDPOINT and flags == 0 and self.bound and not self.connected: + self.connected = True + purpose = "Single synthetic client connection to owned endpoint" + else: + raise SafetyError("unplanned_B_mutation") + self.sequence = seq + return {"type":"grant", "version":1, "seq":seq, "path":str(path), "purpose":purpose} + + + +def mutation_target(grant: dict[str, object], request: dict[str, object]) -> None: + path = Path(grant["path"]) + event = request["event"] + owned_path(path.parent, directory=True) + exists = path.exists() or path.is_symlink() + if event in ("open", "os.mkdir", "socket.bind") and exists: + raise SafetyError("retained_artifact_reuse_forbidden") + if event == "os.chmod": + if path == RUN_ROOT / "home/.truememory": + owned_path(path, directory=True) + else: + socket_owned(path,pre_chmod=True) + elif event == "socket.connect": + socket_owned(path) + + +def socket_owned(path: Path,*,pre_chmod: bool = False) -> os.stat_result: + if path.resolve() != path or any(part.is_symlink() for part in (path, *path.parents)): + raise SafetyError("socket_path_link") + info = path.lstat() + if not stat.S_ISSOCK(info.st_mode) or info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) not in ((0o700,0o600) if pre_chmod else (0o600,)): + raise SafetyError("socket_not_private_owned") + return info + + +def verify_artifacts(value: dict[str, object], child_pid: int) -> None: + detail = value["details"] + socket_info = socket_owned(ENDPOINT) + lock_info = owned_path(ENDPOINT.with_name("model_server.lock")) + pid_path = ENDPOINT.with_name("model_server.pid") + pid_info = owned_path(pid_path) + if read_bounded(pid_path, 32) != str(child_pid).encode("ascii"): + raise SafetyError("actual_PID_bytes") + for name, info in (("socket_identity", socket_info), ("lock_identity", lock_info)): + if detail[name] != {"device":info.st_dev, "inode":info.st_ino}: + raise SafetyError("actual_artifact_identity") + if detail["pid_file"] != {"pid":child_pid,"device":pid_info.st_dev,"inode":pid_info.st_ino,"mode":stat.S_IMODE(pid_info.st_mode)}: + raise SafetyError("actual_PID_identity") + + + +class PressureReader: + def __init__(self) -> None: + import ctypes + self.ctypes = ctypes + self.library = ctypes.CDLL("/usr/lib/libSystem.B.dylib", use_errno=True) + self.read = self.library.sysctlbyname + self.read.argtypes = [ctypes.c_char_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.c_void_p, ctypes.c_size_t] + self.read.restype = ctypes.c_int + + def pressure(self) -> int: + c = self.ctypes + output = c.c_uint32() + size = c.c_size_t(c.sizeof(output)) + if self.read(b"kern.memorystatus_vm_pressure_level", c.byref(output), c.byref(size), None, 0) != 0 or size.value != 4: + raise SafetyError("pressure_unavailable") + if output.value not in (1, 2, 4): + raise SafetyError("pressure_abi_unknown") + return output.value + + +def runtime_contract(package_raw: bytes, interpreter_raw: bytes, linkage_raw: bytes) -> dict[str, object]: + values: list[dict[str, object]] = [] + for raw, checksum in ((package_raw, RUNTIME_PACKET_SHA), (interpreter_raw, INTERPRETER_PACKET_SHA), (linkage_raw, LINKAGE_PACKET_SHA)): + if type(raw) is not bytes or not 0 < len(raw) <= 65536 or digest(raw) != checksum: + raise SafetyError("runtime_packet_changed_or_limit") + value = json.loads(raw) + if type(value) is not dict: + raise SafetyError("runtime_packet_shape") + values.append(value) + package, interpreter, linkage = values + if type(package.get("schema")) is not int or package["schema"] != 2 or package["sources"] != SOURCE_HASHES or package["source_bytes"] != SOURCE_BYTES or package["source_commit"] != SOURCE_COMMIT or package["source_root"] != str(SOURCE_ROOT) or package["data"] != DATA_HASHES: + raise SafetyError("runtime_source_shape") + if package["source_map_sha256"] != SOURCE_MAP_SHA or package["data_map_sha256"] != DATA_MAP_SHA or package["interpreter_runtime_packet_sha256"] != INTERPRETER_PACKET_SHA or package["interpreter_linkage_packet_sha256"] != LINKAGE_PACKET_SHA: + raise SafetyError("runtime_linkage_mismatch") + if package["numpy_version"] != "2.4.6" or len(package["numpy_files"]) != 193 or len(package["psutil_pins"]) != 11 or len(interpreter["exact_content_files"]) != 84 or len(linkage["trusted_system_loader_names"]) != 13: + raise SafetyError("runtime_inventory_mismatch") + if package["staged_wheel_sha256"] != "f723b880e455fea1545c31a9b7fe01def1902af3a36609cfa084fc9548296c51" or package["runtime_closure_complete"] is not False or interpreter["runtime_closure_complete"] is not False or package["stdlib_complete_dependency_bytes_hashed"] is not False or package["trusted_OS_sharedcache_bytes_hashed"] is not False or linkage["sharedcache_bytes_hashed"] is not False: + raise SafetyError("unproved_runtime_claim") + stdlib = interpreter["trusted_stdlib"] + if stdlib["canonical_root"] != "/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12" or stdlib["python_sources_byte_hashed"] is not False or stdlib["excluded_subtrees"] != ["site-packages","test","tests","idlelib","turtledemo","ensurepip","venv"]: + raise SafetyError("stdlib_trust_mismatch") + return {"package":package,"interpreter":interpreter,"linkage":linkage} + + +def load_runtime_contract() -> dict[str, object]: + return runtime_contract(read_bounded(RUNTIME_PACKET,65536,private=False),read_bounded(INTERPRETER_PACKET,65536,private=False),read_bounded(LINKAGE_PACKET,65536,private=False)) + + +def native_file_sha256(path: Path, pin: dict[str, object]) -> str: + size = integer(pin["bytes"], maximum=64 * 1024**2) + checksum = hash_text(pin["sha256"]) + if path.resolve() != path or path.is_symlink(): + raise SafetyError("native_path_link") + fd = os.open(path,os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK) + try: + before = os.fstat(fd) + if not stat.S_ISREG(before.st_mode) or before.st_size != size or any(key in pin and pin[key] != getattr(before,"st_dev" if key=="device" else "st_ino") for key in ("device","inode")): + raise SafetyError("native_file_type_or_size") + hasher = hashlib.sha256() + count = 0 + while count <= size: + data = os.read(fd,min(65536,size-count+1)) + if not data: + break + hasher.update(data) + count += len(data) + after = os.fstat(fd) + if count != size or file_identity(before) != file_identity(after) or hasher.hexdigest() != checksum: + raise SafetyError("native_file_changed") + return checksum + finally: + os.close(fd) + + +def runtime_files(runtime: dict[str, object]) -> dict[Path, dict[str, object]]: + package, interpreter = runtime["package"], runtime["interpreter"] + files: dict[Path, dict[str, object]] = {} + groups = [package["numpy_files"],package["psutil_pins"],package["numpy_metadata"],interpreter["exact_content_files"]] + for group in groups: + for name, pin in group.items(): + path = Path(name) + if not path.is_absolute() or str(path) != name or ".." in path.parts: + raise SafetyError("runtime_file_path") + if path in files and any(files[path][key] != pin[key] for key in ("bytes","sha256")): + raise SafetyError("runtime_overlapping_pin") + integer(pin["bytes"],maximum=64*1024**2) + hash_text(pin["sha256"]) + files[path] = pin + metadata = package["psutil_metadata"] + files[Path(metadata["path"])] = metadata + for name,pin in package["staged_truememory_distribution_metadata"].items(): + path = Path("/Users/j/dev/truememory-maintenance-v1") / name + canonical_path(str(path),Path("/Users/j/dev/truememory-maintenance-v1/.work/artifacts/memory-installed-wheel-v2/metadata")) + files[path] = pin + return files + + +class PinnedPythonLoader(importlib.abc.Loader): + def __init__(self,path: Path,pin: dict[str, object]) -> None: + self.path,self.pin = path,pin + + def create_module(self,spec: importlib.machinery.ModuleSpec) -> None: + return None + + def exec_module(self,module: ModuleType) -> None: + raw = read_bounded(self.path,262144,private=False) + if len(raw) != self.pin["bytes"] or digest(raw) != self.pin["sha256"]: + raise SafetyError("python_source_hash_changed") + exec(compile(raw,str(self.path),"exec",dont_inherit=True),module.__dict__) + + +class TrustedStdlibLoader(importlib.abc.Loader): + """Compile trusted-unhashed stdlib Python bytes; ignore bytecode caches.""" + def __init__(self,path: Path) -> None: + self.path = path + + def create_module(self,spec: importlib.machinery.ModuleSpec) -> None: + return None + + def exec_module(self,module: ModuleType) -> None: + raw = read_bounded(self.path,262144,private=False) + exec(compile(raw,str(self.path),"exec",dont_inherit=True),module.__dict__) + + +class PinnedPackageFinder(importlib.abc.MetaPathFinder): + def __init__(self,runtime: dict[str, object],*,full_package: bool) -> None: + self.runtime,self.full_package = runtime,full_package + self.files = runtime_files(runtime) + self.modules: dict[str,tuple[Path,dict[str,object],bool]] = {} + groups = [runtime["package"]["psutil_pins"]] + if full_package: + groups += [runtime["package"]["numpy_files"],{str(SOURCE_ROOT/name):{"bytes":SOURCE_BYTES[name],"sha256":checksum} for name,checksum in SOURCE_HASHES.items()}] + for group in groups: + for name,pin in group.items(): + path = Path(name) + if path.is_relative_to(SOURCE_ROOT): + parts = path.relative_to(SOURCE_ROOT).parts + else: + prefix = next(part for part in ("numpy","psutil") if part in path.parts) + parts = path.parts[path.parts.index(prefix):] + leaf = parts[-1] + package = leaf == "__init__.py" + stem = leaf.split(".")[0] + module = ".".join(parts[:-1] if package else (*parts[:-1],stem)) + self.modules[module] = (path,pin,package) + self.stdlib = Path(runtime["interpreter"]["trusted_stdlib"]["canonical_root"]) + self.excluded = set(runtime["interpreter"]["trusted_stdlib"]["excluded_subtrees"]) + + def find_spec(self,fullname: str,path: object = None,target: object = None) -> importlib.machinery.ModuleSpec | None: + prefix = fullname.split(".")[0] + if prefix in ("truememory","numpy","psutil"): + if fullname not in self.modules: + raise ImportError("Unannounced package module: " + fullname) + source,pin,package = self.modules[fullname] + if source.suffix == ".so": + native_file_sha256(source,pin) + loader = importlib.machinery.ExtensionFileLoader(fullname,str(source)) + else: + loader = PinnedPythonLoader(source,pin) + return importlib.util.spec_from_file_location(fullname,str(source),loader=loader,submodule_search_locations=[str(source.parent)] if package else None) + if importlib.machinery.BuiltinImporter.find_spec(fullname) is not None or importlib.machinery.FrozenImporter.find_spec(fullname) is not None: + return None + search = path if path is not None else [str(self.stdlib),str(self.stdlib/"lib-dynload")] + spec = importlib.machinery.PathFinder.find_spec(fullname,search) + if spec is None or type(spec.origin) is not str: + raise ImportError("Unannounced runtime module: " + fullname) + source = Path(spec.origin) + if source.resolve() != source or not source.is_relative_to(self.stdlib) or any(part in self.excluded for part in source.relative_to(self.stdlib).parts): + raise ImportError("Untrusted runtime origin: " + fullname) + if source.suffix == ".so": + if source not in self.files: + raise ImportError("Unpinned extension: " + fullname) + native_file_sha256(source,self.files[source]) + elif source.suffix == ".py": + spec.loader = TrustedStdlibLoader(source) + spec.cached = None + else: + raise ImportError("Untrusted runtime type: " + fullname) + return spec + + def find_distributions(self,context: object = None) -> list[object]: + if not self.full_package or context is None or getattr(context,"name",None) != "truememory": + return [] + from importlib.metadata import PathDistribution + root = Path("/Users/j/dev/truememory-maintenance-v1/.work/artifacts/memory-installed-wheel-v2/metadata") + return [PathDistribution(root)] + + +def pinned_runtime() -> ModuleType: + runtime = load_runtime_contract() + package,_interpreter = runtime["package"],runtime["interpreter"] + if Path(sys.executable).resolve() != Path(package["python_realpath"]) or sys.version_info[:3] != (3,12,12): + raise SafetyError("interpreter_mismatch") + for path,pin in runtime_files(runtime).items(): + native_file_sha256(path,pin) + if "psutil" in sys.modules: + raise SafetyError("dependency_preloaded") + sys.meta_path.insert(0,PinnedPackageFinder(runtime,full_package=False)) + import psutil + if psutil.__version__ != "7.2.2": + raise SafetyError("dependency_version_changed") + return psutil + + +def sample(psutil: ModuleType, pressure: PressureReader) -> Sensor: + started = time.monotonic_ns() + row = Sensor(started, pressure.pressure(), psutil.virtual_memory().free, psutil.swap_memory().used) + valid_sensor(asdict(row)) + if time.monotonic_ns() - started > 500_000_000: + raise SafetyError("sensor_call_stale") + return row + + +def observe_identity(psutil: ModuleType, pid: int) -> Identity: + process = psutil.Process(pid) + born = process.create_time() + try: + valid = type(born) in (int, float) and math.isfinite(born) and born > 0 + except (OverflowError, ValueError): + valid = False + if not valid: + raise SafetyError("birth_unavailable") + return Identity(pid, process.uids().real, int(born * 1_000_000_000), os.getpgid(pid)) + + +def announce(path: Path, purpose: str) -> None: + global _announcement_bytes + line = f"Create {path}: {purpose}\n" + _announcement_bytes += len(line.encode("utf-8")) + if _announcement_bytes > 65536: + raise SafetyError("announcement_byte_limit") + sys.stdout.write(line) + sys.stdout.flush() + + +def new_log(path: Path, purpose: str) -> int: + owned_path(path.parent, directory=True) + announce(path, purpose) + return os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) + + +def write_all(fd: int, data: bytes) -> None: + while data: + count = os.write(fd, data) + if count <= 0: + raise SafetyError("write_no_progress") + data = data[count:] + + +def write_pipe(fd: int, data: bytes, deadline: float) -> None: + if len(data) > LIMITS["input_bytes"]: + raise SafetyError("pipe_frame_input_limit") + while data: + remaining = deadline - time.monotonic() + if remaining <= 0: + raise SafetyError("pipe_write_deadline") + try: + count = os.write(fd, data) + except BlockingIOError: + select.select([], [fd], [], min(remaining, 0.01)) + continue + if count <= 0: + raise SafetyError("pipe_write_no_progress") + data = data[count:] + + +def group_gone(pgid: int) -> bool: + integer(pgid, minimum=1) + try: + os.killpg(pgid, 0) + except ProcessLookupError: + return True + except OSError: + return False + return False + + +def member_identity_matches(expected: Identity, actual: Identity, group: int, worker_uid: int) -> bool: + try: + for row in (expected, actual): + integer(row.pid, minimum=1) + integer(row.uid) + integer(row.birth_ns, minimum=1) + integer(row.pgid, minimum=1) + integer(group, minimum=1) + integer(worker_uid) + except SafetyError: + return False + return expected == actual and actual.uid == worker_uid and actual.pgid == group + + +def terminate_owned(child: subprocess.Popen, expected: Identity | None, members: list[Identity], psutil: ModuleType, result: dict) -> None: + deadline = time.monotonic() + LIMITS["termination_seconds"] + for sig, interval in ((signal.SIGKILL, 0.99),): + if time.monotonic() >= deadline: + raise SafetyError("termination_deadline") + if child.poll() is None: + if expected is None: + # Popen still owns an unreaped direct child; its PID cannot be reused. + child.send_signal(sig) + result["termination_authority"] = "unreaped_direct_popen_child" + elif same_owned_identity(expected, observe_identity(psutil, child.pid), os.getuid()): + if result["descendant_containment"] == "unproven": + os.killpg(expected.pgid, sig) + result["termination_authority"] = "fresh_owned_leader_group" + else: + child.send_signal(sig) + result["termination_authority"] = "fresh_owned_direct_child" + else: + raise SafetyError("termination_identity_changed") + if sig == signal.SIGKILL and members and expected is not None: + for member in members: + if time.monotonic() >= deadline: + raise SafetyError("member_termination_deadline") + try: + actual = observe_identity(psutil, member.pid) + except psutil.NoSuchProcess: + continue + if not member_identity_matches(member, actual, expected.pgid, os.getuid()): + raise SafetyError("member_termination_identity_changed") + psutil.Process(member.pid).send_signal(sig) + if not result["reaped"]: + try: + child.wait(timeout=max(0.001, min(interval, deadline - time.monotonic()))) + result["reaped"] = True + except subprocess.TimeoutExpired: + pass + if result["reaped"] and not members: + break + result["exit_code"] = child.returncode + if not result["reaped"]: + raise SafetyError("direct_child_reap_unproven") + if result["descendant_containment"] == "unproven": + if expected is None or len(result["descendants"]) != len(members): + raise SafetyError("member_inventory_unproven") + terminal = True + for member in members: + if time.monotonic() >= deadline: + raise SafetyError("containment_proof_deadline") + try: + actual = observe_identity(psutil, member.pid) + except psutil.NoSuchProcess: + continue + if member_identity_matches(member, actual, expected.pgid, os.getuid()): + terminal = False + result["descendant_containment"] = "proven_terminal" if terminal and group_gone(expected.pgid) else "unproven" + if result["descendant_containment"] != "proven_terminal": + raise SafetyError("owned_group_containment_unproven") + + +def thread_sample_contract(value: object) -> int: + count = integer(value,minimum=1) + if count>B_OS_THREAD_MAX: + raise SafetyError("sampled_thread_limit") + return count + + +def terminal_ACK_contract(value: object) -> dict[str, object]: + row = exact_keys(value,set(TERMINAL_ACK)) + if row["type"]!="result_ack" or type(row["version"]) is not int or row["version"]!=1 or row["case"]!="package-ping": + raise SafetyError("terminal_ACK_mismatch") + return row + + +def execute_case(manifest: dict, case: dict, psutil: ModuleType, pressure: PressureReader, attempt_version: str = "v1") -> dict: + attempt_version = validate_attempt_version(attempt_version) + bootstrap = HARNESS_ROOT / "tests/native/mac-native-package-bootstrap-v3.py" + policy = MutationPolicy(case) + records: list[Sensor] = [] + result = {"case": case["id"], "implemented": True, "executed": False, "passed": False, + "blocked": None, "unproven": ["installed_Mac_package", "installed_host", "models", "retrieval", "physical_hard_cap", "complete_runtime_bytes", "native_sandbox"], + "proof_scope": PROOF_SCOPE, "import_mode": "full_source_package", "synthetic": True, + "source_commit": manifest["source_commit"], "sources": manifest["sources"], + "sensors": [asdict(row) for row in records], "rss_sampled_peak": 0, "limits": LIMITS, + "reaped": False, "exit_code": None, "identity": None, "output_bytes": 0, + "descendants": [], "descendant_containment": "not_observed"} + logs: dict[str, int] = {} + child = None + expected = None + members: list[Identity] = [] + selector = selectors.DefaultSelector() + held_lock = None + frames = bytearray() + protocol_result = None + start = time.monotonic() + try: + records = [] + for index in range(3): + records.append(sample(psutil, pressure)) + if index < 2: + time.sleep(1) + admission(records, time.monotonic_ns()) + case_contract(case) + if ENDPOINT.parent.exists() or ENDPOINT.parent.is_symlink(): + raise SafetyError("retained_endpoint_artifacts_forbidden") + load_runtime_contract() + if digest(read_bounded(SOURCE_ROOT/"truememory/data/l3_weights.json",845,private=False)) != DATA_HASHES["truememory/data/l3_weights.json"]: + raise SafetyError("source_data_changed") + for name, checksum in manifest["sources"].items(): + if digest(read_bounded(SOURCE_ROOT / name, 262144, private=False)) != checksum: + raise SafetyError("source_hash_changed") + if digest(read_bounded(bootstrap, 262144, private=False)) != manifest["bootstrap_sha256"]: + raise SafetyError("bootstrap_hash_changed") + result["sensors"] = [asdict(row) for row in records] + for channel in ("stdout", "stderr", "events"): + logs[channel] = new_log(RUN_ROOT / "logs" / (case["id"] + "-" + channel + "-" + attempt_version + ".log"), "Retained native " + channel + " evidence") + env = {"HOME": manifest["home"], "PATH": "/usr/bin:/bin", "LANG": "C.UTF-8", + "OMP_NUM_THREADS": "1", "OPENBLAS_NUM_THREADS": "1", "MKL_NUM_THREADS": "1", + "VECLIB_MAXIMUM_THREADS": "1", "NUMEXPR_NUM_THREADS": "1", "TRUEMEMORY_NO_MODEL_SERVER": "1"} + request = encoded({"version":1,"manifest":manifest}) + b"\n" + if len(request) > LIMITS["input_bytes"] - LIMITS["events"] * 256: + raise SafetyError("input_limit") + fresh = sample(psutil, pressure) + admission([*records[:-1], fresh], time.monotonic_ns()) + result["sensors"].append(asdict(fresh)) + start = time.monotonic() + child = subprocess.Popen([sys.executable, "-I", "-S", "-B", str(bootstrap)], stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, + cwd=str(RUN_ROOT), close_fds=True, start_new_session=True) + result["executed"] = True + expected = observe_identity(psutil, child.pid) + if not same_owned_identity(expected, expected, os.getuid()): + raise SafetyError("child_identity_unowned") + result["identity"] = asdict(expected) + assert child.stdin is not None and child.stdout is not None and child.stderr is not None + for stream, channel in ((child.stdout, "stdout"), (child.stderr, "stderr")): + os.set_blocking(stream.fileno(), False) + selector.register(stream, selectors.EVENT_READ, channel) + os.set_blocking(child.stdin.fileno(), False) + write_pipe(child.stdin.fileno(), request, min(start + LIMITS["wall_seconds"], time.monotonic() + 0.1)) + input_count = len(request) + previous = fresh + ready = False + next_sample = time.monotonic() + while selector.get_map(): + now = time.monotonic() + if now - start >= LIMITS["wall_seconds"]: + raise SafetyError("child_wall_deadline") + if child.poll() is None and now >= next_sample: + current = sample(psutil, pressure) + if current.pressure != 1 or current.free_bytes < FREE_MIN or current.swap_used_bytes > previous.swap_used_bytes or time.monotonic_ns() - current.monotonic_ns > 500_000_000: + raise SafetyError("during_child_sensor_refusal") + actual = observe_identity(psutil, child.pid) + if not same_owned_identity(expected, actual, os.getuid()): + raise SafetyError("child_identity_changed") + process = psutil.Process(child.pid) + descendants = process.children(recursive=True) + if descendants: + result["descendants"] = [item.pid for item in descendants[:32]] + result["descendant_containment"] = "unproven" + if len(descendants) > 32: + raise SafetyError("descendant_inventory_limit") + members = [observe_identity(psutil, item.pid) for item in descendants] + if any(member.uid != os.getuid() or member.pgid != expected.pgid for member in members): + raise SafetyError("descendant_identity_unowned") + raise SafetyError("unexpected_descendant") + threads = thread_sample_contract(process.num_threads()) + result["threads_sampled_peak"] = max(result.get("threads_sampled_peak",0),threads) + rss = integer(process.memory_info().rss) + result["rss_sampled_peak"] = max(result["rss_sampled_peak"], rss) + if rss > LIMITS["rss_abort_bytes"]: + raise SafetyError("sampled_rss_limit") + if len(result["sensors"]) >= 64: + raise SafetyError("sensor_inventory_limit") + result["sensors"].append(asdict(current)) + previous = current + next_sample = now + 0.1 + for key, _ in selector.select(0.01): + try: + chunk = os.read(key.fileobj.fileno(), 4096) + except BlockingIOError: + continue + if not chunk: + selector.unregister(key.fileobj) + continue + result["output_bytes"] += len(chunk) + if result["output_bytes"] > LIMITS["output_bytes"]: + raise SafetyError("combined_output_limit") + channel = key.data + write_all(logs[channel], chunk) + if channel != "stdout": + continue + frames.extend(chunk) + while b"\n" in frames: + line, _, rest = frames.partition(b"\n") + frames = bytearray(rest) + if len(line) > LIMITS["frame_bytes"]: + raise SafetyError("protocol_frame_limit") + frame_deadline = min(start + LIMITS["wall_seconds"], time.monotonic() + 0.1) + frame = json.loads(line) + if type(frame) is not dict: + raise SafetyError("protocol_object_required") + if frame.get("type") == "mutation": + if not ready or protocol_result is not None: + raise SafetyError("mutation_outside_ready_execution") + try: + grant = policy.decide(frame) + mutation_target(grant, frame) + except (SafetyError, OSError, ValueError) as error: + write_all(logs["events"], encoded({"request": frame, "decision": {"type": "deny", "reason": type(error).__name__ + ":" + str(error)[:256]}}) + b"\n") + os.fsync(logs["events"]) + raise + write_all(logs["events"], encoded({"request": frame, "decision": grant}) + b"\n") + os.fsync(logs["events"]) + announce(Path(grant["path"]), grant["purpose"]) + reply = encoded(grant) + b"\n" + input_count += len(reply) + if input_count > LIMITS["input_bytes"]: + raise SafetyError("input_limit") + write_pipe(child.stdin.fileno(), reply, frame_deadline) + elif frame.get("type") == "result" and protocol_result is None: + if not ready: + raise SafetyError("result_before_ready") + protocol_result = result_contract(frame,manifest,child.pid,os.getuid()) + verify_artifacts(protocol_result,child.pid) + reply = encoded(TERMINAL_ACK) + b"\n" + input_count += len(reply) + if input_count > LIMITS["input_bytes"]: + raise SafetyError("input_limit") + write_pipe(child.stdin.fileno(),reply,frame_deadline) + result["terminal_ACK_sent"] = True + elif frame.get("type") == "ready" and not ready: + validate_ready(frame, child.pid) + ready = True + result["limit_readbacks"] = frame["limits"] + else: + raise SafetyError("unexpected_protocol_frame") + if len(frames) > LIMITS["frame_bytes"]: + raise SafetyError("protocol_frame_limit") + if frames: + raise SafetyError("truncated_protocol_frame") + remaining = max(0.001, start + LIMITS["wall_seconds"] - time.monotonic()) + result["exit_code"] = child.wait(timeout=remaining) + result["reaped"] = True + expected_exit = 73 + if result.get("terminal_ACK_sent") is not True or result["exit_code"] != expected_exit or type(protocol_result) is not dict or protocol_result.get("passed") is not True or protocol_result.get("proof_scope") != PROOF_SCOPE: + raise SafetyError("case_nonterminal_or_failed") + if not group_gone(expected.pgid): + result["descendant_containment"] = "unproven" + raise SafetyError("post_exit_group_containment_unproven") + result["passed"] = True + result["child_result"] = protocol_result + except (SafetyError, OSError, ValueError, subprocess.SubprocessError, psutil.Error) as error: + result["blocked"] = type(error).__name__ + ":" + str(error)[:256] + finally: + if not result["sensors"]: + result["sensors"] = [asdict(row) for row in records] + if child is None: + result["reaped"] = True + if held_lock is not None: + try: + os.close(held_lock) + except OSError as error: + result["blocked"] = "lock_close_failed:" + str(error)[:256] + result["passed"] = False + if child is not None and (not result["reaped"] or result["descendant_containment"] == "unproven"): + try: + terminate_owned(child, expected, members, psutil, result) + except (SafetyError, OSError, ValueError, subprocess.SubprocessError, psutil.Error) as error: + result["blocked"] = "identity_or_reap_unproven:" + str(error)[:256] + result["passed"] = False + result["wall_seconds"] = time.monotonic() - start + try: + selector.close() + except OSError as error: + result["blocked"] = "selector_close_failed:" + str(error)[:256] + result["passed"] = False + if child is not None: + for stream in (child.stdin, child.stdout, child.stderr): + if stream is not None: + try: + stream.close() + except OSError as error: + result["blocked"] = "pipe_close_failed:" + str(error)[:256] + result["passed"] = False + for fd in logs.values(): + try: + os.fsync(fd) + except OSError as error: + result["blocked"] = "evidence_sync_failed:" + str(error)[:256] + result["passed"] = False + finally: + try: + os.close(fd) + except OSError as error: + result["blocked"] = "evidence_close_failed:" + str(error)[:256] + result["passed"] = False + return result + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--manifest", required=True) + parser.add_argument("--execute-root-authorized", action="store_true") + args = parser.parse_args() + if not args.execute_root_authorized or sys.platform != "darwin" or not sys.flags.isolated or not sys.flags.no_site or not sys.flags.dont_write_bytecode: + raise SafetyError("root_isolated_native_execution_required") + sys.dont_write_bytecode = True + os.umask(0o077) + manifest_path = canonical_path(args.manifest, RUN_ROOT) + target, version = evidence_paths(manifest_path) + owned_path(RUN_ROOT, directory=True) + owned_path(RUN_ROOT / "home", directory=True) + manifest_bytes = read_bounded(manifest_path, 65536) + manifest = validate_manifest(json.loads(manifest_bytes)) + if digest(read_bounded(Path(__file__).resolve(), 262144, private=False)) != manifest["supervisor_sha256"]: + raise SafetyError("supervisor_hash_changed") + results: list[dict] = [] + setup_failure = None + # Reserve before runtime admission so a retained refusal cannot be replayed. + fd = new_log(target, "Retained Stage B results; native host/model/hard-cap acceptance remains unproven") + try: + try: + psutil = pinned_runtime() + pressure = PressureReader() + for case in manifest["cases"]: + result = execute_case(manifest, case, psutil, pressure, attempt_version=version) + results.append(result) + if not result["passed"] or not result["reaped"]: + break + except (SafetyError, OSError, ValueError, ImportError) as error: + setup_failure = type(error).__name__ + ":" + str(error)[:256] + data = encoded({"version": 1, "manifest_version": version, "manifest_sha256": digest(manifest_bytes), + "proof_scope": PROOF_SCOPE, "cases": results, "setup_failure": setup_failure, + "runtime_closure_complete":False,"stdlib_bytes_hashed":False,"OS_sharedcache_bytes_hashed":False,"native_security_sandbox":False,"complete": setup_failure is None and len(results) == len(manifest["cases"]) and all(item["passed"] for item in results)}) + b"\n" + if len(data) > LIMITS["output_bytes"]: + raise SafetyError("results_byte_limit") + write_all(fd, data) + finally: + try: + os.fsync(fd) + finally: + os.close(fd) + return 0 if setup_failure is None and len(results) == len(manifest["cases"]) and all(item["passed"] for item in results) else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/native/mac-native-package-acceptance-v4.py b/tests/native/mac-native-package-acceptance-v4.py new file mode 100644 index 00000000..6a0b27ca --- /dev/null +++ b/tests/native/mac-native-package-acceptance-v4.py @@ -0,0 +1,1053 @@ +"""ROOT-run Stage B supervisor. Importing definitions never starts native work. + +This instrumented full-source harness proves one owned AF_UNIX package ping. +It does not certify installed packages, host dispatch, models or a physical cap. +""" +from __future__ import annotations + +import argparse +import hashlib +import importlib.abc +import importlib.machinery +import importlib.util +import json +import math +import os +import re +import select +import selectors +import signal +import stat +import subprocess +import sys +import time +from dataclasses import asdict, dataclass +from pathlib import Path +from types import ModuleType + +RUN_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/native-mac-v1/run-0002") +HARNESS_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/worktrees/mac-native-acceptance-v1") +SOURCE_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/worktrees/whole-process-memory-297-v1") +RUNTIME_PACKET = Path("/Users/j/dev/truememory-maintenance-v1/.work/reports/mac-native-package-runtime-pins-v2.json") +RUNTIME_PACKET_SHA = "9c7ed6eda037c200530fbb913352006886c312d4011dac5e82d1805d3ead69aa" +PROOF_SCOPE = "instrumented_unmodified_full_package_source_af_unix_ping" +SOURCE_HASHES = {'truememory/__init__.py': '00d3ab14540092b63b449d14533b17292018ae4a69cd3f3ef1345e1a02b54d98', 'truememory/_platform.py': '0688763a6fc26ef8db2b87866aabd5a2373a18f4ee735859cc6d595147e310c0', 'truememory/client.py': 'f020ac930617cd5d4848775ddd7390a61665e0fbdc62d51ccaf19bccdeeb3a33', 'truememory/clustering.py': 'ea9c2dbb7d083217983bb962f696e7bfcf08359ba5969f916fa63241baa535a5', 'truememory/consolidation.py': 'dcff2bf4c8f80315453b5078cdae575e4414adf879b69ef8f7983e72478fd474', 'truememory/embedding_target.py': 'da61626141f1ca5587eaa2fa3c27381f80834e79bc32351b5d7a3c20e86c7ddc', 'truememory/engine.py': '898f2b8dd79e8ca18154f8e4b3ef0cbfe785c27b543719f975f4a1f2cf0a010c', 'truememory/fts_search.py': '9c954bd2513ea8e3c82b33e0fbc311cd5b7346d12ee70da011df3f4244dc4389', 'truememory/hybrid.py': '17b281113b2927c6e63088c0d48dc34c2681a9943e1d4f4aa4d0a70d1c36dd17', 'truememory/hyde.py': '127e4d1c01767e13c0d963d6878f01a7329bb749b4510e831ef51c9c879276e4', 'truememory/maintenance.py': '413ffbc08f92ba3ed331cab5e0aebd67ba2cadf2e5400f8b5c09c47f80521819', 'truememory/memory_budget.py': '2d2578554d1c029c5205ac1fd27511f30e3d372a1178bf88cb7b81b0501409a4', 'truememory/model_client.py': 'a8a9332ab7cf532c98594f4f7455642f00d6235d9ca8a4552a75c68a49b7d0f3', 'truememory/model_server.py': '424986a108d0ecd9e984ed77e352fba36305bf9aa3d79517de8fd1cfa4f8701b', 'truememory/personality.py': '4a55a594360435bd56152738af061988b73e3374c3e8757e934c0b88cf4cd2af', 'truememory/personality_style_vec.py': 'f294e67b46ce4473b2cf15a49c80fc348de27f5236834dfbec832fc092c5362e', 'truememory/predictive.py': '88ae1a2f8436b305855a5884e3ac5ebe435106808533dc0bc563d2054d374c41', 'truememory/query_classifier.py': '52f1f3bfe5a75f45eb5aef389b490953422bf1d8fb03c5181bff816440bf2476', 'truememory/rebuild_source.py': 'e46639a50d35b711bc0c0b0de9e7a90652b1386b25356c14d095551673c7bd76', 'truememory/reranker.py': 'c02740b54246ca9f52fd6df90d424f18d3417c36734346ede128c91d3c66e90d', 'truememory/salience.py': 'a005256ecc20c44ad7d4825a11fafa2c2b5951a762450dfce9fd80ec7e12940e', 'truememory/storage.py': 'adc89850a897a2f9b478c8a23ad62fa438d7a214405ea21df886e029cdf4481a', 'truememory/temporal.py': '52e98b3924573db6a209ef27d0318e254e5c976a7a65d75762f901d712505a65', 'truememory/tier_config.py': 'a8e690000c74f7629690c67fb99e34d4bd87f02b78795fb68ec2009f18348489', 'truememory/tier_switch/__init__.py': '2333416f4985986b9fcf6558478f5c3cf9f75c277961c14902162f8f11ea1705', 'truememory/tier_switch/activation.py': '24b820b0ff129fb42b9c13544e42b7c4b2ec8397b427d23b61c60c3e8ff19d47', 'truememory/tier_switch/cache.py': '77bc24c176763629f83d199cc588eca9a9e7432e36e29584a2bd3d475ac314b4', 'truememory/tier_switch/job.py': 'c1def76a9510c3cd600ec11737ee068d71cedae2bc980048fba6c70bdee7a1f0', 'truememory/tier_switch/manager.py': '160a61ff2b2a4b2a7fdeb3c7fa2442ce161f5b1a426735ea3cac53829b6b8c4b', 'truememory/tier_switch/runtime.py': '861893308506c0189e50eea2d21a9da43b2c7be0eb90c56f8fd62e6bb40dc5be', 'truememory/tier_switch/sensors.py': '6471176f75ab5ddce3aa523340a6567ec24f9243c3551f4203baa8ea2f15b55b', 'truememory/tier_switch/serving.py': '25c1524a02affc127e170c16ceee0f61c2b3b221e28c5a19e9fb347aec57bef7', 'truememory/tier_switch/source.py': 'f8ffe9cd5890c5e6520ca52cfe20bad49cb789d2222e29fffc4300bdbc64f227', 'truememory/tier_switch/state_machine.py': '39ccf8d3e944728f7bf18e6efe10d0e8f9db625f9d812220554dd980a932d1b9', 'truememory/tier_switch/throttler.py': '3416ad7681a22b2478e9e4375041f7d2d364b474a81255103e11abe79d5b5f32', 'truememory/tier_switch/worker.py': '8bae8daf54ab06c0693df46578e340cbde874dda0dccfdb5bcfc2a30d3081ef7', 'truememory/vector_search.py': '2acd58a0dd858cc6a3a8c69117c1666f5e74d166ed9aeb57b81da4da45804739'} +SOURCE_FILES = tuple(SOURCE_HASHES) +SOURCE_BYTES = {'truememory/__init__.py': 6932, 'truememory/_platform.py': 5947, 'truememory/client.py': 13639, 'truememory/clustering.py': 22412, 'truememory/consolidation.py': 62951, 'truememory/embedding_target.py': 5225, 'truememory/engine.py': 174355, 'truememory/fts_search.py': 10285, 'truememory/hybrid.py': 12551, 'truememory/hyde.py': 8143, 'truememory/maintenance.py': 137455, 'truememory/memory_budget.py': 41094, 'truememory/model_client.py': 34164, 'truememory/model_server.py': 100943, 'truememory/personality.py': 68823, 'truememory/personality_style_vec.py': 21995, 'truememory/predictive.py': 18593, 'truememory/query_classifier.py': 5045, 'truememory/rebuild_source.py': 19160, 'truememory/reranker.py': 25401, 'truememory/salience.py': 20238, 'truememory/storage.py': 68710, 'truememory/temporal.py': 49851, 'truememory/tier_config.py': 6988, 'truememory/tier_switch/__init__.py': 319, 'truememory/tier_switch/activation.py': 33404, 'truememory/tier_switch/cache.py': 7779, 'truememory/tier_switch/job.py': 25028, 'truememory/tier_switch/manager.py': 47952, 'truememory/tier_switch/runtime.py': 38531, 'truememory/tier_switch/sensors.py': 4336, 'truememory/tier_switch/serving.py': 18642, 'truememory/tier_switch/source.py': 28269, 'truememory/tier_switch/state_machine.py': 5509, 'truememory/tier_switch/throttler.py': 14364, 'truememory/tier_switch/worker.py': 27782, 'truememory/vector_search.py': 96703} +DATA_HASHES = {'truememory/data/l3_weights.json': '5d91643f843b4639375a4324aa87cbd09815a4242b11f3fe4138bae641b2d5b8'} +SOURCE_COMMIT = 'aeecf25f78279629789d98a29bbd8bf817233391' +SOURCE_MAP_SHA = 'e1cbacd6cdfa558110647572b614d0d3983413af3dc988530a30b56d6a06cbd6' +DATA_MAP_SHA = '19d20af55c695fe0952cfc26e4fd339037ac3bbfe05fab4b14bb1ae814ca4ea2' +CASES = frozenset({"package-ping"}) +B_OS_THREAD_MAX = 4 +INTERPRETER_PACKET = RUNTIME_PACKET.with_name("mac-native-interpreter-runtime-v1.json") +INTERPRETER_PACKET_SHA = "73a77a325ba667ff84d395c8ecddd9d21d63be27914c4586c6d5e65e2ca55596" +LINKAGE_PACKET = RUNTIME_PACKET.with_name("mac-native-interpreter-linkage-v1.json") +LINKAGE_PACKET_SHA = "762d535cdb9f021729fe101c654966e52a9d8da285d5fe77bc0b1db815d43843" +ENDPOINT = RUN_ROOT / "home/.truememory/model.sock" +TERMINAL_ACK = {"type":"result_ack","version":1,"case":"package-ping"} +CACHE_KEYS = frozenset({"vector_model","reranker_model","server_embed_state","server_reranker","server_fast_encoder"}) +UNKNOWN_STAGES = ('client frame receive', 'client request measure', 'client request serialize', 'client response materialize', 'frame receive', 'request materialize', 'response metadata', 'response serialize') +CAPABILITY = {'evidence_sha256': [], 'limit_bytes': None, 'metric': 'none', 'physical_hard_cap': False, 'reason': 'No native enforcement witness has been accepted', 'scope': None, 'status': 'unsupported'} +LIMITS = {"wall_seconds": 5, "termination_seconds": 1, "cpu_soft": 2, "cpu_hard": 3, + "rss_abort_bytes": 67108864, "core_bytes": 0, "nofile": 64, + "fsize_bytes": 1048576, "input_bytes": 65536, "output_bytes": 262144, + "events": 32, "frame_bytes": 2048, "ack_ms": 100, "threads": 1} +FREE_MIN = 268435456 +MAX_INT = (1 << 63) - 1 +_SHA = re.compile(r"[0-9a-f]{64}\Z") +_PENDING = re.compile(r"state-[0-9a-f]{32}\.pending\Z") +_ATTEMPT = re.compile(r"v(?:[1-9]|[1-9][0-9])\Z") +_announcement_bytes = 0 + + +class SafetyError(ValueError): + pass + + +def integer(value: object, *, minimum: int = 0, maximum: int = MAX_INT) -> int: + if type(value) is not int or not minimum <= value <= maximum: + raise SafetyError("invalid_integer") + return value + + +def exact_keys(value: object, keys: set[str]) -> dict: + if type(value) is not dict or set(value) != keys: + raise SafetyError("invalid_fields") + return value + + +def encoded(value: object) -> bytes: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8") + + +def digest(data: bytes) -> str: + return hashlib.sha256(data).hexdigest() + + +def hash_text(value: object) -> str: + if type(value) is not str or _SHA.fullmatch(value) is None: + raise SafetyError("invalid_hash") + return value + + +def canonical_path(value: object, root: Path) -> Path: + if type(value) is not str or not value or "\0" in value or len(value.encode("utf-8")) > 1024: + raise SafetyError("invalid_path") + path = Path(value) + if not path.is_absolute() or ".." in path.parts or str(path) != value: + raise SafetyError("noncanonical_path") + if path != root and not path.is_relative_to(root): + raise SafetyError("path_escape") + return path + + +def owned_path(path: Path, *, directory: bool = False) -> os.stat_result: + if path.resolve() != path or any(item.is_symlink() for item in (path, *path.parents)): + raise SafetyError("path_link") + info = path.lstat() + if info.st_uid != os.getuid() or info.st_mode & 0o077: + raise SafetyError("path_not_private_owned") + if directory: + if not stat.S_ISDIR(info.st_mode): + raise SafetyError("path_not_directory") + elif not stat.S_ISREG(info.st_mode) or info.st_nlink != 1: + raise SafetyError("path_not_regular_single_link") + return info + + +def read_bounded(path: Path, maximum: int, *, private: bool = True) -> bytes: + if private: + owned_path(path) + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + try: + before = os.fstat(fd) + if not stat.S_ISREG(before.st_mode) or before.st_size > maximum: + raise SafetyError("read_limit_or_type") + if private and (before.st_uid != os.getuid() or before.st_mode & 0o077 or before.st_nlink != 1): + raise SafetyError("read_not_private") + chunks: list[bytes] = [] + remaining = maximum + 1 + while remaining: + chunk = os.read(fd, min(remaining, 65536)) + if not chunk: + break + chunks.append(chunk) + remaining -= len(chunk) + after = os.fstat(fd) + data = b"".join(chunks) + if file_identity(before) != file_identity(after) or len(data) > maximum or len(data) != after.st_size: + raise SafetyError("read_changed_or_limit") + return data + finally: + os.close(fd) + + +def file_identity(info: os.stat_result) -> list[int]: + return [info.st_dev, info.st_ino, info.st_size, info.st_mtime_ns, info.st_ctime_ns] + + +@dataclass(frozen=True) +class Sensor: + monotonic_ns: int + pressure: int + free_bytes: int + swap_used_bytes: int + + +def valid_sensor(value: object) -> Sensor: + row = exact_keys(value, {"monotonic_ns", "pressure", "free_bytes", "swap_used_bytes"}) + integer(row["monotonic_ns"], minimum=1) + integer(row["pressure"], minimum=1, maximum=4) + integer(row["free_bytes"]) + integer(row["swap_used_bytes"]) + if row["pressure"] not in (1, 2, 4): + raise SafetyError("unknown_pressure_abi") + return Sensor(**row) + + +def admission(samples: list[Sensor], now_ns: int) -> None: + integer(now_ns, minimum=1) + if len(samples) < 3: + raise SafetyError("sensor_count") + previous = None + for sample in samples: + valid_sensor(asdict(sample)) + if sample.pressure != 1 or sample.free_bytes < FREE_MIN: + raise SafetyError("pressure_or_headroom") + if previous is not None and (sample.monotonic_ns <= previous.monotonic_ns or sample.swap_used_bytes > previous.swap_used_bytes): + raise SafetyError("unordered_or_growing_swap") + previous = sample + if samples[-1].monotonic_ns - samples[0].monotonic_ns < 2_000_000_000: + raise SafetyError("sensor_window") + if not 0 <= now_ns - samples[-1].monotonic_ns <= 500_000_000: + raise SafetyError("sensor_stale") + + +@dataclass(frozen=True) +class Identity: + pid: int + uid: int + birth_ns: int + pgid: int + + +def same_owned_identity(expected: Identity, actual: Identity, worker_uid: int) -> bool: + try: + for row in (expected, actual): + integer(row.pid, minimum=1) + integer(row.uid) + integer(row.birth_ns, minimum=1) + integer(row.pgid, minimum=1) + integer(worker_uid) + except SafetyError: + return False + return expected == actual and actual.uid == worker_uid and actual.pgid == actual.pid + + + + + + + + +def case_contract(value: object) -> dict[str, object]: + row = exact_keys(value, {"id"}) + if type(row["id"]) is not str or row["id"] != "package-ping": + raise SafetyError("unsupported_B_case") + return row + + + +def validate_manifest(value: object) -> dict[str, object]: + row = exact_keys(value, {"version", "stage", "synthetic", "root_execution_go", "run_root", "home", "source_root", "source_commit", "sources", "sources_sha256", "data", "data_sha256", "runtime_packet_sha256", "interpreter_packet_sha256", "supervisor_sha256", "bootstrap_sha256", "limits", "cases"}) + if type(row["version"]) is not int or row["version"] != 1 or row["stage"] != "B" or row["synthetic"] is not True or row["root_execution_go"] is not True: + raise SafetyError("manifest_authority_or_version") + if any(type(row[name]) is not str for name in ("stage","run_root","home","source_root","source_commit","sources_sha256","data_sha256","runtime_packet_sha256","interpreter_packet_sha256")): + raise SafetyError("manifest_string_type") + if row["run_root"] != str(RUN_ROOT) or row["home"] != str(RUN_ROOT / "home") or row["source_root"] != str(SOURCE_ROOT) or row["source_commit"] != SOURCE_COMMIT: + raise SafetyError("unexpected_root_or_source") + if type(row["sources"]) is not dict or row["sources"] != SOURCE_HASHES or digest(encoded(row["sources"])) != SOURCE_MAP_SHA or row["sources_sha256"] != SOURCE_MAP_SHA: + raise SafetyError("source_map_mismatch") + if type(row["data"]) is not dict or row["data"] != DATA_HASHES or digest(encoded(row["data"])) != DATA_MAP_SHA or row["data_sha256"] != DATA_MAP_SHA: + raise SafetyError("data_map_mismatch") + if row["runtime_packet_sha256"] != RUNTIME_PACKET_SHA or row["interpreter_packet_sha256"] != INTERPRETER_PACKET_SHA: + raise SafetyError("runtime_packet_mismatch") + hash_text(row["supervisor_sha256"]) + hash_text(row["bootstrap_sha256"]) + if type(row["limits"]) is not dict or row["limits"] != LIMITS or any(type(v) is not int for v in row["limits"].values()): + raise SafetyError("limit_change") + if type(row["cases"]) is not list or len(row["cases"]) != 1: + raise SafetyError("case_count") + case_contract(row["cases"][0]) + return row + + + +def validate_ready(value: object, pid: int) -> dict: + row = exact_keys(value, {"type", "version", "pid", "limits", "audit_installed"}) + integer(pid, minimum=1) + if row["type"] != "ready" or type(row["version"]) is not int or row["version"] != 1 or type(row["pid"]) is not int or row["pid"] != pid or row["audit_installed"] is not True: + raise SafetyError("limits_ready_mismatch") + expected = {"cpu": [2, 3], "core": [0, 0], "nofile": [64, 64], "fsize": [1048576, 1048576]} + limits = exact_keys(row["limits"], set(expected)) + for name, pair in limits.items(): + if type(pair) is not list or len(pair) != 2 or any(type(number) is not int for number in pair) or pair != expected[name]: + raise SafetyError("limits_ready_mismatch") + return row + + +def result_contract(value: object, manifest: dict[str, object], child_pid: int, worker_uid: int) -> dict[str, object]: + integer(child_pid, minimum=1) + integer(worker_uid) + validate_manifest(manifest) + row = exact_keys(value, {"type", "version", "case", "passed", "proof_scope", "import_mode", "source_commit", "sources_sha256", "source_count", "data_sha256", "runtime_packet_sha256", "interpreter_packet_sha256", "details"}) + expected = {"type": "result", "version": 1, "case": "package-ping", "passed": True, "proof_scope": PROOF_SCOPE, "import_mode": "full_source_package", "source_commit": SOURCE_COMMIT, "sources_sha256": SOURCE_MAP_SHA, "source_count": 37, "data_sha256": DATA_MAP_SHA, "runtime_packet_sha256": RUNTIME_PACKET_SHA, "interpreter_packet_sha256": INTERPRETER_PACKET_SHA} + for name, expected_value in expected.items(): + if type(row[name]) is not type(expected_value) or row[name] != expected_value: + raise SafetyError("result_scope_mismatch") + detail = exact_keys(row["details"], {"package_version", "initializer_executed", "paths_exact", "ping", "timeout_ms", "source_data_loaded", "neural_imports", "caches_before", "caches_after", "socket_identity", "socket_uid", "socket_mode", "pid_file", "lock_identity", "bind_lock_held", "registry"}) + if detail["package_version"] != "0.7.6.2" or type(detail["package_version"]) is not str or detail["neural_imports"] != [] or type(detail["neural_imports"]) is not list: + raise SafetyError("package_or_neural_state") + for name in ("initializer_executed", "paths_exact", "source_data_loaded", "bind_lock_held"): + if detail[name] is not True: + raise SafetyError("source_observation_missing") + ping = exact_keys(detail["ping"], {"ok", "protocol"}) + if ping["ok"] is not True or type(ping["protocol"]) is not int or ping["protocol"] != 1 or type(detail["timeout_ms"]) is not int or detail["timeout_ms"] != 200: + raise SafetyError("ping_mismatch") + for name in ("caches_before", "caches_after"): + cache = exact_keys(detail[name], set(CACHE_KEYS)) + if any(value is not None for value in cache.values()): + raise SafetyError("neural_cache_present") + for name in ("socket_identity", "lock_identity"): + identity = exact_keys(detail[name], {"device", "inode"}) + integer(identity["device"]) + integer(identity["inode"], minimum=1) + if integer(detail["socket_uid"]) != worker_uid or integer(detail["socket_mode"]) != 0o600: + raise SafetyError("socket_owner_mode") + pid = exact_keys(detail["pid_file"], {"pid", "device", "inode", "mode"}) + if integer(pid["pid"], minimum=1) != child_pid or integer(pid["mode"]) != 0o600: + raise SafetyError("pid_file_mismatch") + integer(pid["device"]) + integer(pid["inode"], minimum=1) + registry = exact_keys(detail["registry"], {"pid", "metric", "effective_limit_bytes", "owner_count", "closing_owner_count", "lease_count", "reserved_known_bytes", "high_water_known_bytes", "unprofiled_stages", "coverage_truncated", "physical_hard_cap", "os_enforcement"}) + if integer(registry["pid"], minimum=1) != child_pid or (type(registry["metric"]) is not str or registry["metric"] != "process_rss_sampled"): + raise SafetyError("registry_identity") + limit = integer(registry["effective_limit_bytes"], minimum=1, maximum=8 * 1024**3) + if integer(registry["owner_count"]) != 1 or any(integer(registry[name]) != 0 for name in ("closing_owner_count", "lease_count", "reserved_known_bytes")): + raise SafetyError("registry_nonterminal") + integer(registry["high_water_known_bytes"], maximum=limit) + unknown = registry["unprofiled_stages"] + if type(unknown) is not list or tuple(unknown) != UNKNOWN_STAGES or any(type(item) is not str for item in unknown): + raise SafetyError("unknown_coverage_mismatch") + if registry["coverage_truncated"] is not False or registry["physical_hard_cap"] is not False: + raise SafetyError("unsupported_coverage_claim") + capability = exact_keys(registry["os_enforcement"], set(CAPABILITY)) + if capability != CAPABILITY or any(type(capability[name]) is not type(expected_value) for name, expected_value in CAPABILITY.items()): + raise SafetyError("unsupported_enforcement_claim") + if len(encoded(row)) + 1 > LIMITS["frame_bytes"]: + raise SafetyError("result_frame_limit") + return row + + + +def validate_attempt_version(value: object) -> str: + if type(value) is not str or _ATTEMPT.fullmatch(value) is None: + raise SafetyError("invalid_attempt_version") + return value + + +def evidence_paths(manifest_path: Path) -> tuple[Path, str]: + name = manifest_path.name + if manifest_path.parent != RUN_ROOT or not name.startswith("manifest-") or not name.endswith(".json"): + raise SafetyError("unexpected_manifest_path") + version = validate_attempt_version(name[len("manifest-"):-len(".json")]) + return RUN_ROOT / ("results-" + version + ".json"), version + + +class MutationPolicy: + def __init__(self, case: dict[str, object]) -> None: + case_contract(case) + self.sequence = 0 + self.socket_objects = 0 + self.model_dir = RUN_ROOT / "home/.truememory" + self.lock = self.model_dir / "model_server.lock" + self.pid = self.model_dir / "model_server.pid" + self.bound = False + self.connected = False + + def decide(self, value: object) -> dict[str, object]: + row = exact_keys(value, {"type", "version", "seq", "event", "path", "source", "flags"}) + if row["type"] != "mutation" or type(row["version"]) is not int or row["version"] != 1 or row["source"] is not None: + raise SafetyError("mutation_shape") + seq = integer(row["seq"], minimum=1, maximum=LIMITS["events"]) + if seq != self.sequence + 1: + raise SafetyError("mutation_sequence") + path = canonical_path(row["path"], RUN_ROOT) + flags = integer(row["flags"], maximum=(1 << 31) - 1) + event = row["event"] + if event == "os.mkdir" and path == self.model_dir and flags == 0o777: + purpose = "Fresh private synthetic model-server directory under child umask077" + elif event == "os.chmod" and ((path == self.model_dir and flags == 0o700) or (path == ENDPOINT and flags == 0o600 and self.bound)): + purpose = "Owned synthetic endpoint permissions" + elif event == "open" and path in (self.lock, self.pid): + ordinary = flags & ~getattr(os, "O_CLOEXEC", 0) + expected = os.O_CREAT | (os.O_RDWR if path == self.lock else os.O_WRONLY | os.O_TRUNC) + if ordinary != expected: + raise SafetyError("producer_open_flags") + purpose = "Fresh retained bind lock" if path == self.lock else "Fresh owned PID digits without newline" + elif event == "socket.__new__" and path == ENDPOINT and flags == 0 and self.socket_objects < 3: + self.socket_objects += 1 + purpose = "Owned AF_UNIX listener/client/accepted socket wrapper" + elif event == "socket.bind" and path == ENDPOINT and flags == 0 and not self.bound: + self.bound = True + purpose = "Bind fresh private AF_UNIX endpoint" + elif event == "socket.connect" and path == ENDPOINT and flags == 0 and self.bound and not self.connected: + self.connected = True + purpose = "Single synthetic client connection to owned endpoint" + else: + raise SafetyError("unplanned_B_mutation") + self.sequence = seq + return {"type":"grant", "version":1, "seq":seq, "path":str(path), "purpose":purpose} + + + +def mutation_target(grant: dict[str, object], request: dict[str, object]) -> None: + path = Path(grant["path"]) + event = request["event"] + owned_path(path.parent, directory=True) + exists = path.exists() or path.is_symlink() + if event in ("open", "os.mkdir", "socket.bind") and exists: + raise SafetyError("retained_artifact_reuse_forbidden") + if event == "os.chmod": + if path == RUN_ROOT / "home/.truememory": + owned_path(path, directory=True) + else: + socket_owned(path,pre_chmod=True) + elif event == "socket.connect": + socket_owned(path) + + +def socket_owned(path: Path,*,pre_chmod: bool = False) -> os.stat_result: + if path.resolve() != path or any(part.is_symlink() for part in (path, *path.parents)): + raise SafetyError("socket_path_link") + info = path.lstat() + if not stat.S_ISSOCK(info.st_mode) or info.st_uid != os.getuid() or stat.S_IMODE(info.st_mode) not in ((0o700,0o600) if pre_chmod else (0o600,)): + raise SafetyError("socket_not_private_owned") + return info + + +def verify_artifacts(value: dict[str, object], child_pid: int) -> None: + detail = value["details"] + socket_info = socket_owned(ENDPOINT) + lock_info = owned_path(ENDPOINT.with_name("model_server.lock")) + pid_path = ENDPOINT.with_name("model_server.pid") + pid_info = owned_path(pid_path) + if read_bounded(pid_path, 32) != str(child_pid).encode("ascii"): + raise SafetyError("actual_PID_bytes") + for name, info in (("socket_identity", socket_info), ("lock_identity", lock_info)): + if detail[name] != {"device":info.st_dev, "inode":info.st_ino}: + raise SafetyError("actual_artifact_identity") + if detail["pid_file"] != {"pid":child_pid,"device":pid_info.st_dev,"inode":pid_info.st_ino,"mode":stat.S_IMODE(pid_info.st_mode)}: + raise SafetyError("actual_PID_identity") + + + +class PressureReader: + def __init__(self) -> None: + import ctypes + self.ctypes = ctypes + self.library = ctypes.CDLL("/usr/lib/libSystem.B.dylib", use_errno=True) + self.read = self.library.sysctlbyname + self.read.argtypes = [ctypes.c_char_p, ctypes.c_void_p, ctypes.POINTER(ctypes.c_size_t), ctypes.c_void_p, ctypes.c_size_t] + self.read.restype = ctypes.c_int + + def pressure(self) -> int: + c = self.ctypes + output = c.c_uint32() + size = c.c_size_t(c.sizeof(output)) + if self.read(b"kern.memorystatus_vm_pressure_level", c.byref(output), c.byref(size), None, 0) != 0 or size.value != 4: + raise SafetyError("pressure_unavailable") + if output.value not in (1, 2, 4): + raise SafetyError("pressure_abi_unknown") + return output.value + + +def runtime_contract(package_raw: bytes, interpreter_raw: bytes, linkage_raw: bytes) -> dict[str, object]: + values: list[dict[str, object]] = [] + for raw, checksum in ((package_raw, RUNTIME_PACKET_SHA), (interpreter_raw, INTERPRETER_PACKET_SHA), (linkage_raw, LINKAGE_PACKET_SHA)): + if type(raw) is not bytes or not 0 < len(raw) <= 65536 or digest(raw) != checksum: + raise SafetyError("runtime_packet_changed_or_limit") + value = json.loads(raw) + if type(value) is not dict: + raise SafetyError("runtime_packet_shape") + values.append(value) + package, interpreter, linkage = values + if type(package.get("schema")) is not int or package["schema"] != 2 or package["sources"] != SOURCE_HASHES or package["source_bytes"] != SOURCE_BYTES or package["source_commit"] != SOURCE_COMMIT or package["source_root"] != str(SOURCE_ROOT) or package["data"] != DATA_HASHES: + raise SafetyError("runtime_source_shape") + if package["source_map_sha256"] != SOURCE_MAP_SHA or package["data_map_sha256"] != DATA_MAP_SHA or package["interpreter_runtime_packet_sha256"] != INTERPRETER_PACKET_SHA or package["interpreter_linkage_packet_sha256"] != LINKAGE_PACKET_SHA: + raise SafetyError("runtime_linkage_mismatch") + if package["numpy_version"] != "2.4.6" or len(package["numpy_files"]) != 193 or len(package["psutil_pins"]) != 11 or len(interpreter["exact_content_files"]) != 84 or len(linkage["trusted_system_loader_names"]) != 13: + raise SafetyError("runtime_inventory_mismatch") + if package["staged_wheel_sha256"] != "f723b880e455fea1545c31a9b7fe01def1902af3a36609cfa084fc9548296c51" or package["runtime_closure_complete"] is not False or interpreter["runtime_closure_complete"] is not False or package["stdlib_complete_dependency_bytes_hashed"] is not False or package["trusted_OS_sharedcache_bytes_hashed"] is not False or linkage["sharedcache_bytes_hashed"] is not False: + raise SafetyError("unproved_runtime_claim") + stdlib = interpreter["trusted_stdlib"] + if stdlib["canonical_root"] != "/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12" or stdlib["python_sources_byte_hashed"] is not False or stdlib["excluded_subtrees"] != ["site-packages","test","tests","idlelib","turtledemo","ensurepip","venv"]: + raise SafetyError("stdlib_trust_mismatch") + return {"package":package,"interpreter":interpreter,"linkage":linkage} + + +def load_runtime_contract() -> dict[str, object]: + return runtime_contract(read_bounded(RUNTIME_PACKET,65536,private=False),read_bounded(INTERPRETER_PACKET,65536,private=False),read_bounded(LINKAGE_PACKET,65536,private=False)) + + +def native_file_sha256(path: Path, pin: dict[str, object]) -> str: + size = integer(pin["bytes"], maximum=64 * 1024**2) + checksum = hash_text(pin["sha256"]) + if path.resolve() != path or path.is_symlink(): + raise SafetyError("native_path_link") + fd = os.open(path,os.O_RDONLY|os.O_NOFOLLOW|os.O_NONBLOCK) + try: + before = os.fstat(fd) + if not stat.S_ISREG(before.st_mode) or before.st_size != size or any(key in pin and pin[key] != getattr(before,"st_dev" if key=="device" else "st_ino") for key in ("device","inode")): + raise SafetyError("native_file_type_or_size") + hasher = hashlib.sha256() + count = 0 + while count <= size: + data = os.read(fd,min(65536,size-count+1)) + if not data: + break + hasher.update(data) + count += len(data) + after = os.fstat(fd) + if count != size or file_identity(before) != file_identity(after) or hasher.hexdigest() != checksum: + raise SafetyError("native_file_changed") + return checksum + finally: + os.close(fd) + + +def runtime_files(runtime: dict[str, object]) -> dict[Path, dict[str, object]]: + package, interpreter = runtime["package"], runtime["interpreter"] + files: dict[Path, dict[str, object]] = {} + groups = [package["numpy_files"],package["psutil_pins"],package["numpy_metadata"],interpreter["exact_content_files"]] + for group in groups: + for name, pin in group.items(): + path = Path(name) + if not path.is_absolute() or str(path) != name or ".." in path.parts: + raise SafetyError("runtime_file_path") + if path in files and any(files[path][key] != pin[key] for key in ("bytes","sha256")): + raise SafetyError("runtime_overlapping_pin") + integer(pin["bytes"],maximum=64*1024**2) + hash_text(pin["sha256"]) + files[path] = pin + metadata = package["psutil_metadata"] + files[Path(metadata["path"])] = metadata + for name,pin in package["staged_truememory_distribution_metadata"].items(): + path = Path("/Users/j/dev/truememory-maintenance-v1") / name + canonical_path(str(path),Path("/Users/j/dev/truememory-maintenance-v1/.work/artifacts/memory-installed-wheel-v2/metadata")) + files[path] = pin + return files + + +class PinnedPythonLoader(importlib.abc.Loader): + def __init__(self,path: Path,pin: dict[str, object]) -> None: + self.path,self.pin = path,pin + + def create_module(self,spec: importlib.machinery.ModuleSpec) -> None: + return None + + def exec_module(self,module: ModuleType) -> None: + raw = read_bounded(self.path,262144,private=False) + if len(raw) != self.pin["bytes"] or digest(raw) != self.pin["sha256"]: + raise SafetyError("python_source_hash_changed") + exec(compile(raw,str(self.path),"exec",dont_inherit=True),module.__dict__) + + +class TrustedStdlibLoader(importlib.abc.Loader): + """Compile trusted-unhashed stdlib Python bytes; ignore bytecode caches.""" + def __init__(self,path: Path) -> None: + self.path = path + + def create_module(self,spec: importlib.machinery.ModuleSpec) -> None: + return None + + def exec_module(self,module: ModuleType) -> None: + raw = read_bounded(self.path,262144,private=False) + exec(compile(raw,str(self.path),"exec",dont_inherit=True),module.__dict__) + + +class PinnedPackageFinder(importlib.abc.MetaPathFinder): + def __init__(self,runtime: dict[str, object],*,full_package: bool) -> None: + self.runtime,self.full_package = runtime,full_package + self.files = runtime_files(runtime) + self.modules: dict[str,tuple[Path,dict[str,object],bool]] = {} + groups = [runtime["package"]["psutil_pins"]] + if full_package: + groups += [runtime["package"]["numpy_files"],{str(SOURCE_ROOT/name):{"bytes":SOURCE_BYTES[name],"sha256":checksum} for name,checksum in SOURCE_HASHES.items()}] + for group in groups: + for name,pin in group.items(): + path = Path(name) + if path.is_relative_to(SOURCE_ROOT): + parts = path.relative_to(SOURCE_ROOT).parts + else: + prefix = next(part for part in ("numpy","psutil") if part in path.parts) + parts = path.parts[path.parts.index(prefix):] + leaf = parts[-1] + package = leaf == "__init__.py" + stem = leaf.split(".")[0] + module = ".".join(parts[:-1] if package else (*parts[:-1],stem)) + self.modules[module] = (path,pin,package) + self.stdlib = Path(runtime["interpreter"]["trusted_stdlib"]["canonical_root"]) + self.excluded = set(runtime["interpreter"]["trusted_stdlib"]["excluded_subtrees"]) + + def find_spec(self,fullname: str,path: object = None,target: object = None) -> importlib.machinery.ModuleSpec | None: + prefix = fullname.split(".")[0] + if prefix in ("truememory","numpy","psutil"): + if fullname not in self.modules: + raise ImportError("Unannounced package module: " + fullname) + source,pin,package = self.modules[fullname] + if source.suffix == ".so": + native_file_sha256(source,pin) + loader = importlib.machinery.ExtensionFileLoader(fullname,str(source)) + else: + loader = PinnedPythonLoader(source,pin) + return importlib.util.spec_from_file_location(fullname,str(source),loader=loader,submodule_search_locations=[str(source.parent)] if package else None) + if importlib.machinery.BuiltinImporter.find_spec(fullname) is not None or importlib.machinery.FrozenImporter.find_spec(fullname) is not None: + return None + search = path if path is not None else [str(self.stdlib),str(self.stdlib/"lib-dynload")] + spec = importlib.machinery.PathFinder.find_spec(fullname,search) + if spec is None or type(spec.origin) is not str: + raise ImportError("Unannounced runtime module: " + fullname) + source = Path(spec.origin) + if source.resolve() != source or not source.is_relative_to(self.stdlib) or any(part in self.excluded for part in source.relative_to(self.stdlib).parts): + raise ImportError("Untrusted runtime origin: " + fullname) + if source.suffix == ".so": + if source not in self.files: + raise ImportError("Unpinned extension: " + fullname) + native_file_sha256(source,self.files[source]) + elif source.suffix == ".py": + spec.loader = TrustedStdlibLoader(source) + spec.cached = None + else: + raise ImportError("Untrusted runtime type: " + fullname) + return spec + + def find_distributions(self,context: object = None) -> list[object]: + if not self.full_package or context is None or getattr(context,"name",None) != "truememory": + return [] + from importlib.metadata import PathDistribution + root = Path("/Users/j/dev/truememory-maintenance-v1/.work/artifacts/memory-installed-wheel-v2/metadata") + return [PathDistribution(root)] + + +def pinned_runtime() -> ModuleType: + runtime = load_runtime_contract() + package,_interpreter = runtime["package"],runtime["interpreter"] + if Path(sys.executable).resolve() != Path(package["python_realpath"]) or sys.version_info[:3] != (3,12,12): + raise SafetyError("interpreter_mismatch") + for path,pin in runtime_files(runtime).items(): + native_file_sha256(path,pin) + if "psutil" in sys.modules: + raise SafetyError("dependency_preloaded") + sys.meta_path.insert(0,PinnedPackageFinder(runtime,full_package=False)) + import psutil + if psutil.__version__ != "7.2.2": + raise SafetyError("dependency_version_changed") + return psutil + + +def sample(psutil: ModuleType, pressure: PressureReader) -> Sensor: + started = time.monotonic_ns() + row = Sensor(started, pressure.pressure(), psutil.virtual_memory().free, psutil.swap_memory().used) + valid_sensor(asdict(row)) + if time.monotonic_ns() - started > 500_000_000: + raise SafetyError("sensor_call_stale") + return row + + +def observe_identity(psutil: ModuleType, pid: int) -> Identity: + process = psutil.Process(pid) + born = process.create_time() + try: + valid = type(born) in (int, float) and math.isfinite(born) and born > 0 + except (OverflowError, ValueError): + valid = False + if not valid: + raise SafetyError("birth_unavailable") + return Identity(pid, process.uids().real, int(born * 1_000_000_000), os.getpgid(pid)) + + +def announce(path: Path, purpose: str) -> None: + global _announcement_bytes + line = f"Create {path}: {purpose}\n" + _announcement_bytes += len(line.encode("utf-8")) + if _announcement_bytes > 65536: + raise SafetyError("announcement_byte_limit") + sys.stdout.write(line) + sys.stdout.flush() + + +def new_log(path: Path, purpose: str) -> int: + owned_path(path.parent, directory=True) + announce(path, purpose) + return os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW, 0o600) + + +def write_all(fd: int, data: bytes) -> None: + while data: + count = os.write(fd, data) + if count <= 0: + raise SafetyError("write_no_progress") + data = data[count:] + + +def write_pipe(fd: int, data: bytes, deadline: float) -> None: + if len(data) > LIMITS["input_bytes"]: + raise SafetyError("pipe_frame_input_limit") + while data: + remaining = deadline - time.monotonic() + if remaining <= 0: + raise SafetyError("pipe_write_deadline") + try: + count = os.write(fd, data) + except BlockingIOError: + select.select([], [fd], [], min(remaining, 0.01)) + continue + if count <= 0: + raise SafetyError("pipe_write_no_progress") + data = data[count:] + + +def group_gone(pgid: int) -> bool: + integer(pgid, minimum=1) + try: + os.killpg(pgid, 0) + except ProcessLookupError: + return True + except OSError: + return False + return False + + +def member_identity_matches(expected: Identity, actual: Identity, group: int, worker_uid: int) -> bool: + try: + for row in (expected, actual): + integer(row.pid, minimum=1) + integer(row.uid) + integer(row.birth_ns, minimum=1) + integer(row.pgid, minimum=1) + integer(group, minimum=1) + integer(worker_uid) + except SafetyError: + return False + return expected == actual and actual.uid == worker_uid and actual.pgid == group + + +def terminate_owned(child: subprocess.Popen, expected: Identity | None, members: list[Identity], psutil: ModuleType, result: dict) -> None: + deadline = time.monotonic() + LIMITS["termination_seconds"] + for sig, interval in ((signal.SIGKILL, 0.99),): + if time.monotonic() >= deadline: + raise SafetyError("termination_deadline") + if child.poll() is None: + if expected is None: + # Popen still owns an unreaped direct child; its PID cannot be reused. + child.send_signal(sig) + result["termination_authority"] = "unreaped_direct_popen_child" + elif same_owned_identity(expected, observe_identity(psutil, child.pid), os.getuid()): + if result["descendant_containment"] == "unproven": + os.killpg(expected.pgid, sig) + result["termination_authority"] = "fresh_owned_leader_group" + else: + child.send_signal(sig) + result["termination_authority"] = "fresh_owned_direct_child" + else: + raise SafetyError("termination_identity_changed") + if sig == signal.SIGKILL and members and expected is not None: + for member in members: + if time.monotonic() >= deadline: + raise SafetyError("member_termination_deadline") + try: + actual = observe_identity(psutil, member.pid) + except psutil.NoSuchProcess: + continue + if not member_identity_matches(member, actual, expected.pgid, os.getuid()): + raise SafetyError("member_termination_identity_changed") + psutil.Process(member.pid).send_signal(sig) + if not result["reaped"]: + try: + child.wait(timeout=max(0.001, min(interval, deadline - time.monotonic()))) + result["reaped"] = True + except subprocess.TimeoutExpired: + pass + if result["reaped"] and not members: + break + result["exit_code"] = child.returncode + if not result["reaped"]: + raise SafetyError("direct_child_reap_unproven") + if result["descendant_containment"] == "unproven": + if expected is None or len(result["descendants"]) != len(members): + raise SafetyError("member_inventory_unproven") + terminal = True + for member in members: + if time.monotonic() >= deadline: + raise SafetyError("containment_proof_deadline") + try: + actual = observe_identity(psutil, member.pid) + except psutil.NoSuchProcess: + continue + if member_identity_matches(member, actual, expected.pgid, os.getuid()): + terminal = False + result["descendant_containment"] = "proven_terminal" if terminal and group_gone(expected.pgid) else "unproven" + if result["descendant_containment"] != "proven_terminal": + raise SafetyError("owned_group_containment_unproven") + + +def thread_sample_contract(value: object) -> int: + count = integer(value,minimum=1) + if count>B_OS_THREAD_MAX: + raise SafetyError("sampled_thread_limit") + return count + + +def terminal_ACK_contract(value: object) -> dict[str, object]: + row = exact_keys(value,set(TERMINAL_ACK)) + if row["type"]!="result_ack" or type(row["version"]) is not int or row["version"]!=1 or row["case"]!="package-ping": + raise SafetyError("terminal_ACK_mismatch") + return row + + +def execute_case(manifest: dict, case: dict, psutil: ModuleType, pressure: PressureReader, attempt_version: str = "v1") -> dict: + attempt_version = validate_attempt_version(attempt_version) + bootstrap = HARNESS_ROOT / "tests/native/mac-native-package-bootstrap-v4.py" + policy = MutationPolicy(case) + records: list[Sensor] = [] + result = {"case": case["id"], "implemented": True, "executed": False, "passed": False, + "blocked": None, "unproven": ["installed_Mac_package", "installed_host", "models", "retrieval", "physical_hard_cap", "complete_runtime_bytes", "native_sandbox"], + "proof_scope": PROOF_SCOPE, "import_mode": "full_source_package", "synthetic": True, + "source_commit": manifest["source_commit"], "sources": manifest["sources"], + "sensors": [asdict(row) for row in records], "rss_sampled_peak": 0, "limits": LIMITS, + "reaped": False, "exit_code": None, "identity": None, "output_bytes": 0, + "descendants": [], "descendant_containment": "not_observed"} + logs: dict[str, int] = {} + child = None + expected = None + members: list[Identity] = [] + selector = selectors.DefaultSelector() + held_lock = None + frames = bytearray() + protocol_result = None + start = time.monotonic() + try: + records = [] + for index in range(3): + records.append(sample(psutil, pressure)) + if index < 2: + time.sleep(1) + admission(records, time.monotonic_ns()) + case_contract(case) + if ENDPOINT.parent.exists() or ENDPOINT.parent.is_symlink(): + raise SafetyError("retained_endpoint_artifacts_forbidden") + load_runtime_contract() + if digest(read_bounded(SOURCE_ROOT/"truememory/data/l3_weights.json",845,private=False)) != DATA_HASHES["truememory/data/l3_weights.json"]: + raise SafetyError("source_data_changed") + for name, checksum in manifest["sources"].items(): + if digest(read_bounded(SOURCE_ROOT / name, 262144, private=False)) != checksum: + raise SafetyError("source_hash_changed") + if digest(read_bounded(bootstrap, 262144, private=False)) != manifest["bootstrap_sha256"]: + raise SafetyError("bootstrap_hash_changed") + result["sensors"] = [asdict(row) for row in records] + for channel in ("stdout", "stderr", "events"): + logs[channel] = new_log(RUN_ROOT / "logs" / (case["id"] + "-" + channel + "-" + attempt_version + ".log"), "Retained native " + channel + " evidence") + env = {"HOME": manifest["home"], "PATH": "/usr/bin:/bin", "LANG": "C.UTF-8", + "OMP_NUM_THREADS": "1", "OPENBLAS_NUM_THREADS": "1", "MKL_NUM_THREADS": "1", + "VECLIB_MAXIMUM_THREADS": "1", "NUMEXPR_NUM_THREADS": "1", "TRUEMEMORY_NO_MODEL_SERVER": "1"} + request = encoded({"version":1,"manifest":manifest}) + b"\n" + if len(request) > LIMITS["input_bytes"] - LIMITS["events"] * 256: + raise SafetyError("input_limit") + fresh = sample(psutil, pressure) + admission([*records[:-1], fresh], time.monotonic_ns()) + result["sensors"].append(asdict(fresh)) + start = time.monotonic() + child = subprocess.Popen([sys.executable, "-I", "-S", "-B", str(bootstrap)], stdin=subprocess.PIPE, + stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=env, + cwd=str(RUN_ROOT), close_fds=True, start_new_session=True) + result["executed"] = True + expected = observe_identity(psutil, child.pid) + if not same_owned_identity(expected, expected, os.getuid()): + raise SafetyError("child_identity_unowned") + result["identity"] = asdict(expected) + assert child.stdin is not None and child.stdout is not None and child.stderr is not None + for stream, channel in ((child.stdout, "stdout"), (child.stderr, "stderr")): + os.set_blocking(stream.fileno(), False) + selector.register(stream, selectors.EVENT_READ, channel) + os.set_blocking(child.stdin.fileno(), False) + write_pipe(child.stdin.fileno(), request, min(start + LIMITS["wall_seconds"], time.monotonic() + 0.1)) + input_count = len(request) + previous = fresh + ready = False + next_sample = time.monotonic() + while selector.get_map(): + now = time.monotonic() + if now - start >= LIMITS["wall_seconds"]: + raise SafetyError("child_wall_deadline") + if child.poll() is None and now >= next_sample: + current = sample(psutil, pressure) + if current.pressure != 1 or current.free_bytes < FREE_MIN or current.swap_used_bytes > previous.swap_used_bytes or time.monotonic_ns() - current.monotonic_ns > 500_000_000: + raise SafetyError("during_child_sensor_refusal") + actual = observe_identity(psutil, child.pid) + if not same_owned_identity(expected, actual, os.getuid()): + raise SafetyError("child_identity_changed") + process = psutil.Process(child.pid) + descendants = process.children(recursive=True) + if descendants: + result["descendants"] = [item.pid for item in descendants[:32]] + result["descendant_containment"] = "unproven" + if len(descendants) > 32: + raise SafetyError("descendant_inventory_limit") + members = [observe_identity(psutil, item.pid) for item in descendants] + if any(member.uid != os.getuid() or member.pgid != expected.pgid for member in members): + raise SafetyError("descendant_identity_unowned") + raise SafetyError("unexpected_descendant") + threads = thread_sample_contract(process.num_threads()) + result["threads_sampled_peak"] = max(result.get("threads_sampled_peak",0),threads) + rss = integer(process.memory_info().rss) + result["rss_sampled_peak"] = max(result["rss_sampled_peak"], rss) + if rss > LIMITS["rss_abort_bytes"]: + raise SafetyError("sampled_rss_limit") + if len(result["sensors"]) >= 64: + raise SafetyError("sensor_inventory_limit") + result["sensors"].append(asdict(current)) + previous = current + next_sample = now + 0.1 + for key, _ in selector.select(0.01): + try: + chunk = os.read(key.fileobj.fileno(), 4096) + except BlockingIOError: + continue + if not chunk: + selector.unregister(key.fileobj) + continue + result["output_bytes"] += len(chunk) + if result["output_bytes"] > LIMITS["output_bytes"]: + raise SafetyError("combined_output_limit") + channel = key.data + write_all(logs[channel], chunk) + if channel != "stdout": + continue + frames.extend(chunk) + while b"\n" in frames: + line, _, rest = frames.partition(b"\n") + frames = bytearray(rest) + if len(line) > LIMITS["frame_bytes"]: + raise SafetyError("protocol_frame_limit") + frame_deadline = min(start + LIMITS["wall_seconds"], time.monotonic() + 0.1) + frame = json.loads(line) + if type(frame) is not dict: + raise SafetyError("protocol_object_required") + if frame.get("type") == "mutation": + if not ready or protocol_result is not None: + raise SafetyError("mutation_outside_ready_execution") + try: + grant = policy.decide(frame) + mutation_target(grant, frame) + except (SafetyError, OSError, ValueError) as error: + write_all(logs["events"], encoded({"request": frame, "decision": {"type": "deny", "reason": type(error).__name__ + ":" + str(error)[:256]}}) + b"\n") + os.fsync(logs["events"]) + raise + write_all(logs["events"], encoded({"request": frame, "decision": grant}) + b"\n") + os.fsync(logs["events"]) + announce(Path(grant["path"]), grant["purpose"]) + reply = encoded(grant) + b"\n" + input_count += len(reply) + if input_count > LIMITS["input_bytes"]: + raise SafetyError("input_limit") + write_pipe(child.stdin.fileno(), reply, frame_deadline) + elif frame.get("type") == "result" and protocol_result is None: + if not ready: + raise SafetyError("result_before_ready") + protocol_result = result_contract(frame,manifest,child.pid,os.getuid()) + verify_artifacts(protocol_result,child.pid) + reply = encoded(TERMINAL_ACK) + b"\n" + input_count += len(reply) + if input_count > LIMITS["input_bytes"]: + raise SafetyError("input_limit") + write_pipe(child.stdin.fileno(),reply,frame_deadline) + result["terminal_ACK_sent"] = True + elif frame.get("type") == "ready" and not ready: + validate_ready(frame, child.pid) + ready = True + result["limit_readbacks"] = frame["limits"] + else: + raise SafetyError("unexpected_protocol_frame") + if len(frames) > LIMITS["frame_bytes"]: + raise SafetyError("protocol_frame_limit") + if frames: + raise SafetyError("truncated_protocol_frame") + remaining = max(0.001, start + LIMITS["wall_seconds"] - time.monotonic()) + result["exit_code"] = child.wait(timeout=remaining) + result["reaped"] = True + expected_exit = 73 + if result.get("terminal_ACK_sent") is not True or result["exit_code"] != expected_exit or type(protocol_result) is not dict or protocol_result.get("passed") is not True or protocol_result.get("proof_scope") != PROOF_SCOPE: + raise SafetyError("case_nonterminal_or_failed") + if not group_gone(expected.pgid): + result["descendant_containment"] = "unproven" + raise SafetyError("post_exit_group_containment_unproven") + result["passed"] = True + result["child_result"] = protocol_result + except (SafetyError, OSError, ValueError, subprocess.SubprocessError, psutil.Error) as error: + result["blocked"] = type(error).__name__ + ":" + str(error)[:256] + finally: + if not result["sensors"]: + result["sensors"] = [asdict(row) for row in records] + if child is None: + result["reaped"] = True + if held_lock is not None: + try: + os.close(held_lock) + except OSError as error: + result["blocked"] = "lock_close_failed:" + str(error)[:256] + result["passed"] = False + if child is not None and (not result["reaped"] or result["descendant_containment"] == "unproven"): + try: + terminate_owned(child, expected, members, psutil, result) + except (SafetyError, OSError, ValueError, subprocess.SubprocessError, psutil.Error) as error: + result["blocked"] = "identity_or_reap_unproven:" + str(error)[:256] + result["passed"] = False + result["wall_seconds"] = time.monotonic() - start + try: + selector.close() + except OSError as error: + result["blocked"] = "selector_close_failed:" + str(error)[:256] + result["passed"] = False + if child is not None: + for stream in (child.stdin, child.stdout, child.stderr): + if stream is not None: + try: + stream.close() + except OSError as error: + result["blocked"] = "pipe_close_failed:" + str(error)[:256] + result["passed"] = False + for fd in logs.values(): + try: + os.fsync(fd) + except OSError as error: + result["blocked"] = "evidence_sync_failed:" + str(error)[:256] + result["passed"] = False + finally: + try: + os.close(fd) + except OSError as error: + result["blocked"] = "evidence_close_failed:" + str(error)[:256] + result["passed"] = False + return result + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--manifest", required=True) + parser.add_argument("--execute-root-authorized", action="store_true") + args = parser.parse_args() + if not args.execute_root_authorized or sys.platform != "darwin" or not sys.flags.isolated or not sys.flags.no_site or not sys.flags.dont_write_bytecode: + raise SafetyError("root_isolated_native_execution_required") + sys.dont_write_bytecode = True + os.umask(0o077) + manifest_path = canonical_path(args.manifest, RUN_ROOT) + target, version = evidence_paths(manifest_path) + owned_path(RUN_ROOT, directory=True) + owned_path(RUN_ROOT / "home", directory=True) + manifest_bytes = read_bounded(manifest_path, 65536) + manifest = validate_manifest(json.loads(manifest_bytes)) + if digest(read_bounded(Path(__file__).resolve(), 262144, private=False)) != manifest["supervisor_sha256"]: + raise SafetyError("supervisor_hash_changed") + results: list[dict] = [] + setup_failure = None + # Reserve before runtime admission so a retained refusal cannot be replayed. + fd = new_log(target, "Retained Stage B results; native host/model/hard-cap acceptance remains unproven") + try: + try: + psutil = pinned_runtime() + pressure = PressureReader() + for case in manifest["cases"]: + result = execute_case(manifest, case, psutil, pressure, attempt_version=version) + results.append(result) + if not result["passed"] or not result["reaped"]: + break + except (SafetyError, OSError, ValueError, ImportError) as error: + setup_failure = type(error).__name__ + ":" + str(error)[:256] + data = encoded({"version": 1, "manifest_version": version, "manifest_sha256": digest(manifest_bytes), + "proof_scope": PROOF_SCOPE, "cases": results, "setup_failure": setup_failure, + "runtime_closure_complete":False,"stdlib_bytes_hashed":False,"OS_sharedcache_bytes_hashed":False,"native_security_sandbox":False,"complete": setup_failure is None and len(results) == len(manifest["cases"]) and all(item["passed"] for item in results)}) + b"\n" + if len(data) > LIMITS["output_bytes"]: + raise SafetyError("results_byte_limit") + write_all(fd, data) + finally: + try: + os.fsync(fd) + finally: + os.close(fd) + return 0 if setup_failure is None and len(results) == len(manifest["cases"]) and all(item["passed"] for item in results) else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/native/mac-native-package-bootstrap-v3.py b/tests/native/mac-native-package-bootstrap-v3.py new file mode 100644 index 00000000..e22eaa0a --- /dev/null +++ b/tests/native/mac-native-package-bootstrap-v3.py @@ -0,0 +1,415 @@ +"""ROOT-only Stage B worker; native entry point requires isolated Mac execution. + +Verified source is unmodified. Audit telemetry is not a native security sandbox. +""" +from __future__ import annotations + +import hashlib +import importlib +import json +import os +import resource +import select +import signal +import socket +import stat +import sys +import threading +import time +import types +from dataclasses import asdict, is_dataclass +from pathlib import Path + +HARNESS_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/worktrees/mac-native-acceptance-v1") +SOURCE_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/worktrees/whole-process-memory-297-v1") +RUN_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/native-mac-v1/run-0002") +LIMITS = {"wall_seconds":5,"termination_seconds":1,"cpu_soft":2,"cpu_hard":3, + "rss_abort_bytes":67108864,"core_bytes":0,"nofile":64,"fsize_bytes":1048576, + "input_bytes":65536,"output_bytes":262144,"events":32,"frame_bytes":2048, + "ack_ms":100,"threads":1} +PROOF_SCOPE = "instrumented_unmodified_full_package_source_af_unix_ping" +NEURAL_PREFIXES = frozenset({"torch","transformers","sentence_transformers","model2vec","mlx","mlx_lm","onnxruntime","tensorflow","jax","flax","safetensors","huggingface_hub","llama_cpp","openai","anthropic"}) + +class BootstrapError(ValueError): + pass + + +def encoded(value: object) -> bytes: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8") + + +class Protocol: + def __init__(self) -> None: + self.buffer = bytearray() + self.read_bytes = 0 + self.written_bytes = 0 + + def emit(self, value: object) -> None: + data = encoded(value) + b"\n" + if len(data) > LIMITS["frame_bytes"]: + raise BootstrapError("output_frame_limit") + self.written_bytes += len(data) + if self.written_bytes > LIMITS["output_bytes"]: + raise BootstrapError("output_limit") + while data: + count = os.write(1, data) + if count <= 0: + raise BootstrapError("output_no_progress") + data = data[count:] + + def receive(self, maximum: int, seconds: float) -> dict: + deadline = time.monotonic() + seconds + while b"\n" not in self.buffer: + remaining = deadline - time.monotonic() + if remaining <= 0 or not select.select([0], [], [], remaining)[0]: + raise BootstrapError("input_deadline") + chunk = os.read(0, min(4096, maximum + 1)) + if not chunk: + raise BootstrapError("input_eof") + self.read_bytes += len(chunk) + self.buffer.extend(chunk) + if self.read_bytes > LIMITS["input_bytes"] or len(self.buffer) > maximum: + raise BootstrapError("input_limit") + line, _, remaining = self.buffer.partition(b"\n") + self.buffer = bytearray(remaining) + if len(line) > maximum: + raise BootstrapError("input_frame_limit") + value = json.loads(line) + if type(value) is not dict: + raise BootstrapError("input_shape") + return value + + +def arm_limits() -> dict[str, list[int]]: + if sys.platform != "darwin" or not sys.flags.isolated or not sys.flags.no_site or not sys.flags.dont_write_bytecode: + raise BootstrapError("isolated_mac_child_required") + os.umask(0o077) + sys.dont_write_bytecode = True + expected = {"cpu": (2, 3), "core": (0, 0), "nofile": (64, 64), "fsize": (1048576, 1048576)} + kinds = {"cpu": resource.RLIMIT_CPU, "core": resource.RLIMIT_CORE, + "nofile": resource.RLIMIT_NOFILE, "fsize": resource.RLIMIT_FSIZE} + for name, limits in expected.items(): + current = resource.getrlimit(kinds[name]) + if any(old != resource.RLIM_INFINITY and old < new for old, new in zip(current, limits)): + raise BootstrapError("existing_limit_below_reviewed_limit") + resource.setrlimit(kinds[name], limits) + if resource.getrlimit(kinds[name]) != limits: + raise BootstrapError("limit_readback_failed") + signal.signal(signal.SIGALRM, signal.SIG_DFL) + signal.setitimer(signal.ITIMER_REAL, 5) + return {name: list(limits) for name, limits in expected.items()} + + +def compile_module(name: str, path: Path, checksum: str, *, package: bool = False) -> types.ModuleType: + if path.is_symlink() or path.resolve() != path: + raise BootstrapError("source_path_link") + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + try: + before = os.fstat(fd) + if not stat.S_ISREG(before.st_mode) or before.st_size > 262144: + raise BootstrapError("source_read_type_or_limit") + stream = os.fdopen(fd, "rb") + fd = None + with stream: + data = stream.read(262145) + after = os.fstat(stream.fileno()) + finally: + if fd is not None: + os.close(fd) + if (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns, before.st_ctime_ns) != (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns, after.st_ctime_ns): + raise BootstrapError("source_changed_during_read") + if len(data) > 262144 or hashlib.sha256(data).hexdigest() != checksum: + raise BootstrapError("source_hash_changed") + module = types.ModuleType(name) + module.__file__ = str(path) + module.__package__ = name if package else name.rpartition(".")[0] + if package: + module.__path__ = [str(path.parent)] + sys.modules[name] = module + exec(compile(data, str(path), "exec", dont_inherit=True), module.__dict__) + return module + + +def require(condition: bool, reason: str) -> None: + if not condition: + raise BootstrapError(reason) + + +class AuditGuard: + def __init__(self,protocol: Protocol,manifest: dict[str,object],policy: types.ModuleType,runtime: dict[str,object]) -> None: + self.protocol,self.manifest,self.policy,self.runtime = protocol,manifest,policy,runtime + self.sequence = 0 + self.activated = False + self.exchange_lock = threading.Lock() + self.open_intents: list[tuple[Path,int]] = [] + self.known_fds: dict[int,tuple[int,int]] = {} + self.approved: dict[Path,int] = {} + self.stdlib = Path(runtime["interpreter"]["trusted_stdlib"]["canonical_root"]) + self.excluded = set(runtime["interpreter"]["trusted_stdlib"]["excluded_subtrees"]) + self.read_paths = {SOURCE_ROOT/name for name in policy.SOURCE_HASHES} | {SOURCE_ROOT/name for name in policy.DATA_HASHES} + self.read_paths |= set(policy.runtime_files(runtime)) + self.read_paths |= {policy.RUNTIME_PACKET,policy.INTERPRETER_PACKET,policy.LINKAGE_PACKET,HARNESS_ROOT/"tests/native/mac-native-package-acceptance-v3.py"} + self.endpoint = RUN_ROOT/"home/.truememory/model.sock" + self.listener: socket.socket | None = None + self.client: socket.socket | None = None + self.accepted: socket.socket | None = None + self.accepted_identity: tuple[int,int] | None = None + self.accepted_checked = False + self.neural_imports: list[str] = [] + self.socket_count = 0 + self.terminal = False + self.ctypes_namespace_loads = 0 + + def path(self,value: object) -> Path: + if type(value) is bytes: + value = value.decode("utf-8") + if type(value) is not str or not value or "\0" in value or not Path(value).is_absolute() or ".." in Path(value).parts: + raise BootstrapError("path_argument") + path = Path(value) + if str(path) != value or path.resolve() != path or any(part.is_symlink() for part in (path,*path.parents)): + raise BootstrapError("path_link_or_noncanonical") + return path + + def readable(self,path: Path) -> bool: + if path in self.read_paths or path in self.approved: + return True + if path.is_relative_to(self.stdlib): + relative = path.relative_to(self.stdlib) + return path.suffix == ".py" and not any(part in self.excluded for part in relative.parts) + return False + + def grant(self,event: str,path: Path,*,flags: int) -> None: + if self.terminal or not self.exchange_lock.acquire(timeout=0.1): + raise BootstrapError("B_exchange_lock_or_terminal") + try: + self.sequence += 1 + require(self.sequence<=LIMITS["events"],"mutation_event_limit") + self.protocol.emit({"type":"mutation","version":1,"seq":self.sequence,"event":event,"path":str(path),"source":None,"flags":flags}) + answer = self.protocol.receive(LIMITS["frame_bytes"],0.1) + require(set(answer)=={"type","version","seq","path","purpose"} and answer["type"]=="grant" and type(answer["version"]) is int and answer["version"]==1 and type(answer["seq"]) is int and answer["seq"]==self.sequence and answer["path"]==str(path) and type(answer["purpose"]) is str and bool(answer["purpose"]),"grant_mismatch") + if event == "open": + self.approved[path] = flags & os.O_ACCMODE + finally: + self.exchange_lock.release() + + def descriptor(self,fd: int,mode: object) -> None: + import fcntl + require(type(fd) is int and fd not in (0,1,2) and type(mode) is str and mode in ("r","rb","w","wb"),"unsupported_descriptor_wrapper") + info = os.fstat(fd) + require(stat.S_ISREG(info.st_mode) and bool(self.open_intents),"descriptor_not_preapproved_regular") + path,allowed = self.open_intents.pop() + current = path.lstat() + require(not path.is_symlink() and stat.S_ISREG(current.st_mode) and (current.st_dev,current.st_ino)==(info.st_dev,info.st_ino),"descriptor_fresh_identity") + access = fcntl.fcntl(fd,fcntl.F_GETFL)&os.O_ACCMODE + require(allowed==access and ((mode.startswith("r") and access==os.O_RDONLY) or (mode.startswith("w") and access==os.O_WRONLY)),"descriptor_access") + if path.is_relative_to(RUN_ROOT): + require(info.st_uid==os.getuid() and stat.S_IMODE(info.st_mode)==0o600,"owned_descriptor_mode") + require(len(self.known_fds|{fd:(info.st_dev,info.st_ino)})<=32,"descriptor_inventory_limit") + self.known_fds[fd] = (info.st_dev,info.st_ino) + + def socket_new(self,args: tuple[object,...]) -> None: + obj,family,kind,protocol = args + require(type(family) is int and family==socket.AF_UNIX and type(kind) is int and kind==socket.SOCK_STREAM and type(protocol) is int and protocol==0 and self.socket_count<3,"unplanned_socket_type_or_count") + frame = sys._getframe(2) + initializer = None + caller = None + for _ in range(6): + filename = Path(frame.f_code.co_filename) + if filename==self.stdlib/"socket.py" and frame.f_code.co_name=="__init__": + initializer = frame + if initializer is not None and frame is not initializer: + caller = frame + break + if frame.f_back is None: + break + frame = frame.f_back + require(initializer is not None and caller is not None,"socket_source_frame") + fileno = initializer.f_locals.get("fileno") + filename = Path(caller.f_code.co_filename) + if fileno is None and filename==SOURCE_ROOT/"truememory/model_server.py" and caller.f_code.co_name=="run" and self.listener is None: + self.listener = obj + elif fileno is None and filename==SOURCE_ROOT/"truememory/model_client.py" and caller.f_code.co_name=="_connect" and self.client is None: + self.client = obj + elif type(fileno) is int and filename==self.stdlib/"socket.py" and caller.f_code.co_name=="accept" and caller.f_locals.get("self") is self.listener and caller.f_locals.get("fd")==fileno and self.accepted is None: + require(self.listener is not None and self.listener.getsockname()==str(self.endpoint),"accepted_listener_endpoint") + info = os.fstat(fileno) + require(stat.S_ISSOCK(info.st_mode) and info.st_uid==os.getuid(),"accepted_descriptor_owner_type") + self.accepted,self.accepted_identity = obj,(info.st_dev,info.st_ino) + self.accepted_checked = True + else: + raise BootstrapError("foreign_socket_or_fileno") + self.socket_count += 1 + self.grant("socket.__new__",self.endpoint,flags=0) + + def ctypes_process_namespace(self,args: tuple[object,...]) -> None: + require(type(args) is tuple and len(args)==1 and args[0] is None and type(self.ctypes_namespace_loads) is int and self.ctypes_namespace_loads==0,"ctypes_process_namespace_args_or_replay") + module = sys.modules.get("ctypes") + require(type(module) is types.ModuleType,"ctypes_actual_module_missing") + namespace = module.__dict__ + frame = sys._getframe(2) + initializer = frame.f_back + filename = str(self.stdlib/"ctypes/__init__.py") + require(frame.f_code.co_filename==filename and frame.f_code.co_name=="__init__" and frame.f_globals is namespace,"ctypes_constructor_frame") + require(initializer is not None and initializer.f_code.co_filename==filename and initializer.f_code.co_name=="" and initializer.f_globals is namespace,"ctypes_module_initializer_frame") + constructor = namespace.get("CDLL") + dll_type = namespace.get("PyDLL") + require(type(constructor) is type and type(dll_type) is type and "pythonapi" not in namespace,"ctypes_initializer_namespace") + require(type(constructor.__init__) is types.FunctionType and frame.f_code is constructor.__init__.__code__,"ctypes_actual_constructor_code") + instance = frame.f_locals.get("self") + require(type(instance) is dll_type and getattr(instance,"_name",False) is None,"ctypes_actual_PyDLL_None") + self.ctypes_namespace_loads += 1 + + def __call__(self,event: str,args: tuple[object,...]) -> None: + if event=="truememory.native.audit_probe": + require(args==("fixed-stage-B-v1",),"audit_probe_mismatch") + self.activated = True + return + if event=="open": + path,mode,flags = args + require(type(flags) is int,"open_flags_type") + if type(path) is int: + self.descriptor(path,mode) + return + target = self.path(path) + if flags&(os.O_CREAT|os.O_WRONLY|os.O_RDWR|os.O_TRUNC|os.O_APPEND): + require(target.is_relative_to(RUN_ROOT),"write_escape") + self.grant("open",target,flags=flags) + else: + require(self.readable(target),"unplanned_read") + self.open_intents[:] = [(target,flags&os.O_ACCMODE)] if mode is None else [] + elif event=="os.mkdir": + path,mode,dir_fd = args + require(dir_fd in (-1,None) and type(mode) is int,"mkdir_descriptor") + self.grant(event,self.path(path),flags=mode) + elif event=="os.chmod": + path,mode,dir_fd = args + require(dir_fd in (-1,None) and type(mode) is int,"chmod_descriptor") + self.grant(event,self.path(path),flags=mode) + elif event=="socket.__new__": + self.socket_new(args) + elif event in ("socket.bind","socket.connect"): + obj,address = args + require(address==str(self.endpoint),"foreign_socket_address") + require(obj is (self.listener if event=="socket.bind" else self.client),"foreign_socket_actor") + self.grant(event,self.endpoint,flags=0) + elif event.startswith("socket."): + raise BootstrapError("foreign_socket_operation:"+event) + elif event in ("os.listdir","os.scandir"): + target = self.path(args[0]) + require(target==self.stdlib or (target.is_relative_to(self.stdlib) and not any(part in self.excluded for part in target.relative_to(self.stdlib).parts)) or target in {path.parent for path in self.read_paths},"unplanned_directory_read") + elif event=="ctypes.dlopen": + self.ctypes_process_namespace(args) + elif event in {"subprocess.Popen","os.fork","os.forkpty","os.posix_spawn","os.spawn","os.system","os.exec","sqlite3.connect","sqlite3.load_extension","sqlite3.enable_load_extension","os.remove","os.rmdir","os.rename","os.link","os.symlink","os.truncate","os.chown","os.chflags","resource.setrlimit","sys.addaudithook","sys.setprofile","sys.settrace"}: + raise BootstrapError("forbidden_native_operation:"+event) + elif event in ("os.kill","os.killpg"): + require(event=="os.kill" and args==(os.getpid(),0),"foreign_signal") + elif event=="import": + name = args[0] + require(type(name) is str,"import_name_type") + if name.split(".")[0] in NEURAL_PREFIXES: + self.neural_imports.append(name) + raise BootstrapError("neural_import_forbidden") + + def terminal_result(self,result: dict[str,object]) -> None: + require(self.exchange_lock.acquire(timeout=0.1),"terminal_exchange_lock") + try: + self.terminal = True + self.protocol.emit(result) + answer = self.protocol.receive(LIMITS["frame_bytes"],0.1) + self.policy.terminal_ACK_contract(answer) + os._exit(73) + finally: + self.exchange_lock.release() + + +def cache_observations(server: object,vector: types.ModuleType,reranker: types.ModuleType) -> dict[str,object]: + return {"vector_model":vector._model,"reranker_model":reranker._model, + "server_embed_state":server._embed_state,"server_reranker":server._reranker,"server_fast_encoder":server._fast_encoder} + + +def execute_case(manifest: dict[str,object],policy: types.ModuleType,runtime: dict[str,object],guard: AuditGuard) -> dict[str,object]: + finder = policy.PinnedPackageFinder(runtime,full_package=True) + sys.meta_path.insert(0,finder) + sys.path[:] = [str(guard.stdlib),str(guard.stdlib/"lib-dynload")] + for name in tuple(sys.modules): + require(name.split(".")[0] not in NEURAL_PREFIXES,"neural_dependency_preloaded") + package = importlib.import_module("truememory") + require(package.__file__==str(SOURCE_ROOT/"truememory/__init__.py") and package.__version__=="0.7.6.2" and hasattr(package,"Memory") and hasattr(package,"TrueMemoryEngine"),"actual_initializer_missing") + model_server = importlib.import_module("truememory.model_server") + model_client = importlib.import_module("truememory.model_client") + vector = importlib.import_module("truememory.vector_search") + reranker = importlib.import_module("truememory.reranker") + salience = importlib.import_module("truememory.salience") + memory = importlib.import_module("truememory.memory_budget") + for name,checksum in manifest["sources"].items(): + module_name = name[:-12].replace("/",".") if name.endswith("/__init__.py") else name[:-3].replace("/",".") + require(module_name in sys.modules and sys.modules[module_name].__file__==str(SOURCE_ROOT/name),"source_closure_not_loaded") + raw = policy.read_bounded(SOURCE_ROOT/"truememory/data/l3_weights.json",845,private=False) + require(len(raw)==845 and policy.digest(raw)==manifest["data"]["truememory/data/l3_weights.json"],"source_data_changed") + data = json.loads(raw) + require(salience._L3_WEIGHTS==tuple(data["weights"]) and len(salience._L3_WEIGHTS)==13 and salience._L3_BIAS==float(data["bias"]),"actual_salience_data_missing") + require(os.environ.get("TRUEMEMORY_NO_MODEL_SERVER")=="1" and model_server._USE_UNIX is True and model_client._USE_UNIX is True and model_server.SOCK_PATH==guard.endpoint and model_client.SOCK_PATH==guard.endpoint,"owned_AF_UNIX_paths") + server = model_server.ModelServer(automatic_rss_budget=True) + before = cache_observations(server,vector,reranker) + require(all(value is None for value in before.values()),"pre_ping_neural_cache") + runner = threading.Thread(target=server.run,name="native-package-server-v1",daemon=True) + runner.start() + ready_deadline = time.monotonic()+0.5 + while True: + require(runner.is_alive(),"server_thread_stopped") + listener = guard.listener + if listener is not None and listener.fileno()>=0 and listener.getsockopt(socket.SOL_SOCKET,socket.SO_ACCEPTCONN)==1 and model_server.PID_PATH.is_file(): + break + require(time.monotonic() int: + readback = arm_limits() + protocol = Protocol() + request = protocol.receive(LIMITS["input_bytes"],1) + require(set(request)=={"version","manifest"} and type(request["version"]) is int and request["version"]==1,"request_shape") + manifest = request["manifest"] + require(type(manifest) is dict and manifest.get("home")==str(RUN_ROOT/"home") and os.environ.get("HOME")==manifest["home"],"request_HOME") + policy = compile_module("mac_native_package_policy_v3",HARNESS_ROOT/"tests/native/mac-native-package-acceptance-v3.py",manifest["supervisor_sha256"]) + policy.validate_manifest(manifest) + runtime = policy.load_runtime_contract() + guard = AuditGuard(protocol,manifest,policy,runtime) + sys.addaudithook(guard) + sys.audit("truememory.native.audit_probe","fixed-stage-B-v1") + require(guard.activated,"audit_hook_missing") + protocol.emit({"type":"ready","version":1,"pid":os.getpid(),"limits":readback,"audit_installed":True}) + result = execute_case(manifest,policy,runtime,guard) + guard.terminal_result(result) + raise BootstrapError("terminal_exit_missing") + + +if __name__=="__main__": + raise SystemExit(main()) diff --git a/tests/native/mac-native-package-bootstrap-v4.py b/tests/native/mac-native-package-bootstrap-v4.py new file mode 100644 index 00000000..08a4287b --- /dev/null +++ b/tests/native/mac-native-package-bootstrap-v4.py @@ -0,0 +1,431 @@ +"""ROOT-only Stage B worker; native entry point requires isolated Mac execution. + +Verified source is unmodified. Audit telemetry is not a native security sandbox. +""" +from __future__ import annotations + +import hashlib +import importlib +import json +import os +import resource +import select +import signal +import socket +import stat +import sys +import threading +import time +import types +from dataclasses import asdict, is_dataclass +from pathlib import Path + +HARNESS_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/worktrees/mac-native-acceptance-v1") +SOURCE_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/worktrees/whole-process-memory-297-v1") +RUN_ROOT = Path("/Users/j/dev/truememory-maintenance-v1/.work/native-mac-v1/run-0002") +LIMITS = {"wall_seconds":5,"termination_seconds":1,"cpu_soft":2,"cpu_hard":3, + "rss_abort_bytes":67108864,"core_bytes":0,"nofile":64,"fsize_bytes":1048576, + "input_bytes":65536,"output_bytes":262144,"events":32,"frame_bytes":2048, + "ack_ms":100,"threads":1} +PROOF_SCOPE = "instrumented_unmodified_full_package_source_af_unix_ping" +NEURAL_PREFIXES = frozenset({"torch","transformers","sentence_transformers","model2vec","mlx","mlx_lm","onnxruntime","tensorflow","jax","flax","safetensors","huggingface_hub","llama_cpp","openai","anthropic"}) + +class BootstrapError(ValueError): + pass + + +def encoded(value: object) -> bytes: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8") + + +class Protocol: + def __init__(self) -> None: + self.buffer = bytearray() + self.read_bytes = 0 + self.written_bytes = 0 + + def emit(self, value: object) -> None: + data = encoded(value) + b"\n" + if len(data) > LIMITS["frame_bytes"]: + raise BootstrapError("output_frame_limit") + self.written_bytes += len(data) + if self.written_bytes > LIMITS["output_bytes"]: + raise BootstrapError("output_limit") + while data: + count = os.write(1, data) + if count <= 0: + raise BootstrapError("output_no_progress") + data = data[count:] + + def receive(self, maximum: int, seconds: float) -> dict: + deadline = time.monotonic() + seconds + while b"\n" not in self.buffer: + remaining = deadline - time.monotonic() + if remaining <= 0 or not select.select([0], [], [], remaining)[0]: + raise BootstrapError("input_deadline") + chunk = os.read(0, min(4096, maximum + 1)) + if not chunk: + raise BootstrapError("input_eof") + self.read_bytes += len(chunk) + self.buffer.extend(chunk) + if self.read_bytes > LIMITS["input_bytes"] or len(self.buffer) > maximum: + raise BootstrapError("input_limit") + line, _, remaining = self.buffer.partition(b"\n") + self.buffer = bytearray(remaining) + if len(line) > maximum: + raise BootstrapError("input_frame_limit") + value = json.loads(line) + if type(value) is not dict: + raise BootstrapError("input_shape") + return value + + +def arm_limits() -> dict[str, list[int]]: + if sys.platform != "darwin" or not sys.flags.isolated or not sys.flags.no_site or not sys.flags.dont_write_bytecode: + raise BootstrapError("isolated_mac_child_required") + os.umask(0o077) + sys.dont_write_bytecode = True + expected = {"cpu": (2, 3), "core": (0, 0), "nofile": (64, 64), "fsize": (1048576, 1048576)} + kinds = {"cpu": resource.RLIMIT_CPU, "core": resource.RLIMIT_CORE, + "nofile": resource.RLIMIT_NOFILE, "fsize": resource.RLIMIT_FSIZE} + for name, limits in expected.items(): + current = resource.getrlimit(kinds[name]) + if any(old != resource.RLIM_INFINITY and old < new for old, new in zip(current, limits)): + raise BootstrapError("existing_limit_below_reviewed_limit") + resource.setrlimit(kinds[name], limits) + if resource.getrlimit(kinds[name]) != limits: + raise BootstrapError("limit_readback_failed") + signal.signal(signal.SIGALRM, signal.SIG_DFL) + signal.setitimer(signal.ITIMER_REAL, 5) + return {name: list(limits) for name, limits in expected.items()} + + +def compile_module(name: str, path: Path, checksum: str, *, package: bool = False) -> types.ModuleType: + if path.is_symlink() or path.resolve() != path: + raise BootstrapError("source_path_link") + fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK) + try: + before = os.fstat(fd) + if not stat.S_ISREG(before.st_mode) or before.st_size > 262144: + raise BootstrapError("source_read_type_or_limit") + stream = os.fdopen(fd, "rb") + fd = None + with stream: + data = stream.read(262145) + after = os.fstat(stream.fileno()) + finally: + if fd is not None: + os.close(fd) + if (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns, before.st_ctime_ns) != (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns, after.st_ctime_ns): + raise BootstrapError("source_changed_during_read") + if len(data) > 262144 or hashlib.sha256(data).hexdigest() != checksum: + raise BootstrapError("source_hash_changed") + module = types.ModuleType(name) + module.__file__ = str(path) + module.__package__ = name if package else name.rpartition(".")[0] + if package: + module.__path__ = [str(path.parent)] + sys.modules[name] = module + exec(compile(data, str(path), "exec", dont_inherit=True), module.__dict__) + return module + + +def require(condition: bool, reason: str) -> None: + if not condition: + raise BootstrapError(reason) + + +class AuditGuard: + def __init__(self,protocol: Protocol,manifest: dict[str,object],policy: types.ModuleType,runtime: dict[str,object]) -> None: + self.protocol,self.manifest,self.policy,self.runtime = protocol,manifest,policy,runtime + self.sequence = 0 + self.activated = False + self.exchange_lock = threading.Lock() + self.open_intents: list[tuple[Path,int]] = [] + self.known_fds: dict[int,tuple[int,int]] = {} + self.approved: dict[Path,int] = {} + self.stdlib = Path(runtime["interpreter"]["trusted_stdlib"]["canonical_root"]) + self.excluded = set(runtime["interpreter"]["trusted_stdlib"]["excluded_subtrees"]) + self.read_paths = {SOURCE_ROOT/name for name in policy.SOURCE_HASHES} | {SOURCE_ROOT/name for name in policy.DATA_HASHES} + self.read_paths |= set(policy.runtime_files(runtime)) + self.read_paths |= {policy.RUNTIME_PACKET,policy.INTERPRETER_PACKET,policy.LINKAGE_PACKET,HARNESS_ROOT/"tests/native/mac-native-package-acceptance-v4.py"} + self.endpoint = RUN_ROOT/"home/.truememory/model.sock" + self.listener: socket.socket | None = None + self.client: socket.socket | None = None + self.accepted: socket.socket | None = None + self.accepted_identity: tuple[int,int] | None = None + self.accepted_checked = False + self.neural_imports: list[str] = [] + self.socket_count = 0 + self.terminal = False + self.ctypes_namespace_loads = 0 + + def path(self,value: object) -> Path: + if type(value) is bytes: + value = value.decode("utf-8") + if type(value) is not str or not value or "\0" in value or not Path(value).is_absolute() or ".." in Path(value).parts: + raise BootstrapError("path_argument") + path = Path(value) + if str(path) != value or path.resolve() != path or any(part.is_symlink() for part in (path,*path.parents)): + raise BootstrapError("path_link_or_noncanonical") + return path + + def readable(self,path: Path) -> bool: + if path in self.read_paths or path in self.approved: + return True + if path.is_relative_to(self.stdlib): + relative = path.relative_to(self.stdlib) + return path.suffix == ".py" and not any(part in self.excluded for part in relative.parts) + return False + + def grant(self,event: str,path: Path,*,flags: int) -> None: + if self.terminal or not self.exchange_lock.acquire(timeout=0.1): + raise BootstrapError("B_exchange_lock_or_terminal") + try: + self.sequence += 1 + require(self.sequence<=LIMITS["events"],"mutation_event_limit") + self.protocol.emit({"type":"mutation","version":1,"seq":self.sequence,"event":event,"path":str(path),"source":None,"flags":flags}) + answer = self.protocol.receive(LIMITS["frame_bytes"],0.1) + require(set(answer)=={"type","version","seq","path","purpose"} and answer["type"]=="grant" and type(answer["version"]) is int and answer["version"]==1 and type(answer["seq"]) is int and answer["seq"]==self.sequence and answer["path"]==str(path) and type(answer["purpose"]) is str and bool(answer["purpose"]),"grant_mismatch") + if event == "open": + self.approved[path] = flags & os.O_ACCMODE + finally: + self.exchange_lock.release() + + def descriptor(self,fd: int,mode: object) -> None: + import fcntl + require(type(fd) is int and fd not in (0,1,2) and type(mode) is str and mode in ("r","rb","w","wb"),"unsupported_descriptor_wrapper") + info = os.fstat(fd) + require(stat.S_ISREG(info.st_mode) and bool(self.open_intents),"descriptor_not_preapproved_regular") + path,allowed = self.open_intents.pop() + current = path.lstat() + require(not path.is_symlink() and stat.S_ISREG(current.st_mode) and (current.st_dev,current.st_ino)==(info.st_dev,info.st_ino),"descriptor_fresh_identity") + access = fcntl.fcntl(fd,fcntl.F_GETFL)&os.O_ACCMODE + require(allowed==access and ((mode.startswith("r") and access==os.O_RDONLY) or (mode.startswith("w") and access==os.O_WRONLY)),"descriptor_access") + if path.is_relative_to(RUN_ROOT): + require(info.st_uid==os.getuid() and stat.S_IMODE(info.st_mode)==0o600,"owned_descriptor_mode") + require(len(self.known_fds|{fd:(info.st_dev,info.st_ino)})<=32,"descriptor_inventory_limit") + self.known_fds[fd] = (info.st_dev,info.st_ino) + + def socket_new(self,args: tuple[object,...]) -> None: + obj,family,kind,protocol = args + require(type(family) is int and family==socket.AF_UNIX and type(kind) is int and kind==socket.SOCK_STREAM and type(protocol) is int and protocol==0 and self.socket_count<3,"unplanned_socket_type_or_count") + frame = sys._getframe(2) + initializer = None + caller = None + for _ in range(6): + filename = Path(frame.f_code.co_filename) + if filename==self.stdlib/"socket.py" and frame.f_code.co_name=="__init__": + initializer = frame + if initializer is not None and frame is not initializer: + caller = frame + break + if frame.f_back is None: + break + frame = frame.f_back + require(initializer is not None and caller is not None,"socket_source_frame") + fileno = initializer.f_locals.get("fileno") + filename = Path(caller.f_code.co_filename) + if fileno is None and filename==SOURCE_ROOT/"truememory/model_server.py" and caller.f_code.co_name=="run" and self.listener is None: + self.listener = obj + elif fileno is None and filename==SOURCE_ROOT/"truememory/model_client.py" and caller.f_code.co_name=="_connect" and self.client is None: + self.client = obj + elif type(fileno) is int and filename==self.stdlib/"socket.py" and caller.f_code.co_name=="accept" and caller.f_locals.get("self") is self.listener and caller.f_locals.get("fd")==fileno and self.accepted is None: + require(self.listener is not None and self.listener.getsockname()==str(self.endpoint),"accepted_listener_endpoint") + info = os.fstat(fileno) + require(stat.S_ISSOCK(info.st_mode) and info.st_uid==os.getuid(),"accepted_descriptor_owner_type") + self.accepted,self.accepted_identity = obj,(info.st_dev,info.st_ino) + self.accepted_checked = True + else: + raise BootstrapError("foreign_socket_or_fileno") + self.socket_count += 1 + self.grant("socket.__new__",self.endpoint,flags=0) + + def ctypes_process_namespace(self,args: tuple[object,...]) -> None: + require(type(args) is tuple and len(args)==1 and args[0] is None and type(self.ctypes_namespace_loads) is int and self.ctypes_namespace_loads==0,"ctypes_process_namespace_args_or_replay") + module = sys.modules.get("ctypes") + require(type(module) is types.ModuleType,"ctypes_actual_module_missing") + namespace = module.__dict__ + frame = sys._getframe(2) + initializer = frame.f_back + filename = str(self.stdlib/"ctypes/__init__.py") + require(frame.f_code.co_filename==filename and frame.f_code.co_name=="__init__" and frame.f_globals is namespace,"ctypes_constructor_frame") + require(initializer is not None and initializer.f_code.co_filename==filename and initializer.f_code.co_name=="" and initializer.f_globals is namespace,"ctypes_module_initializer_frame") + constructor = namespace.get("CDLL") + dll_type = namespace.get("PyDLL") + require(type(constructor) is type and type(dll_type) is type and "pythonapi" not in namespace,"ctypes_initializer_namespace") + require(type(constructor.__init__) is types.FunctionType and frame.f_code is constructor.__init__.__code__,"ctypes_actual_constructor_code") + instance = frame.f_locals.get("self") + require(type(instance) is dll_type and getattr(instance,"_name",False) is None,"ctypes_actual_PyDLL_None") + self.ctypes_namespace_loads += 1 + + def __call__(self,event: str,args: tuple[object,...]) -> None: + if event=="truememory.native.audit_probe": + require(args==("fixed-stage-B-v1",),"audit_probe_mismatch") + self.activated = True + return + if event=="open": + path,mode,flags = args + require(type(flags) is int,"open_flags_type") + if type(path) is int: + self.descriptor(path,mode) + return + target = self.path(path) + if flags&(os.O_CREAT|os.O_WRONLY|os.O_RDWR|os.O_TRUNC|os.O_APPEND): + require(target.is_relative_to(RUN_ROOT),"write_escape") + self.grant("open",target,flags=flags) + else: + require(self.readable(target),"unplanned_read") + self.open_intents[:] = [(target,flags&os.O_ACCMODE)] if mode is None else [] + elif event=="os.mkdir": + path,mode,dir_fd = args + require(dir_fd in (-1,None) and type(mode) is int,"mkdir_descriptor") + self.grant(event,self.path(path),flags=mode) + elif event=="os.chmod": + path,mode,dir_fd = args + require(dir_fd in (-1,None) and type(mode) is int,"chmod_descriptor") + self.grant(event,self.path(path),flags=mode) + elif event=="socket.__new__": + self.socket_new(args) + elif event in ("socket.bind","socket.connect"): + obj,address = args + require(address==str(self.endpoint),"foreign_socket_address") + require(obj is (self.listener if event=="socket.bind" else self.client),"foreign_socket_actor") + self.grant(event,self.endpoint,flags=0) + elif event.startswith("socket."): + raise BootstrapError("foreign_socket_operation:"+event) + elif event in ("os.listdir","os.scandir"): + target = self.path(args[0]) + require(target==self.stdlib or (target.is_relative_to(self.stdlib) and not any(part in self.excluded for part in target.relative_to(self.stdlib).parts)) or target in {path.parent for path in self.read_paths},"unplanned_directory_read") + elif event=="ctypes.dlopen": + self.ctypes_process_namespace(args) + elif event in {"subprocess.Popen","os.fork","os.forkpty","os.posix_spawn","os.spawn","os.system","os.exec","sqlite3.connect","sqlite3.load_extension","sqlite3.enable_load_extension","os.remove","os.rmdir","os.rename","os.link","os.symlink","os.truncate","os.chown","os.chflags","resource.setrlimit","sys.addaudithook","sys.setprofile","sys.settrace"}: + raise BootstrapError("forbidden_native_operation:"+event) + elif event in ("os.kill","os.killpg"): + require(event=="os.kill" and args==(os.getpid(),0),"foreign_signal") + elif event=="import": + name = args[0] + require(type(name) is str,"import_name_type") + if name.split(".")[0] in NEURAL_PREFIXES: + self.neural_imports.append(name) + raise BootstrapError("neural_import_forbidden") + + def terminal_result(self,result: dict[str,object]) -> None: + require(self.exchange_lock.acquire(timeout=0.1),"terminal_exchange_lock") + try: + self.terminal = True + self.protocol.emit(result) + answer = self.protocol.receive(LIMITS["frame_bytes"],0.1) + self.policy.terminal_ACK_contract(answer) + os._exit(73) + finally: + self.exchange_lock.release() + + +def cache_observations(server: object,vector: types.ModuleType,reranker: types.ModuleType) -> dict[str,object]: + return {"vector_model":vector._model,"reranker_model":reranker._model, + "server_embed_state":server._embed_state,"server_reranker":server._reranker,"server_fast_encoder":server._fast_encoder} + + +def listener_eligible(guard: AuditGuard,listener: socket.socket | None,pid_path: Path) -> bool: + if listener is None or listener is not guard.listener or pid_path != guard.endpoint.with_name("model_server.pid"): + return False + try: + descriptor = listener.fileno() + if type(descriptor) is not int or descriptor < 3: + return False + endpoint = listener.getsockname() + if type(endpoint) is not str or endpoint != str(guard.endpoint): + return False + info = guard.policy.owned_path(pid_path) + return stat.S_ISREG(info.st_mode) and info.st_uid == os.getuid() and not info.st_mode & 0o077 and info.st_nlink == 1 + except (OSError,ValueError): + return False + + +def execute_case(manifest: dict[str,object],policy: types.ModuleType,runtime: dict[str,object],guard: AuditGuard) -> dict[str,object]: + finder = policy.PinnedPackageFinder(runtime,full_package=True) + sys.meta_path.insert(0,finder) + sys.path[:] = [str(guard.stdlib),str(guard.stdlib/"lib-dynload")] + for name in tuple(sys.modules): + require(name.split(".")[0] not in NEURAL_PREFIXES,"neural_dependency_preloaded") + package = importlib.import_module("truememory") + require(package.__file__==str(SOURCE_ROOT/"truememory/__init__.py") and package.__version__=="0.7.6.2" and hasattr(package,"Memory") and hasattr(package,"TrueMemoryEngine"),"actual_initializer_missing") + model_server = importlib.import_module("truememory.model_server") + model_client = importlib.import_module("truememory.model_client") + vector = importlib.import_module("truememory.vector_search") + reranker = importlib.import_module("truememory.reranker") + salience = importlib.import_module("truememory.salience") + memory = importlib.import_module("truememory.memory_budget") + for name,checksum in manifest["sources"].items(): + module_name = name[:-12].replace("/",".") if name.endswith("/__init__.py") else name[:-3].replace("/",".") + require(module_name in sys.modules and sys.modules[module_name].__file__==str(SOURCE_ROOT/name),"source_closure_not_loaded") + raw = policy.read_bounded(SOURCE_ROOT/"truememory/data/l3_weights.json",845,private=False) + require(len(raw)==845 and policy.digest(raw)==manifest["data"]["truememory/data/l3_weights.json"],"source_data_changed") + data = json.loads(raw) + require(salience._L3_WEIGHTS==tuple(data["weights"]) and len(salience._L3_WEIGHTS)==13 and salience._L3_BIAS==float(data["bias"]),"actual_salience_data_missing") + require(os.environ.get("TRUEMEMORY_NO_MODEL_SERVER")=="1" and model_server._USE_UNIX is True and model_client._USE_UNIX is True and model_server.SOCK_PATH==guard.endpoint and model_client.SOCK_PATH==guard.endpoint,"owned_AF_UNIX_paths") + server = model_server.ModelServer(automatic_rss_budget=True) + before = cache_observations(server,vector,reranker) + require(all(value is None for value in before.values()),"pre_ping_neural_cache") + runner = threading.Thread(target=server.run,name="native-package-server-v1",daemon=True) + runner.start() + ready_deadline = time.monotonic()+0.5 + while True: + require(runner.is_alive(),"server_thread_stopped") + listener = guard.listener + if listener_eligible(guard,listener,model_server.PID_PATH): + break + require(time.monotonic() int: + readback = arm_limits() + protocol = Protocol() + request = protocol.receive(LIMITS["input_bytes"],1) + require(set(request)=={"version","manifest"} and type(request["version"]) is int and request["version"]==1,"request_shape") + manifest = request["manifest"] + require(type(manifest) is dict and manifest.get("home")==str(RUN_ROOT/"home") and os.environ.get("HOME")==manifest["home"],"request_HOME") + policy = compile_module("mac_native_package_policy_v4",HARNESS_ROOT/"tests/native/mac-native-package-acceptance-v4.py",manifest["supervisor_sha256"]) + policy.validate_manifest(manifest) + runtime = policy.load_runtime_contract() + guard = AuditGuard(protocol,manifest,policy,runtime) + sys.addaudithook(guard) + sys.audit("truememory.native.audit_probe","fixed-stage-B-v1") + require(guard.activated,"audit_hook_missing") + protocol.emit({"type":"ready","version":1,"pid":os.getpid(),"limits":readback,"audit_installed":True}) + result = execute_case(manifest,policy,runtime,guard) + guard.terminal_result(result) + raise BootstrapError("terminal_exit_missing") + + +if __name__=="__main__": + raise SystemExit(main()) diff --git a/tests/test-clustered-search-materialization-754.py b/tests/test-clustered-search-materialization-754.py index 6702ee13..11e6bfdc 100644 --- a/tests/test-clustered-search-materialization-754.py +++ b/tests/test-clustered-search-materialization-754.py @@ -8,6 +8,7 @@ import random import sqlite3 import struct +import sys import tempfile import types import unittest @@ -228,12 +229,20 @@ def synthetic_import(name: str, globals: dict | None = None, locals: dict | None return NUMPY if name == "truememory.vector_search": return vector + if name == "truememory.memory_budget": + return memory_budget if name == "truememory.mps_utils": return types.SimpleNamespace(encode_with_model_ownership=encode) if name == "truememory" or name.startswith("truememory."): raise AssertionError("unexpected application import") return builtins.__import__(name, globals, locals, fromlist, level) + memory_budget = types.ModuleType("synthetic_cluster_memory_budget") + sys.modules[memory_budget.__name__] = memory_budget + memory_budget.__dict__["__builtins__"] = dict(vars(builtins), __import__=synthetic_import) + budget_path = ROOT / "truememory/memory_budget.py" + exec(compile(budget_path.read_text(), str(budget_path), "exec"), memory_budget.__dict__) + module = types.ModuleType("synthetic_clustering") module.__dict__["__builtins__"] = dict(vars(builtins), __import__=synthetic_import) path = ROOT / "truememory/clustering.py" diff --git a/tests/test-linux-preexec-enforcement-297.py b/tests/test-linux-preexec-enforcement-297.py new file mode 100644 index 00000000..4fbeed17 --- /dev/null +++ b/tests/test-linux-preexec-enforcement-297.py @@ -0,0 +1,1042 @@ +"""Synthetic public-contract controls; actual manager/kernel execution is separate.""" +from __future__ import annotations + +import ast +from contextlib import ExitStack +from dataclasses import replace +import hashlib +import importlib.util +from pathlib import Path +import sys +import time +from types import SimpleNamespace +import unittest +from unittest.mock import patch + + +ROOT = Path(__file__).resolve().parents[1] +MODULE_PATH = ROOT / "truememory_memory_bootstrap.py" +SPEC = importlib.util.spec_from_file_location("synthetic_linux_preexec_author", MODULE_PATH) +assert SPEC is not None and SPEC.loader is not None +MODULE = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = MODULE +SPEC.loader.exec_module(MODULE) + + +def identity(cap: int = 67108864) -> object: + generation = "a" * 32 + return MODULE.LinuxInvocationIdentity( + 1, generation, "12345678-1234-1234-1234-123456789abc", 1000, + "app-truememory" + generation + ".slice", "b" * 32, 29, 31, 37, + "/user.slice/app.slice/app-truememory" + generation + ".slice", + "truememory-memory" + generation + ".service", "c" * 32, 31, 41, + "/user.slice/app.slice/app-truememory" + generation + ".slice/child.service", + 43, 47, 1000.5, cap, 0, "d" * 64, "e" * 64, + ) + + +def evidence(value: object) -> object: + return MODULE.LinuxScopeEvidence(value, 0x63677270, "cgroup2", True, True, True, + value.uid, value.main_pid, value.start_ticks, value.generation, + value.manifest_sha256, (value.main_pid,), value.cap_bytes, 0, "max", "max", True) + + +def receipt(value: object) -> object: + return MODULE.LinuxExitReceipt(1, value, 0, None, "success", 0, None, + (("max", 0), ("oom", 0)), (("max", 1), ("oom", 1)), False, + ((value.slice_relative_path, ()), (value.service_relative_path, ())), + True, 5, "f" * 64, (("manifest-v1.json", "e" * 64),), + "linux_cgroup_charged_domain", False, value.cap_bytes, False) + + +class InertAdapter: + def __init__(self) -> None: + self.identity = identity() + self.record = MODULE.LinuxProvisionalLaunch(self.identity, object(), object(), object(), object()) + self.calls: list[str] = [] + self.deadlines: list[float] = [] + self.terminal = False + + def start(self, policy: object, generation: str, python_executable: str, deadline: float) -> object: + self.calls.append("start") + self.deadlines.append(deadline) + return self.record + + def inspect(self, record: object, deadline: float) -> object: + self.calls.append("inspect") + self.deadlines.append(deadline) + result = evidence(self.identity) + return replace(result, members=()) if self.terminal else result + + def inspect_existing(self, policy: object, *, pid: int, birth_time: float, generation: str, + claim: dict[str, object], deadline: float) -> object: + self.calls.append("inspect_existing") + self.deadlines.append(deadline) + return evidence(self.identity) + + def barrier(self, record: object, value: object, deadline: float) -> None: + self.calls.append("barrier") + self.deadlines.append(deadline) + + def capture(self, record: object, value: object, deadline: float) -> object: + self.calls.append("capture") + self.deadlines.append(deadline) + return receipt(self.identity) + + def stop(self, record: object, value: object, deadline: float) -> None: + self.calls.append("stop") + self.deadlines.append(deadline) + + +class TestLinuxPreexecContract(unittest.TestCase): + def setUp(self) -> None: + self.adapter = InertAdapter() + self.deadline = time.monotonic() + 10.0 + self.policy = MODULE.LinuxDaemonLaunchPolicy(1, 67108864, 0) + + def launch(self) -> object: + with patch.object(MODULE, "get_linux_adapter", return_value=self.adapter): + return MODULE.launch_owned_linux(self.policy, generation="a" * 32, + python_executable="/synthetic/python", deadline=self.deadline) + + def test_literal_flag_only(self) -> None: + self.assertIs(MODULE.required_cap_selected({}), False) + self.assertIs(MODULE.required_cap_selected({"TRUEMEMORY_REQUIRE_PROCESS_MEMORY_CAP": "1"}), True) + for invalid in ("0", "", " 1", "1 ", 1, True, None): + with self.subTest(invalid=invalid), self.assertRaises((ValueError, TypeError)): + MODULE.required_cap_selected({"TRUEMEMORY_REQUIRE_PROCESS_MEMORY_CAP": invalid}) + + def test_exact_budget_arithmetic(self) -> None: + env = {"TRUEMEMORY_REQUIRE_PROCESS_MEMORY_CAP": "1"} + self.assertEqual(MODULE.plan_required_linux_launch(env, physical_total=34359738368).cap_bytes, 8589934592) + self.assertEqual(MODULE.plan_required_linux_launch(env, physical_total=1073741824).cap_bytes, 268435456) + env["TRUEMEMORY_MODEL_SERVER_MAX_RSS_MB"] = "64" + self.assertEqual(MODULE.plan_required_linux_launch(env, physical_total=None).cap_bytes, 64 * 1048576) + self.assertIs(MODULE.plan_required_linux_launch(env, physical_total=None).physical_hard_cap, False) + self.assertIs(MODULE.plan_required_linux_launch(env, physical_total=None).dynamic_registry_cap, False) + + def test_invalid_budget_does_not_fall_back(self) -> None: + for invalid in ("0", "00", "064", "+64", "64 ", "64.0", "-1", True, 64, str(2**63), "8796093022208"): + env = {"TRUEMEMORY_REQUIRE_PROCESS_MEMORY_CAP": "1", "TRUEMEMORY_MODEL_SERVER_MAX_RSS_MB": invalid} + with self.subTest(invalid=invalid), self.assertRaises((ValueError, TypeError)): + MODULE.plan_required_linux_launch(env, physical_total=34359738368) + for total in (None, 0, 1, 3, True, 1.5, 2**63): + with self.subTest(total=total), self.assertRaises((ValueError, TypeError)): + MODULE.plan_required_linux_launch({"TRUEMEMORY_REQUIRE_PROCESS_MEMORY_CAP": "1"}, physical_total=total) + + def test_constructor_subclasses_rejected(self) -> None: + class PolicySubtype(MODULE.LinuxDaemonLaunchPolicy): + pass + with self.assertRaises(TypeError): + PolicySubtype(1, 67108864, 0) + with self.assertRaises(ValueError): + replace(self.policy, cap_bytes=True) + with self.assertRaises(ValueError): + replace(self.adapter.identity, birth_time=47) + + def test_reflective_mutation_revalidated_before_adapter(self) -> None: + object.__setattr__(self.policy, "cap_bytes", True) + with patch.object(MODULE, "get_linux_adapter", return_value=self.adapter), self.assertRaises(ValueError): + MODULE.launch_owned_linux(self.policy, generation="a" * 32, + python_executable="/synthetic/python", deadline=self.deadline) + self.assertEqual(self.adapter.calls, []) + + def test_deadline_generation_executable_refuse_before_adapter(self) -> None: + for field, invalid in (("deadline", True), ("deadline", float("inf")), + ("deadline", time.monotonic() - 1), ("generation", "A" * 32), + ("python_executable", "relative/python")): + arguments = {"generation": "a" * 32, "python_executable": "/synthetic/python", "deadline": self.deadline} + arguments[field] = invalid + with self.subTest(field=field, invalid=invalid), patch.object(MODULE, "get_linux_adapter", return_value=self.adapter), self.assertRaises((ValueError, TypeError, TimeoutError)): + MODULE.launch_owned_linux(self.policy, **arguments) + self.assertEqual(self.adapter.calls, []) + + def test_start_retains_all_resources_without_release(self) -> None: + handle = self.launch() + self.assertEqual(handle.pid, 43) + self.assertIs(handle.record, self.adapter.record) + for name in ("runner", "pidfd", "scope_fd", "endpoint"): + self.assertIs(getattr(handle.record, name), getattr(self.adapter.record, name)) + self.assertEqual(self.adapter.calls, ["start", "inspect"]) + self.assertEqual(self.adapter.deadlines, [self.deadline, self.deadline]) + + def test_claim_actual_pid_and_birth_before_barrier(self) -> None: + handle = self.launch() + claim = {"generation": "a" * 32, "launcher": {"pid": 101, "born": 2000.5}, "child": {"pid": 43, "born": 1000.5}} + handle.release_after_claim(claim, deadline=self.deadline) + self.assertEqual(self.adapter.calls, ["start", "inspect", "inspect", "barrier"]) + self.assertTrue(all(value == self.deadline for value in self.adapter.deadlines)) + self.assertTrue(handle._released) + + def test_wrong_claim_cannot_release(self) -> None: + handle = self.launch() + for field, invalid in (("pid", 101), ("pid", True), ("born", 47), ("born", 47.0)): + child = {"pid": 43, "born": 1000.5} + child[field] = invalid + claim = {"generation": "a" * 32, "launcher": {"pid": 101, "born": 2000.5}, "child": child} + with self.subTest(field=field, invalid=invalid), self.assertRaises((ValueError, TypeError)): + handle.release_after_claim(claim, deadline=self.deadline) + self.assertNotIn("barrier", self.adapter.calls) + + def test_foreign_evidence_refuses_return(self) -> None: + bad = evidence(self.adapter.identity) + object.__setattr__(bad, "filesystem_magic", 0x01021994) + with patch.object(self.adapter, "inspect", return_value=bad), patch.object(MODULE, "get_linux_adapter", return_value=self.adapter), self.assertRaises(ValueError): + self.launch() + + def test_terminal_receipt_before_stop(self) -> None: + handle = self.launch() + self.adapter.terminal = True + actual = MODULE.stop_owned_exact(handle, deadline=self.deadline) + self.assertEqual(actual.identity, self.adapter.identity) + self.assertLess(self.adapter.calls.index("capture"), self.adapter.calls.index("stop")) + self.assertIs(actual.physical_hard_cap, False) + self.assertIs(actual.dynamic_registry_cap, False) + self.assertIsNone(actual.memory_peak) + + def test_live_or_undurable_receipt_cannot_stop(self) -> None: + handle = self.launch() + self.adapter.terminal = True + for field, invalid in (("populated", True), ("durable_before_stop", False), ("physical_hard_cap", True), + ("dynamic_registry_cap", True), ("kernel_cap_bytes", 1), + ("descendant_members", ((self.adapter.identity.slice_relative_path, (43,)),))): + bad = receipt(self.adapter.identity) + object.__setattr__(bad, field, invalid) + with self.subTest(field=field), patch.object(self.adapter, "capture", return_value=bad), self.assertRaises((ValueError, RuntimeError)): + MODULE.stop_owned_exact(handle, deadline=self.deadline) + self.assertNotIn("stop", self.adapter.calls) + + def test_identity_drift_before_stop(self) -> None: + handle = self.launch() + self.adapter.terminal = True + foreign = replace(self.adapter.identity, slice_invocation_id="f" * 32) + with patch.object(self.adapter, "inspect", return_value=evidence(foreign)), self.assertRaises(RuntimeError): + MODULE.stop_owned_exact(handle, deadline=self.deadline) + self.assertNotIn("capture", self.adapter.calls) + self.assertNotIn("stop", self.adapter.calls) + + def test_live_domain_cannot_capture_or_stop(self) -> None: + handle = self.launch() + with self.assertRaises(RuntimeError): + MODULE.stop_owned_exact(handle, deadline=self.deadline) + self.assertNotIn("capture", self.adapter.calls) + self.assertNotIn("stop", self.adapter.calls) + + def test_counters_and_owned_paths_are_strict(self) -> None: + original = receipt(self.adapter.identity) + for field, invalid in (("events_after", (("max", 0), ("oom", -1))), + ("events_after", (("max", 1), ("max", 2))), + ("descendant_members", (("/foreign.slice", ()),)), + ("artifact_sha256", (("../foreign", "a" * 64),))): + with self.subTest(field=field), self.assertRaises((ValueError, RuntimeError)): + replace(original, **{field: invalid}) + + def existing(self, **changes: object) -> object: + values = {"pid": 43, "birth_time": 1000.5, "generation": "a" * 32, + "claim": {"generation": "a" * 32, "launcher": {"pid": 101, "born": 2000.5}, + "child": {"pid": 43, "born": 1000.5}}, + "deadline": self.deadline, "adapter": self.adapter} + values.update(changes) + return MODULE.inspect_existing_owned_linux(self.policy, **values) + + def test_readonly_join_has_no_launch_or_stop_authority(self) -> None: + result = self.existing() + self.assertIs(type(result), MODULE.LinuxScopeEvidence) + self.assertEqual(self.adapter.calls, ["inspect_existing"]) + self.assertEqual(self.adapter.deadlines, [self.deadline]) + self.assertFalse(hasattr(result, "release_after_claim")) + + def test_readonly_join_rejects_inputs_before_observer(self) -> None: + for name, invalid in (("pid", True), ("pid", 0), ("birth_time", 47), ("birth_time", float("nan")), + ("generation", "A" * 32), ("deadline", True), ("deadline", float("inf")), + ("deadline", time.monotonic() - 1), ("claim", None)): + with self.subTest(name=name), self.assertRaises((ValueError, TypeError, TimeoutError)): + self.existing(**{name: invalid}) + self.assertEqual(self.adapter.calls, []) + + def test_readonly_join_rejects_foreign_claim_before_observer(self) -> None: + for name, invalid in (("pid", 101), ("pid", True), ("born", 47), ("born", 47.0)): + child = {"pid": 43, "born": 1000.5} + child[name] = invalid + claim = {"generation": "a" * 32, "launcher": {"pid": 101, "born": 2000.5}, "child": child} + with self.subTest(name=name), self.assertRaises((ValueError, TypeError)): + self.existing(claim=claim) + self.assertEqual(self.adapter.calls, []) + + def test_readonly_join_accepts_smaller_real_cap(self) -> None: + self.policy = MODULE.LinuxDaemonLaunchPolicy(1, 134217728, 0) + result = self.existing() + self.assertEqual(result.cap_bytes, 67108864) + self.assertIs(result.kernel_limit_preexec, True) + + def test_readonly_join_refuses_larger_real_cap(self) -> None: + self.policy = MODULE.LinuxDaemonLaunchPolicy(1, 33554432, 0) + with self.assertRaises(RuntimeError): + self.existing() + self.assertEqual(self.adapter.calls, ["inspect_existing"]) + + def test_readonly_join_revalidates_reflective_evidence(self) -> None: + for name, invalid in (("peer_pid", 101), ("members", (43, 101)), ("cap_bytes", 1), + ("swap_bytes", 1), ("leaf_memory_max", "67108864"), ("kernel_limit_preexec", False)): + value = evidence(self.adapter.identity) + object.__setattr__(value, name, invalid) + with self.subTest(name=name), patch.object(self.adapter, "inspect_existing", return_value=value), self.assertRaises((ValueError, RuntimeError)): + self.existing() + self.assertNotIn("start", self.adapter.calls) + self.assertNotIn("stop", self.adapter.calls) + + def test_readonly_join_callback_cannot_change_inputs(self) -> None: + claim = {"generation": "a" * 32, "launcher": {"pid": 101, "born": 2000.5}, "child": {"pid": 43, "born": 1000.5}} + def mutate(policy: object, **kwargs: object) -> object: + claim["child"]["pid"] = 101 + return evidence(self.adapter.identity) + with patch.object(self.adapter, "inspect_existing", side_effect=mutate), self.assertRaises((ValueError, RuntimeError)): + self.existing(claim=claim) + + def test_json_duplicate_keys_and_nonfinite_refused(self) -> None: + for raw in (b'{"pid":43,"pid":101}', b'{"birth":NaN}', b'{"birth":Infinity}'): + with self.subTest(raw=raw), self.assertRaises(ValueError): + MODULE._strict_json(raw) + + def test_same_pid_exec_preserves_ordinary_final_argv(self) -> None: + source = MODULE_PATH.read_text() + self.assertIn('[python_executable, "-I", "-S", str(bootstrap)', source) + self.assertIn('[manifest["interpreter"]["path"], "-m", "truememory.model_server"]', source) + self.assertIn('os.execve(manifest["interpreter"]["path"]', source) + self.assertNotIn('importlib.import_module("site").main()', source) + self.assertIn('"preexec_gate_passed": True', source) + self.assertIn('"application-ready"', source) + + def test_original_snapshot_restores_reserved_name_values(self) -> None: + original = {"TRUEMEMORY_REQUIRE_PROCESS_MEMORY_CAP": "1", "TRUEMEMORY_MODEL_SERVER_GENERATION": "a" * 32, + "TRUEMEMORY_CAP_CHILD": "1", "INVOCATION_ID": "original-synthetic-value", + "PYTHONHASHSEED": "43", "PYTHONMALLOC": "malloc"} + packet = MODULE._original_environment_packet(original, generation="a" * 32, manifest_sha256="e" * 64) + with patch.dict(MODULE.os.environ, {"INVOCATION_ID": "manager-synthesized-value"}): + restored = MODULE._original_environment(packet["forwarded_environment"], generation="a" * 32, + names=sorted(original)) + self.assertEqual(restored, original) + self.assertEqual(restored["INVOCATION_ID"], "original-synthetic-value") + self.assertEqual(restored["PYTHONHASHSEED"], "43") + + def test_environment_snapshot_exact_complete_frame_bound(self) -> None: + original = {"TRUEMEMORY_REQUIRE_PROCESS_MEMORY_CAP": "1", "TRUEMEMORY_MODEL_SERVER_GENERATION": "a" * 32, + "TRUEMEMORY_CAP_CHILD": "1", "SYNTHETIC": "value"} + packet = MODULE._original_environment_packet(original, generation="a" * 32, manifest_sha256="e" * 64) + self.assertLessEqual(len(MODULE._canonical(packet)) + 4, 65536) + oversized = {**original, "SYNTHETIC": "synthetic-private-value" * 5000} + with self.assertRaises(RuntimeError) as caught: + MODULE._original_environment_packet(oversized, generation="a" * 32, manifest_sha256="e" * 64) + self.assertNotIn("synthetic-private-value", str(caught.exception)) + for invalid in (None, 43, "invalid\x00value"): + with self.subTest(invalid=invalid), self.assertRaises((ValueError, RuntimeError)): + MODULE._original_environment({**original, "SYNTHETIC": invalid}, generation="a" * 32) + with self.assertRaises(RuntimeError): + MODULE._original_environment(original, generation="a" * 32, names=sorted(original) + ["MISSING"]) + + def test_original_snapshot_stays_out_of_retained_manifest(self) -> None: + tree = ast.parse(MODULE_PATH.read_text()) + adapter = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == "LinuxAdapter") + start = next(node for node in adapter.body if isinstance(node, ast.FunctionDef) and node.name == "start") + assignment = next(node for node in ast.walk(start) if isinstance(node, ast.Assign) + and any(isinstance(target, ast.Name) and target.id == "manifest" for target in node.targets)) + self.assertIsInstance(assignment.value, ast.Dict) + keys = {key.value for key in assignment.value.keys if isinstance(key, ast.Constant)} + self.assertIn("forwarded_names", keys) + self.assertNotIn("forwarded_environment", keys) + self.assertNotIn("environment", keys) + self.assertNotIn("environment_sha256", keys) + + def test_packaging_and_protected_capsule(self) -> None: + project = (ROOT / "pyproject.toml").read_text() + self.assertIn('truememory-model-server = "truememory.model_server:main"', project) + self.assertIn('truememory-model-server-capped = "truememory_memory_bootstrap:main"', project) + self.assertIn('[tool.hatch.build.targets.wheel.force-include]', project) + self.assertIn('"truememory_memory_bootstrap.py" = "truememory_memory_bootstrap.py"', project) + self.assertEqual(hashlib.sha256((ROOT / "truememory/memory_bootstrap.py").read_bytes()).hexdigest(), + "a33c0f4bf25bfa590390f50296623455491333ace98da97e4a3511e6aee50c5d") + tree = ast.parse(MODULE_PATH.read_text()) + imports = [node for node in ast.walk(tree) if isinstance(node, (ast.Import, ast.ImportFrom))] + for node in imports: + names = [node.module] if isinstance(node, ast.ImportFrom) else [alias.name for alias in node.names] + self.assertFalse(any(name is not None and name.startswith("truememory") for name in names)) + + +class TestLinuxBackendSixFixes(unittest.TestCase): + def test_preexec_packet_requires_exact_boolean(self) -> None: + expected = {"generation": "a" * 32, "manifest_sha256": "e" * 64, "preexec_gate_passed": True} + self.assertEqual(MODULE._literal_packet(dict(expected), expected, "preexec"), expected) + for invalid in (1, 1.0, "true", None, False): + with self.subTest(invalid=invalid), self.assertRaises((ValueError, RuntimeError)): + MODULE._literal_packet({**expected, "preexec_gate_passed": invalid}, expected, "preexec") + + def test_final_ack_requires_exact_boolean(self) -> None: + expected = {"generation": "a" * 32, "manifest_sha256": "e" * 64, "gate_passed": True} + self.assertEqual(MODULE._literal_packet(dict(expected), expected, "ack"), expected) + for invalid in (1, 1.0, "true", None, False): + with self.subTest(invalid=invalid), self.assertRaises((ValueError, RuntimeError)): + MODULE._literal_packet({**expected, "gate_passed": invalid}, expected, "ack") + + def test_ready_packet_requires_exact_scalar_types_and_keys(self) -> None: + value = identity() + expected = {"version": 1, "generation": value.generation, "manifest_sha256": value.manifest_sha256, + "pid": value.main_pid, "kernel_cap_bytes": value.cap_bytes, "metric": "linux_cgroup_charged_domain", + "physical_hard_cap": False, "dynamic_registry_cap": False} + self.assertEqual(MODULE._literal_packet(dict(expected), expected, "ready"), expected) + for name, invalid in (("version", True), ("version", 1.0), ("pid", 43.0), + ("kernel_cap_bytes", 67108864.0), ("physical_hard_cap", 0), + ("dynamic_registry_cap", 0), ("physical_hard_cap", "false")): + with self.subTest(name=name, invalid=invalid), self.assertRaises((ValueError, RuntimeError)): + MODULE._literal_packet({**expected, name: invalid}, expected, "ready") + missing = dict(expected) + del missing["version"] + for invalid in (None, [], missing, {**expected, "extra": 1}, {**expected, 1: "extra"}): + with self.subTest(invalid=invalid), self.assertRaises((ValueError, RuntimeError)): + MODULE._literal_packet(invalid, expected, "ready") + + def test_pruned_limits_only_allowed_for_empty_terminal_evidence(self) -> None: + value = identity() + terminal = replace(evidence(value), members=(), leaf_memory_max="unavailable_pruned", leaf_swap_max="unavailable_pruned") + self.assertIs(MODULE._validate_evidence(terminal, value, terminal=True), terminal) + with self.assertRaises((ValueError, RuntimeError)): + MODULE._validate_evidence(terminal, value) + for changes in ({"members": (value.main_pid,)}, {"leaf_swap_max": "max"}, + {"leaf_memory_max": "max"}, {"leaf_memory_max": None}): + with self.subTest(changes=changes), self.assertRaises((ValueError, RuntimeError)): + replace(terminal, **changes) + + def inspect_terminal(self, *, control_group: str | None = None, leaf: str = "pruned", + exec_pid: int = 43, members: tuple[int, ...] = (), exited: bool = True) -> tuple[object, list[str]]: + value = identity() + trace: list[str] = [] + scope = MODULE._NativeScope(29, "/synthetic", 70, 71, (), pidfd_device=101, pidfd_inode=103) + runner = MODULE._NativeRunner(object(), object(), {}) + endpoint = MODULE._NativeEndpoint(SimpleNamespace(fileno=lambda: 73), SimpleNamespace(fileno=lambda: 74), + value.main_pid, value.uid, {"generation": value.generation, "manifest_sha256": value.manifest_sha256, + "pid": value.main_pid, "start_ticks": value.start_ticks, "bootstrap_sha256": value.bootstrap_sha256}, + 107, 109, 113, 127) + record = MODULE.LinuxProvisionalLaunch(value, runner, 72, scope, endpoint) + metadata = {70: (31, 37), 71: (31, 41), 72: (101, 103), 73: (107, 109), + 74: (113, 127), 75: (31, 37), 76: (31, 999 if leaf == "replacement" else 41)} + + def fstat(descriptor: int) -> object: + device, inode = metadata[descriptor] + return SimpleNamespace(st_uid=1000, st_dev=device, st_ino=inode) + + def open_directory(path: Path, *, uid: int | None = None) -> int: + self.assertEqual(uid, value.uid) + if str(path) == "/synthetic" + value.slice_relative_path: + trace.append("open_slice") + return 75 + self.assertEqual(str(path), "/synthetic" + value.service_relative_path) + trace.append("open_leaf") + if leaf == "pruned": + raise FileNotFoundError("synthetic pruned leaf") + if leaf == "permission": + raise PermissionError("synthetic inaccessible leaf") + return 76 + + def properties(unit: str, fields: tuple[str, ...], deadline: float) -> dict[str, str]: + if unit == value.slice_unit: + trace.append("slice_manager") + return {"InvocationID": value.slice_invocation_id, "ControlGroup": value.slice_relative_path, + "MemoryMax": str(value.cap_bytes), "MemorySwapMax": "0", "ActiveState": "active"} + self.assertEqual(unit, value.service_unit) + trace.append("service_manager") + return {"InvocationID": value.service_invocation_id, + "ControlGroup": value.service_relative_path if control_group is None else control_group, + "MainPID": "0", "ExecMainPID": str(exec_pid), "ActiveState": "active", "SubState": "exited"} + + def read_at(descriptor: int, name: str) -> str: + if descriptor == 70: + return {"memory.max": str(value.cap_bytes), "memory.swap.max": "0"}[name] + self.assertEqual(descriptor, 71) + self.assertIn(name, ("memory.max", "memory.swap.max")) + return "max" + + def census(descriptor: int, relative: str) -> tuple[tuple[str, tuple[int, ...]], ...]: + trace.append("census") + return ((relative, members),) + + def process_identity(pid: int) -> tuple[int, float]: + trace.append("process_exited") + raise FileNotFoundError("synthetic exited PID") + + def pidfd_ready(read: list[int], write: list[int], errors: list[int], timeout: int) -> tuple[list[int], list[int], list[int]]: + trace.append("pidfd_exit") + return ([72] if exited else [], [], []) + + with ExitStack() as stack: + for target, name, replacement in ((MODULE.sys, "platform", "linux"), (MODULE.os, "getuid", lambda: 1000), + (MODULE, "_boot_id", lambda: value.boot_id), (MODULE, "_mount", lambda: (29, "/synthetic")), + (MODULE.os, "fstat", fstat), (MODULE.os, "close", lambda descriptor: None), + (MODULE, "_open_directory", open_directory), (MODULE, "_filesystem_magic", lambda descriptor: None), + (MODULE, "_properties", properties), (MODULE, "_read_at", read_at), + (MODULE, "_ancestor_limits", lambda *args: ()), (MODULE, "_recursive_members", census), + (MODULE, "_process_identity", process_identity), (MODULE.select, "select", pidfd_ready), + (MODULE, "_peer", lambda connection: (value.main_pid, value.uid))): + stack.enter_context(patch.object(target, name, replacement)) + result = MODULE.LinuxAdapter().inspect(record, time.monotonic() + 10.0) + return result, trace + + def test_actual_inspector_accepts_only_proven_terminal_pruning(self) -> None: + for control_group in (None, ""): + with self.subTest(control_group=control_group): + result, trace = self.inspect_terminal(control_group=control_group) + self.assertEqual(result.members, ()) + self.assertEqual(result.leaf_memory_max, "unavailable_pruned") + self.assertEqual(result.leaf_swap_max, "unavailable_pruned") + for proof in ("service_manager", "census", "pidfd_exit", "process_exited"): + self.assertLess(trace.index(proof), trace.index("open_leaf")) + + def test_actual_inspector_refuses_pruning_without_owned_terminal_proof(self) -> None: + for changes in ({"control_group": "/foreign.service"}, {"exec_pid": 99}, + {"members": (43, 99)}, {"exited": False}, {"leaf": "permission"}, + {"leaf": "replacement"}, {"leaf": "present", "control_group": ""}): + with self.subTest(changes=changes), self.assertRaises((ValueError, RuntimeError, OSError)): + self.inspect_terminal(**changes) + present, _ = self.inspect_terminal(leaf="present") + self.assertEqual((present.leaf_memory_max, present.leaf_swap_max), ("max", "max")) + + def capture_terminal(self, *, pruned: bool, missing_leaf_events: bool = False, + slice_decreased: bool = False) -> tuple[object, dict[str, object]]: + value = identity() + terminal = replace(evidence(value), members=()) + if pruned: + terminal = replace(terminal, leaf_memory_max="unavailable_pruned", leaf_swap_max="unavailable_pruned") + baseline = (("max", 1), ("oom", 0)) + payloads: list[dict[str, object]] = [] + + class Store: + directory = 75 + hashes = {"manifest-v1.json": "e" * 64} + + def observe(self, name: str, **kwargs: object) -> None: + return None + + def append(self, phase: str, payload: dict[str, object]) -> tuple[int, str]: + payloads.append(payload) + return 5, "f" * 64 + + def verify(self) -> None: + return None + + scope = SimpleNamespace(slice_fd=70, service_fd=71, baseline=baseline, baseline_local=baseline, + leaf_baseline=baseline, leaf_baseline_local=baseline) + record = MODULE.LinuxProvisionalLaunch(value, SimpleNamespace(store=Store()), 72, scope, object()) + adapter = MODULE.LinuxAdapter() + manager = {"InvocationID": value.service_invocation_id, "ExecMainPID": str(value.main_pid), + "MainPID": "0", "ExecMainCode": "1", "ExecMainStatus": "0", "Result": "success", "SubState": "exited"} + + def read_at(descriptor: int, name: str) -> str: + self.assertEqual(descriptor, 70) + if name == "memory.peak": + raise FileNotFoundError("synthetic unavailable peak") + return {"cgroup.events": "populated 0\n", "memory.current": "123"}[name] + + def events(descriptor: int, name: str = "memory.events") -> tuple[tuple[str, int], ...]: + if descriptor == 71: + self.assertFalse(pruned, "A pruned leaf must not receive counter reads") + if missing_leaf_events: + raise FileNotFoundError("synthetic missing leaf counter") + self.assertIn(descriptor, (70, 71)) + return (("max", 0 if slice_decreased and descriptor == 70 else 2), ("oom", 1)) + + with ExitStack() as stack: + stack.enter_context(patch.object(adapter, "inspect", return_value=terminal)) + stack.enter_context(patch.object(MODULE, "_properties", return_value=manager)) + stack.enter_context(patch.object(MODULE, "_recursive_members", return_value=((value.slice_relative_path, ()),))) + stack.enter_context(patch.object(MODULE, "_read_at", read_at)) + stack.enter_context(patch.object(MODULE, "_controller_events", events)) + result = adapter.capture(record, value, time.monotonic() + 10.0) + self.assertEqual(len(payloads), 1) + return result, payloads[0] + + def test_actual_capture_retains_pruned_baselines_and_null_after(self) -> None: + result, payload = self.capture_terminal(pruned=True) + self.assertEqual(result.events_before, (("max", 1), ("oom", 0))) + self.assertEqual(result.events_after, (("max", 2), ("oom", 1))) + self.assertEqual(payload["leaf_hierarchical_before"], result.events_before) + self.assertEqual(payload["leaf_local_before"], result.events_before) + for name in ("leaf_hierarchical_after", "leaf_local_after", "leaf_hierarchical_deltas", "leaf_local_deltas"): + self.assertIsNone(payload[name]) + self.assertIs(payload["leaf_counters_available"], False) + self.assertEqual(payload["leaf_counter_status"], "unavailable_pruned") + self.assertIsNone(result.memory_peak) + + def test_actual_capture_requires_slice_counters_and_real_pruning(self) -> None: + with self.assertRaises(RuntimeError): + self.capture_terminal(pruned=True, slice_decreased=True) + with self.assertRaises(RuntimeError): + self.capture_terminal(pruned=False, missing_leaf_events=True) + _, payload = self.capture_terminal(pruned=False) + self.assertIs(payload["leaf_counters_available"], True) + self.assertEqual(payload["leaf_counter_status"], "available") + self.assertEqual(payload["leaf_hierarchical_deltas"], (("max", 1), ("oom", 1))) + + +class TestLinuxRetainedPreexecPackets(unittest.TestCase): + def validate_reader(self, method: str, payload: object, value: object) -> object: + """Run each actual retained-packet validation block without native I/O.""" + tree = ast.parse(MODULE_PATH.read_text()) + if method == "inspect_existing": + adapter = next(node for node in tree.body if isinstance(node, ast.ClassDef) and node.name == "LinuxAdapter") + function = next(node for node in adapter.body if isinstance(node, ast.FunctionDef) and node.name == method) + else: + function = next(node for node in tree.body if isinstance(node, ast.FunctionDef) and node.name == method) + guarded = next(node for node in function.body if isinstance(node, ast.Try)) + start = next(index for index, node in enumerate(guarded.body) + if isinstance(node, ast.Assign) and any(isinstance(target, ast.Name) and target.id == "preexec_data" for target in node.targets)) + end = next(index for index in range(start + 1, len(guarded.body)) + if isinstance(guarded.body[index], ast.Assign) + and any(isinstance(target, ast.Name) and target.id == "barrier" for target in guarded.body[index].targets)) + wrapper = ast.parse("def validate(preexec_data, identity, identity_sha):\n pass\n") + wrapper.body[0].body = guarded.body[start + 1:end + 1] + [ast.Return(value=ast.Name(id="barrier", ctx=ast.Load()))] + namespace = {"_literal_packet": MODULE._literal_packet, "_preexec_payload": MODULE._preexec_payload, + "EnforcementUnavailable": MODULE.EnforcementUnavailable} + exec(compile(ast.fix_missing_locations(wrapper), "", "exec"), namespace) + return namespace["validate"](payload, value, "f" * 64) + + def test_retained_readers_accept_exact_packets(self) -> None: + value = identity() + barrier = {"descriptor": 5, "device": 7, "inode": 11} + expected = MODULE._preexec_payload(value, "f" * 64, barrier) + for method in ("inspect_existing", "_restore_child_context"): + with self.subTest(method=method): + self.assertEqual(self.validate_reader(method, dict(expected), value), barrier) + + def test_retained_readers_refuse_equal_scalar_type_aliases(self) -> None: + value = identity() + expected = MODULE._preexec_payload(value, "f" * 64, {"descriptor": 5, "device": 7, "inode": 11}) + invalid_fields = (("version", True), ("version", 1.0), ("pid", float(value.main_pid)), + ("start_ticks", float(value.start_ticks)), ("kernel_cap_bytes", float(value.cap_bytes)), + ("physical_hard_cap", 0), ("physical_hard_cap", 0.0), + ("dynamic_registry_cap", 0), ("dynamic_registry_cap", 0.0)) + for method in ("inspect_existing", "_restore_child_context"): + for name, invalid in invalid_fields: + with self.subTest(method=method, name=name, invalid=invalid), self.assertRaises((ValueError, RuntimeError)): + self.validate_reader(method, {**expected, name: invalid}, value) + + def test_retained_readers_refuse_non_dict_and_changed_schema(self) -> None: + class Packet(dict[str, object]): + pass + + class GetTrap: + def get(self, key: str) -> object: + raise AssertionError("Non-dict retained packet accessed get") + + value = identity() + expected = MODULE._preexec_payload(value, "f" * 64, {"descriptor": 5, "device": 7, "inode": 11}) + missing = dict(expected) + del missing["version"] + for method in ("inspect_existing", "_restore_child_context"): + for invalid in (None, [], False, 0, "{}", Packet(expected), GetTrap(), missing, + {**expected, "extra": 1}, {**expected, 1: "extra"}): + with self.subTest(method=method, invalid_type=type(invalid).__name__), self.assertRaises(MODULE.EnforcementUnavailable): + self.validate_reader(method, invalid, value) + + def test_retained_readers_preserve_strict_nested_barrier_validation(self) -> None: + value = identity() + barrier = {"descriptor": 5, "device": 7, "inode": 11} + expected = MODULE._preexec_payload(value, "f" * 64, barrier) + invalid_barriers = (None, [], {"descriptor": 5, "device": 7}, {**barrier, "extra": 1}, + {**barrier, "descriptor": True}, {**barrier, "descriptor": 1}, + {**barrier, "device": 7.0}, {**barrier, "inode": 11.0}) + for method in ("inspect_existing", "_restore_child_context"): + for invalid in invalid_barriers: + with self.subTest(method=method, invalid=invalid), self.assertRaises((ValueError, RuntimeError)): + self.validate_reader(method, {**expected, "barrier": invalid}, value) + + +class TerminationStore: + directory = 75 + + def __init__(self, trace: list[str]) -> None: + self.trace = trace + self.path = Path("/synthetic/evidence") + self.metadata = SimpleNamespace(st_dev=211, st_ino=223, st_uid=1000) + self.callback = None + self.sequence = 0 + + def verify(self) -> None: + self.trace.append("store_verify") + + def append(self, phase: str, payload: object) -> tuple[int, str]: + self.trace.append(phase) + self.sequence += 1 + if self.callback is not None: + self.callback(phase) + return self.sequence, "f" * 64 + + +class TerminationAdapter(InertAdapter): + def __init__(self) -> None: + super().__init__() + self.trace: list[str] = [] + self.store = TerminationStore(self.trace) + self.record = MODULE.LinuxProvisionalLaunch(self.identity, SimpleNamespace(store=self.store), + object(), object(), object()) + self.callback = None + + def terminate(self, record: object, value: object, deadline: float) -> None: + self.calls.append("terminate") + self.trace.append("terminate") + self.deadlines.append(deadline) + if self.callback is not None: + self.callback() + self.terminal = True + + +def native_termination_record() -> tuple[object, list[str]]: + value = identity() + trace: list[str] = [] + store = TerminationStore(trace) + scope = MODULE._NativeScope(29, "/synthetic", 70, 71, (), pidfd_device=101, pidfd_inode=103) + runner = MODULE._NativeRunner(object(), store, {"version": 1}) + endpoint = MODULE._NativeEndpoint(SimpleNamespace(fileno=lambda: 73), SimpleNamespace(fileno=lambda: 74), + value.main_pid, value.uid, {}, 107, 109, 113, 127) + return MODULE.LinuxProvisionalLaunch(value, runner, 72, scope, endpoint), trace + + +class TestLinuxOwnedTerminationContract(unittest.TestCase): + def owned(self) -> tuple[object, object]: + adapter = TerminationAdapter() + handle = MODULE.OwnedLinuxLaunch(adapter.record, MODULE.LinuxDaemonLaunchPolicy(1, 67108864, 0), adapter) + return handle, adapter + + def test_deadline_exact_float_before_callbacks(self) -> None: + for invalid in (1005, True, None, float("nan"), float("inf"), 1000.0, 999.0): + handle, adapter = self.owned() + with self.subTest(invalid=invalid), patch.object(MODULE.time, "monotonic", return_value=1000.0), self.assertRaises((ValueError, TypeError, TimeoutError)): + MODULE.terminate_owned_exact(handle, deadline=invalid) + self.assertEqual(adapter.calls, []) + self.assertEqual(adapter.trace, []) + + def test_public_owner_rejects_nonhandle_and_subtype(self) -> None: + handle, adapter = self.owned() + class Subtype(MODULE.OwnedLinuxLaunch): + pass + subtype = Subtype(handle.record, handle.policy, adapter) + for invalid in (None, handle.record, evidence(adapter.identity), subtype): + with self.subTest(invalid_type=type(invalid).__name__), patch.object(MODULE.time, "monotonic", return_value=1000.0), self.assertRaises((ValueError, TypeError)): + MODULE.terminate_owned_exact(invalid, deadline=1005.0) + self.assertEqual(adapter.trace, []) + + def test_intent_precedes_adapter_and_capture_precedes_stop(self) -> None: + handle, adapter = self.owned() + with patch.object(MODULE.time, "monotonic", return_value=1000.0): + result = MODULE.terminate_owned_exact(handle, deadline=1100.0) + self.assertIs(type(result), MODULE.LinuxExitReceipt) + self.assertLess(adapter.trace.index("termination-intent"), adapter.trace.index("terminate")) + self.assertLess(adapter.calls.index("capture"), adapter.calls.index("stop")) + self.assertTrue(all(value == 1005.0 for value in adapter.deadlines)) + self.assertIsNone(getattr(MODULE._termination_context, "owner", None)) + + def test_shorter_original_deadline_is_not_extended(self) -> None: + handle, adapter = self.owned() + with patch.object(MODULE.time, "monotonic", return_value=1000.0): + MODULE.terminate_owned_exact(handle, deadline=1001.5) + self.assertTrue(all(value == 1001.5 for value in adapter.deadlines)) + + def test_durable_callback_mutation_refuses_before_adapter(self) -> None: + handle, adapter = self.owned() + adapter.store.callback = lambda phase: setattr(handle, "_adapter", object()) + with patch.object(MODULE.time, "monotonic", return_value=1000.0), self.assertRaises(RuntimeError): + MODULE.terminate_owned_exact(handle, deadline=1005.0) + self.assertEqual(adapter.calls, []) + self.assertIn("termination-intent", adapter.trace) + + def test_adapter_reflective_policy_mutation_blocks_capture(self) -> None: + handle, adapter = self.owned() + adapter.callback = lambda: object.__setattr__(handle.policy, "cap_bytes", True) + with patch.object(MODULE.time, "monotonic", return_value=1000.0), self.assertRaises((ValueError, RuntimeError)): + MODULE.terminate_owned_exact(handle, deadline=1005.0) + self.assertNotIn("capture", adapter.calls) + self.assertNotIn("stop", adapter.calls) + + def test_store_swap_blocks_foreign_failure_write(self) -> None: + handle, adapter = self.owned() + foreign = TerminationStore([]) + adapter.callback = lambda: setattr(handle.record.runner, "store", foreign) + with patch.object(MODULE.time, "monotonic", return_value=1000.0), self.assertRaises(RuntimeError): + MODULE.terminate_owned_exact(handle, deadline=1005.0) + self.assertEqual(foreign.trace, []) + self.assertNotIn("stop", adapter.calls) + + def test_expiration_after_intent_blocks_new_effects(self) -> None: + handle, adapter = self.owned() + clock = [1000.0] + adapter.store.callback = lambda phase: clock.__setitem__(0, 1006.0) + with patch.object(MODULE.time, "monotonic", side_effect=lambda: clock[0]), self.assertRaises(TimeoutError): + MODULE.terminate_owned_exact(handle, deadline=1005.0) + self.assertEqual(adapter.calls, []) + + def native_sequence(self, *, observations: list[tuple[bool, bool]], signals: list[bool], waits: list[bool]) -> tuple[list[int], list[float], list[str]]: + record, trace = native_termination_record() + sent: list[int] = [] + wait_deadlines: list[float] = [] + def send(*args: object) -> bool: + sent.append(int(args[3])) + return signals.pop(0) + def wait(*args: object) -> bool: + wait_deadlines.append(args[3]) + return waits.pop(0) + with ExitStack() as stack: + stack.enter_context(patch.object(MODULE.time, "monotonic", return_value=1000.0)) + stack.enter_context(patch.object(MODULE, "_termination_native_snapshot", side_effect=observations)) + stack.enter_context(patch.object(MODULE, "_signal_owned_pidfd", side_effect=send)) + stack.enter_context(patch.object(MODULE, "_wait_pidfd_exit", side_effect=wait)) + stack.enter_context(patch.object(MODULE, "_wait_owned_terminal", side_effect=lambda *args: trace.append("terminal_verified"))) + MODULE.LinuxAdapter().terminate(record, record.identity, 1100.0) + return sent, wait_deadlines, trace + + def test_native_term_then_conditional_kill(self) -> None: + sent, waits, trace = self.native_sequence(observations=[(False, False)] * 3, signals=[False, True], waits=[False]) + self.assertEqual(sent, [int(MODULE.signal.SIGTERM), int(MODULE.signal.SIGKILL)]) + self.assertEqual(waits, [1001.0]) + self.assertLess(trace.index("termination-native-intent"), trace.index("termination-signal-intent")) + self.assertLess(trace.index("terminal_verified"), trace.index("termination-manager-reaped")) + + def test_native_term_exit_skips_kill(self) -> None: + sent, waits, _ = self.native_sequence(observations=[(False, False)] * 2, signals=[False], waits=[True]) + self.assertEqual(sent, [int(MODULE.signal.SIGTERM)]) + self.assertEqual(waits, [1001.0]) + + def test_already_exited_skips_all_signals(self) -> None: + sent, waits, trace = self.native_sequence(observations=[(True, True)], signals=[], waits=[]) + self.assertEqual(sent, []) + self.assertEqual(waits, []) + self.assertIn("termination-manager-reaped", trace) + + def test_exit_before_kill_skips_kill_signal(self) -> None: + sent, waits, _ = self.native_sequence(observations=[(False, False), (False, False), (True, False)], signals=[False], waits=[False]) + self.assertEqual(sent, [int(MODULE.signal.SIGTERM)]) + self.assertEqual(waits, [1001.0]) + + def test_manager_reap_wait_has_no_deadline_renewal(self) -> None: + record, _ = native_termination_record() + resources = MODULE._termination_resources(record) + seen: list[float] = [] + def pause(read: list[int], write: list[int], errors: list[int], delay: float) -> tuple[list[int], list[int], list[int]]: + self.assertEqual(read, []) + seen.append(delay) + return [], [], [] + with patch.object(MODULE.time, "monotonic", return_value=1000.0), patch.object(MODULE, "_termination_native_snapshot", side_effect=[(True, False), (True, True)]), patch.object(MODULE.select, "select", side_effect=pause): + MODULE._wait_owned_terminal(MODULE.LinuxAdapter(), record, record.identity, resources, 1000.01) + self.assertEqual(len(seen), 1) + self.assertLessEqual(seen[0], 0.01 + 1e-9) + + def test_pidfd_target_requires_exact_native_identity(self) -> None: + with patch.object(MODULE, "_read_path", return_value=b"Pid:\t43\nNSpid:\t43\n") as reader: + self.assertIs(MODULE._pidfd_target(90, 43), True) + self.assertEqual(reader.call_args.kwargs, {"bound": 4096}) + for raw in (b"Pid: 44\nNSpid: 44\n", b"Pid: 43\nNSpid: 44\n", b"Pid: 43\nPid: 43\nNSpid: 43\n", b"Pid: 43\n", b"Pid: -1\nNSpid: 43\n"): + with self.subTest(raw=raw), patch.object(MODULE, "_read_path", return_value=raw), self.assertRaises((ValueError, RuntimeError)): + MODULE._pidfd_target(90, 43) + with patch.object(MODULE, "_read_path", return_value=b"Pid: -1\nNSpid: -1\n"): + self.assertIs(MODULE._pidfd_target(90, 43), False) + + def pidfd_call(self, *, error: OSError | None = None, rebound: bool = False, duplicate: int = 90, + exit_on_esrch: bool = True) -> tuple[list[str], list[int]]: + record, trace = native_termination_record() + resources = MODULE._termination_resources(record) + exited = [False] + replaced = [False] + closed: list[int] = [] + def fstat(fd: int) -> object: + return SimpleNamespace(st_dev=101, st_ino=999 if fd == 90 and replaced[0] else 103) + def ready(read: list[int], write: list[int], errors: list[int], timeout: float) -> tuple[list[int], list[int], list[int]]: + return (list(read) if exited[0] else []), [], [] + def send(fd: int, requested_signal: int, info: object, flags: int) -> None: + self.assertEqual((fd, info, flags), (90, None, 0)) + trace.append("pidfd_signal") + if rebound: + replaced[0] = True + if isinstance(error, ProcessLookupError) and exit_on_esrch: + exited[0] = True + if error is not None: + raise error + def atomic(fd: int, command: int, minimum: int) -> int: + self.assertEqual((fd, command, minimum), (72, 1030, 3)) + trace.append("atomic_duplicate") + return duplicate + with ExitStack() as stack: + for target, name, replacement in ((MODULE.time, "monotonic", lambda: 1000.0), + (MODULE.os, "fstat", fstat), (MODULE.os, "close", lambda fd: closed.append(fd)), + (MODULE.select, "select", ready), + (MODULE, "_read_path", lambda *args, **kwargs: b"Pid: 43\nNSpid: 43\n"), + (MODULE.importlib, "import_module", lambda name: SimpleNamespace(F_DUPFD_CLOEXEC=1030, fcntl=atomic))): + stack.enter_context(patch.object(target, name, replacement)) + stack.enter_context(patch.object(MODULE.signal, "pidfd_send_signal", side_effect=send, create=True)) + MODULE._signal_owned_pidfd(record, record.identity, resources, MODULE.signal.SIGTERM, 1005.0) + return trace, closed + + def test_private_atomic_duplicate_only_is_signaled_and_closed(self) -> None: + trace, closed = self.pidfd_call() + self.assertLess(trace.index("atomic_duplicate"), trace.index("pidfd_signal")) + self.assertEqual(closed, [90]) + self.assertNotIn(72, closed) + + def test_private_esrch_requires_actual_exit(self) -> None: + trace, closed = self.pidfd_call(error=ProcessLookupError(3, "synthetic exited target")) + self.assertIn("pidfd_signal", trace) + self.assertEqual(closed, [90]) + + def test_native_permission_error_never_falls_back(self) -> None: + with self.assertRaises(PermissionError): + self.pidfd_call(error=PermissionError(1, "synthetic denied")) + + def test_esrch_without_observed_exit_refuses(self) -> None: + with self.assertRaises(RuntimeError): + self.pidfd_call(error=ProcessLookupError(3, "synthetic unresolved target"), exit_on_esrch=False) + + def test_private_signal_selection_rejects_before_fd_observers(self) -> None: + record, _ = native_termination_record() + resources = MODULE._termination_resources(record) + for invalid in (True, None, "TERM", 0, int(MODULE.signal.SIGUSR1)): + with self.subTest(invalid=invalid), patch.object(MODULE.time, "monotonic", return_value=1000.0), patch.object(MODULE.os, "fstat") as observer, self.assertRaises(ValueError): + MODULE._signal_owned_pidfd(record, record.identity, resources, invalid, 1005.0) + observer.assert_not_called() + + def test_native_deadline_rejects_before_any_store_observer(self) -> None: + record, trace = native_termination_record() + for invalid in (1005, True, float("inf"), 1000.0): + with self.subTest(invalid=invalid), patch.object(MODULE.time, "monotonic", return_value=1000.0), self.assertRaises((ValueError, TimeoutError)): + MODULE.LinuxAdapter().terminate(record, record.identity, invalid) + self.assertEqual(trace, []) + + def test_rebound_private_descriptor_is_not_closed(self) -> None: + with self.assertRaises(RuntimeError): + self.pidfd_call(rebound=True) + + def test_original_descriptor_is_never_used_as_private_duplicate(self) -> None: + with self.assertRaises(RuntimeError): + self.pidfd_call(duplicate=72) + + def test_fresh_public_inspect_precedes_delegate(self) -> None: + handle, adapter = self.owned() + original = adapter.inspect + def inspect(record: object, deadline: float) -> object: + adapter.trace.append("fresh-inspect") + return original(record, deadline) + adapter.inspect = inspect + with patch.object(MODULE.time, "monotonic", return_value=1000.0): + MODULE.terminate_owned_exact(handle, deadline=1100.0) + self.assertLess(adapter.trace.index("termination-intent"), adapter.trace.index("fresh-inspect")) + self.assertLess(adapter.trace.index("fresh-inspect"), adapter.trace.index("terminate")) + self.assertTrue(all(value == 1005.0 for value in adapter.deadlines)) + + def test_public_empty_domain_skips_delegate(self) -> None: + handle, adapter = self.owned() + adapter.terminal = True + with patch.object(MODULE.time, "monotonic", return_value=1000.0): + result = MODULE.terminate_owned_exact(handle, deadline=1005.0) + self.assertIs(type(result), MODULE.LinuxExitReceipt) + self.assertNotIn("terminate", adapter.calls) + self.assertIn("capture", adapter.calls) + self.assertLess(adapter.calls.index("capture"), adapter.calls.index("stop")) + + def test_invalid_fresh_evidence_blocks_delegate(self) -> None: + for field, invalid in (("mount_authenticated", False), ("namespace_authenticated", False), + ("membership_authenticated", False), ("kernel_limit_preexec", False), + ("cap_bytes", True), ("cap_bytes", 67108865), ("swap_bytes", 1), + ("members", (43, 44)), ("members", [43]), ("peer_pid", 44), + ("peer_start_ticks", 48), ("generation", "b" * 32), + ("manifest_sha256", "a" * 64), ("leaf_memory_max", "0")): + handle, adapter = self.owned() + observed = evidence(adapter.identity) + object.__setattr__(observed, field, invalid) + adapter.inspect = lambda record, deadline: observed + with self.subTest(field=field, invalid=invalid), patch.object(MODULE.time, "monotonic", return_value=1000.0), self.assertRaises((ValueError, RuntimeError)): + MODULE.terminate_owned_exact(handle, deadline=1005.0) + self.assertNotIn("terminate", adapter.calls) + self.assertNotIn("capture", adapter.calls) + self.assertNotIn("stop", adapter.calls) + + def test_fresh_evidence_exact_type_is_required(self) -> None: + for invalid in (None, False, SimpleNamespace(**vars(evidence(identity())))): + handle, adapter = self.owned() + adapter.inspect = lambda record, deadline: invalid + with self.subTest(invalid_type=type(invalid).__name__), patch.object(MODULE.time, "monotonic", return_value=1000.0), self.assertRaises((TypeError, ValueError, RuntimeError)): + MODULE.terminate_owned_exact(handle, deadline=1005.0) + self.assertNotIn("terminate", adapter.calls) + self.assertNotIn("capture", adapter.calls) + self.assertNotIn("stop", adapter.calls) + + def test_fresh_inspect_owner_mutation_blocks_delegate(self) -> None: + mutations = ( + ("record", lambda handle, adapter: setattr(handle, "record", MODULE.LinuxProvisionalLaunch(adapter.identity, object(), object(), object(), object()))), + ("policy", lambda handle, adapter: setattr(handle, "policy", MODULE.LinuxDaemonLaunchPolicy(1, 67108864, 0))), + ("adapter", lambda handle, adapter: setattr(handle, "_adapter", object())), + ("released", lambda handle, adapter: setattr(handle, "_released", True)), + ("identity", lambda handle, adapter: object.__setattr__(handle.record, "identity", replace(adapter.identity, start_ticks=48))), + ("runner", lambda handle, adapter: object.__setattr__(handle.record, "runner", SimpleNamespace(store=adapter.store))), + ("pidfd", lambda handle, adapter: object.__setattr__(handle.record, "pidfd", object())), + ("scope", lambda handle, adapter: object.__setattr__(handle.record, "scope_fd", object())), + ("endpoint", lambda handle, adapter: object.__setattr__(handle.record, "endpoint", object())), + ("store", lambda handle, adapter: setattr(handle.record.runner, "store", TerminationStore([]))), + ("directory", lambda handle, adapter: setattr(adapter.store, "directory", 76)), + ("metadata", lambda handle, adapter: setattr(adapter.store, "metadata", SimpleNamespace(st_dev=211, st_ino=223, st_uid=1000))), + ("path", lambda handle, adapter: setattr(adapter.store, "path", Path("/synthetic/rebound"))), + ) + for empty in (False, True): + for name, mutate in mutations: + handle, adapter = self.owned() + observed = evidence(adapter.identity) + if empty: + observed = replace(observed, members=()) + def inspect(record: object, deadline: float) -> object: + mutate(handle, adapter) + return observed + adapter.inspect = inspect + with self.subTest(empty=empty, mutation=name), patch.object(MODULE.time, "monotonic", return_value=1000.0), self.assertRaises((TypeError, ValueError, RuntimeError)): + MODULE.terminate_owned_exact(handle, deadline=1005.0) + self.assertNotIn("terminate", adapter.calls) + self.assertNotIn("capture", adapter.calls) + self.assertNotIn("stop", adapter.calls) + + def test_public_inspect_expiration_blocks_delegate(self) -> None: + handle, adapter = self.owned() + clock = [1000.0] + def inspect(record: object, deadline: float) -> object: + clock[0] = 1006.0 + return evidence(adapter.identity) + adapter.inspect = inspect + with patch.object(MODULE.time, "monotonic", side_effect=lambda: clock[0]), self.assertRaises(TimeoutError): + MODULE.terminate_owned_exact(handle, deadline=1005.0) + self.assertNotIn("terminate", adapter.calls) + self.assertNotIn("capture", adapter.calls) + self.assertNotIn("stop", adapter.calls) + + def test_public_inspect_failure_blocks_delegate(self) -> None: + handle, adapter = self.owned() + def inspect(record: object, deadline: float) -> object: + raise PermissionError(1, "synthetic observer denial") + adapter.inspect = inspect + with patch.object(MODULE.time, "monotonic", return_value=1000.0), self.assertRaises(PermissionError): + MODULE.terminate_owned_exact(handle, deadline=1005.0) + self.assertNotIn("terminate", adapter.calls) + self.assertNotIn("capture", adapter.calls) + self.assertNotIn("stop", adapter.calls) + + def test_live_fresh_evidence_requires_terminal_after_delegate(self) -> None: + handle, adapter = self.owned() + adapter.terminate = lambda record, value, deadline: adapter.calls.append("terminate") + with patch.object(MODULE.time, "monotonic", return_value=1000.0), self.assertRaises(RuntimeError): + MODULE.terminate_owned_exact(handle, deadline=1005.0) + self.assertIn("terminate", adapter.calls) + self.assertNotIn("capture", adapter.calls) + self.assertNotIn("stop", adapter.calls) + + def test_empty_fresh_evidence_requires_terminal_receipt(self) -> None: + handle, adapter = self.owned() + adapter.terminal = True + adapter.capture = lambda record, value, deadline: None + with patch.object(MODULE.time, "monotonic", return_value=1000.0), self.assertRaises((TypeError, ValueError, RuntimeError)): + MODULE.terminate_owned_exact(handle, deadline=1005.0) + self.assertNotIn("terminate", adapter.calls) + self.assertNotIn("stop", adapter.calls) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-linux-startup-correlation-297.py b/tests/test-linux-startup-correlation-297.py new file mode 100644 index 00000000..d833408b --- /dev/null +++ b/tests/test-linux-startup-correlation-297.py @@ -0,0 +1,452 @@ +"""Synthetic pure S0 controls. No native shim or application import.""" +from __future__ import annotations + +import ast +from dataclasses import fields, replace +import hashlib +import inspect +import json +import math +from pathlib import Path +import re +import sys +import types +import unittest + + +SOURCE = Path(__file__).resolve().parents[1] / "truememory_memory_bootstrap.py" +PUBLIC_CLASSES = ("StartupCreateStructure", "StartupBusContext", "StartupProperty", "StartupMessageEnvelope", "StartupCorrelationState", "StartupInvocationBinding") +PUBLIC_FUNCTIONS = ("redacted_create_metadata_sha256", "new_startup_correlation", "mark_startup_send_attempt", "bind_startup_request_cookie", "parse_startup_envelope", "startup_envelope_evidence_sha256", "advance_startup_correlation", "invalidate_startup_correlation", "expire_startup_correlation", "startup_correlation_binding") + + +def load_pure_source() -> types.ModuleType: + tree = ast.parse(SOURCE.read_text()) + selected = [] + for node in tree.body: + if isinstance(node, ast.Assign) and all(isinstance(target, ast.Name) and target.id.startswith("_STARTUP_") for target in node.targets): + selected.append(node) + elif isinstance(node, ast.ClassDef) and node.name in PUBLIC_CLASSES + ("EnforcementUnavailable",): + selected.append(node) + elif isinstance(node, ast.FunctionDef) and (node.name in PUBLIC_FUNCTIONS or node.name.startswith("_startup_")): + selected.append(node) + module = types.ModuleType("synthetic_startup_correlation_297") + module.__dict__.update({"dataclass": __import__("dataclasses").dataclass, "hashlib": hashlib, "json": json, "math": math, "re": re}) + sys.modules[module.__name__] = module + body = [ast.ImportFrom(module="__future__", names=[ast.alias(name="annotations")], level=0)] + selected + exec(compile(ast.fix_missing_locations(ast.Module(body=body, type_ignores=[])), str(SOURCE), "exec"), module.__dict__) + return module + + +class StartupCorrelationTests(unittest.TestCase): + @classmethod + def setUpClass(cls) -> None: + cls.api = load_pure_source() + + def setUp(self) -> None: + a = self.api + self.context = a.StartupBusContext(1, "a" * 32, ":1.7", ":1.42", 1000) + self.name = "app-synthetic.service" + self.path = "/org/freedesktop/systemd1/unit/app_2dsynthetic_2eservice" + self.job = "/org/freedesktop/systemd1/job/17" + self.metadata = a.StartupCreateStructure(1, self.name, self.path, "fail", (("Environment", "as"), ("ExecStart", "a(sasb)")), ("LANG", "PYTHONPATH"), ()) + self.invocation = bytes(range(1, 17)) + + def state(self, *, cookie: bool = True) -> object: + a = self.api + state = a.new_startup_correlation(self.context, self.metadata, deadline=10.0, now=1.0) + state = a.mark_startup_send_attempt(state, now=1.1) + return a.bind_startup_request_cookie(state, cookie=42, now=1.2) if cookie else state + + def envelope(self, kind: str, receive_ordinal: int, *, serial: int | None = None, **changes: object) -> object: + packet: dict[str, object] = dict(version=1, ordinal=receive_ordinal, kind=kind, sender=":1.7", destination=None, serial=receive_ordinal + 100 if serial is None else serial, reply_cookie=None, path="/org/freedesktop/systemd1", interface="org.freedesktop.systemd1.Manager", member=None, signature="", payload=(), error_name=None, error_body_redacted=False) + if kind == "unit_new" or kind == "unit_removed": + packet.update(member="UnitNew" if kind == "unit_new" else "UnitRemoved", signature="so", payload=(self.name, self.path)) + elif kind == "job_new" or kind == "job_removed": + packet.update(member="JobNew" if kind == "job_new" else "JobRemoved", signature="uos" if kind == "job_new" else "uoss", payload=(17, self.job, self.name) if kind == "job_new" else (17, self.job, self.name, "done")) + elif kind == "unit_properties": + packet.update(path=self.path, interface="org.freedesktop.DBus.Properties", member="PropertiesChanged", signature="sa{sv}as", payload=("org.freedesktop.systemd1.Unit", (self.api.StartupProperty("Job", "(uo)", (17, self.job)), self.api.StartupProperty("InvocationID", "ay", self.invocation)), ())) + elif kind == "create_return" or kind == "create_error": + packet.update(destination=":1.42", reply_cookie=42, path=None, interface=None, member=None, signature="o" if kind == "create_return" else "s", payload=(self.job,) if kind == "create_return" else (), error_name=None if kind == "create_return" else "org.freedesktop.systemd1.UnitExists", error_body_redacted=kind == "create_error") + packet.update(changes) + return self.api.parse_startup_envelope(packet) + + def advance(self, state: object, *envelopes: object) -> object: + for envelope in envelopes: + state = self.api.advance_startup_correlation(state, envelope, now=2.0) + return state + + def correlated(self, *, done: bool = False) -> object: + kinds = ["unit_new", "job_new", "unit_properties", "create_return"] + if done: + kinds.append("job_removed") + return self.advance(self.state(), *(self.envelope(kind, index) for index, kind in enumerate(kinds, 1))) + + def assert_refuses(self, callback: object) -> None: + with self.assertRaises(self.api.EnforcementUnavailable) as caught: + callback() + self.assertEqual(str(caught.exception), "Invalid bounded startup correlation evidence") + + def test_public_fields_required_frozen_slots(self) -> None: + for name in PUBLIC_CLASSES: + with self.subTest(name=name): + cls = getattr(self.api, name) + self.assertTrue(cls.__dataclass_params__.frozen) + self.assertTrue(hasattr(cls, "__slots__")) + self.assertTrue(all(parameter.default is inspect.Parameter.empty for parameter in inspect.signature(cls).parameters.values())) + self.assertFalse(hasattr(self.context, "__dict__")) + + def test_subtype_construction_refuses(self) -> None: + class Derived(self.api.StartupBusContext): + pass + self.assert_refuses(lambda: Derived(1, "a" * 32, ":1.7", ":1.42", 1000)) + + def test_context_boundaries(self) -> None: + for changes in ({"version": True}, {"bus_id": "A" * 32}, {"manager_unique_name": "org.systemd"}, {"client_unique_name": ":1.7"}, {"manager_uid": True}, {"manager_uid": 2**32 - 1}): + with self.subTest(changes=tuple(changes)): + self.assert_refuses(lambda: replace(self.context, **changes)) + + def test_metadata_context_independent_hash(self) -> None: + other = replace(self.context, manager_unique_name=":1.9", client_unique_name=":1.10") + first = self.api.new_startup_correlation(self.context, self.metadata, deadline=10.0, now=1.0) + second = self.api.new_startup_correlation(other, self.metadata, deadline=10.0, now=1.0) + self.assertEqual(first.redacted_request_metadata_sha256, second.redacted_request_metadata_sha256) + self.assertNotEqual(first.context, second.context) + + def test_metadata_sha_domain_projection(self) -> None: + projection = {"domain": "truememory.startup.redacted_create_structure.v1", "version": 1, "path": "/org/freedesktop/systemd1", "interface": "org.freedesktop.systemd1.Manager", "member": "StartTransientUnit", "signature": "ssa(sv)a(sa(sv))", "unit_name": self.name, "unit_path": self.path, "job_mode": "fail", "property_signatures": self.metadata.property_signatures, "environment_names": self.metadata.environment_names, "auxiliary_units": ()} + raw = json.dumps(projection, sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False).encode() + self.assertEqual(self.api.redacted_create_metadata_sha256(self.metadata), hashlib.sha256(raw).hexdigest()) + self.assertNotIn("destination", projection) + self.assertNotIn("environment_values", {field.name for field in fields(self.metadata)}) + + def test_metadata_sort_uniqueness_and_values(self) -> None: + for changes in ({"job_mode": "replace"}, {"auxiliary_units": (("x",),)}, {"property_signatures": (("X", "s"), ("X", "s"))}, {"property_signatures": (("Z", "s"), ("A", "s"))}, {"environment_names": ("PYTHONPATH", "LANG")}, {"environment_names": ("LANG", "LANG")}, {"environment_names": ("SECRET=value",)}, {"property_signatures": (("Environment", "s"),)}): + with self.subTest(field=tuple(changes)): + self.assert_refuses(lambda: replace(self.metadata, **changes)) + + def test_unit_path_original_escaping(self) -> None: + value = self.api.StartupCreateStructure(1, "1_a-b.service", "/org/freedesktop/systemd1/unit/_31_5fa_2db_2eservice", "fail", (), (), ()) + self.assertEqual(value.unit_name, "1_a-b.service") + self.assert_refuses(lambda: replace(value, unit_path=self.path)) + + def test_metadata_reflective_mutation_revalidated(self) -> None: + object.__setattr__(self.metadata, "auxiliary_units", ["secret"]) + self.assert_refuses(lambda: self.api.redacted_create_metadata_sha256(self.metadata)) + + def test_metadata_environment_names_not_shell_identifiers(self) -> None: + metadata = replace(self.metadata, environment_names=("1-NAME", "unicode-é")) + self.assertEqual(metadata.environment_names, ("1-NAME", "unicode-é")) + + def test_variants_exact_immutable(self) -> None: + good = (("s", ""), ("b", False), ("u", 2**32 - 1), ("t", 2**64 - 1), ("ay", b"x"), ("as", ("a", "b")), ("(uo)", (0, "/")), ("(uo)", (17, "/other_object"))) + for signature, value in good: + with self.subTest(signature=signature): + self.assertEqual(self.api.StartupProperty("Extra", signature, value).value, value) + bad = (("u", True), ("t", 2**64), ("ay", bytearray(b"x")), ("as", ["x"]), ("b", 1), ("s", object()), ("a{sv}", ()), ("(uo)", (17, "/bad-path"))) + for signature, value in bad: + with self.subTest(signature=signature): + self.assert_refuses(lambda: self.api.StartupProperty("Extra", signature, value)) + + def test_all_native21_fields_and_invalidations(self) -> None: + a = self.api + changed = tuple(a.StartupProperty(name, signature, (17, self.job) if name == "Job" else self.invocation if signature == "ay" else "active" if signature == "s" else True if signature == "b" else 7) for name, signature in a._STARTUP_NATIVE_PROPERTIES) + self.assertEqual(len(changed), 21) + packet = self.envelope("unit_properties", 3, payload=("org.freedesktop.systemd1.Unit", changed, ("Conditions", "Asserts"))) + state = self.advance(self.state(), self.envelope("unit_new", 1), self.envelope("job_new", 2), packet, self.envelope("create_return", 4)) + self.assertEqual(state.phase, "invocation_observed") + + def test_invocation_empty_and_zero_valid_nonbinding(self) -> None: + for value in (b"", bytes(16)): + with self.subTest(length=len(value)): + packet = self.envelope("unit_properties", 3, payload=("org.freedesktop.systemd1.Unit", (self.api.StartupProperty("Job", "(uo)", (17, self.job)), self.api.StartupProperty("InvocationID", "ay", value)), ())) + state = self.advance(self.state(), self.envelope("unit_new", 1), self.envelope("job_new", 2), packet, self.envelope("create_return", 4)) + self.assertEqual(state.phase, "reply_bound_pending_invocation") + self.assertIsNone(self.api.startup_correlation_binding(state)) + + def test_invocation_invalid_lengths(self) -> None: + for size in (1, 15, 17, 4097): + with self.subTest(size=size): + self.assert_refuses(lambda: self.api.StartupProperty("InvocationID", "ay", b"x" * size)) + + def test_native_name_signature_constraints(self) -> None: + for name, signature, value in (("Job", "s", "17"), ("InvocationID", "s", "secret"), ("ActiveState", "u", 0)): + self.assert_refuses(lambda: self.api.StartupProperty(name, signature, value)) + + def test_property_per_use_reflective_mutation(self) -> None: + value = self.api.StartupProperty("Extra", "s", "valid") + object.__setattr__(value, "value", ["secret"]) + self.assert_refuses(lambda: self.envelope("unit_properties", 1, payload=("org.freedesktop.systemd1.Unit", (value,), ()))) + + def test_plain_dict_parser_only_precise_keys(self) -> None: + packet = self.envelope("unit_new", 1) + mapping = {field.name: getattr(packet, field.name) for field in fields(packet)} + self.assertEqual(self.api.parse_startup_envelope(mapping), packet) + for bad in ({**mapping, "environment_values": "secret"}, {key: value for key, value in mapping.items() if key != "version"}, list(mapping.items())): + self.assert_refuses(lambda: self.api.parse_startup_envelope(bad)) + class DerivedDict(dict): + pass + self.assert_refuses(lambda: self.api.parse_startup_envelope(DerivedDict(mapping))) + + def test_all_seven_envelope_kinds(self) -> None: + for kind in ("create_return", "create_error", "unit_new", "unit_removed", "job_new", "job_removed", "unit_properties"): + with self.subTest(kind=kind): + self.assertEqual(self.envelope(kind, 1).kind, kind) + + def test_envelope_strict_scalars_and_headers(self) -> None: + for changes in ({"version": True}, {"serial": True}, {"ordinal": 0}, {"serial": 2**32}, {"ordinal": 2**64}, {"member": "JobNew"}, {"interface": "org.freedesktop.systemd1.Unit"}, {"signature": "s"}, {"reply_cookie": 1}, {"error_body_redacted": 0}, {"payload": [self.name, self.path]}): + with self.subTest(field=tuple(changes)): + self.assert_refuses(lambda: self.envelope("unit_new", 1, **changes)) + + def test_create_reply_canonical_job_path(self) -> None: + for path in ("/org/freedesktop/systemd1/job/017", "/org/freedesktop/systemd1/job/0", "/org/freedesktop/systemd1/job/4294967296", "/foreign/17"): + self.assert_refuses(lambda: self.envelope("create_return", 1, payload=(path,))) + + def test_error_body_never_admitted(self) -> None: + self.assert_refuses(lambda: self.envelope("create_error", 1, payload=("synthetic-secret",))) + self.assert_refuses(lambda: self.envelope("create_error", 1, error_body_redacted=False)) + value = self.envelope("create_error", 1) + self.assertEqual(value.payload, ()) + self.assertEqual(len(self.api.startup_envelope_evidence_sha256(value)), 64) + + def test_properties_duplicate_changed_and_invalidated(self) -> None: + value = self.api.StartupProperty("Extra", "s", "x") + for changed, invalidated in (((value, value), ()), ((), ("Job", "Job"))): + self.assert_refuses(lambda: self.envelope("unit_properties", 1, payload=("org.freedesktop.systemd1.Unit", changed, invalidated))) + + def test_subtype_packet_not_selected_generic(self) -> None: + self.assert_refuses(lambda: self.envelope("unit_properties", 1, payload=("org.freedesktop.systemd1.Service", (), ()))) + + def test_evidence_digest_native_header_sensitive(self) -> None: + first = self.envelope("unit_new", 1) + second = replace(first, serial=200) + self.assertNotEqual(self.api.startup_envelope_evidence_sha256(first), self.api.startup_envelope_evidence_sha256(second)) + self.assertEqual(self.api.startup_envelope_evidence_sha256(first), self.api.startup_envelope_evidence_sha256(first)) + + def test_evidence_digest_tagged_bytes(self) -> None: + packet = self.envelope("unit_properties", 1) + projection = self.api._startup_envelope_projection(packet) + self.assertEqual(projection["domain"], "truememory.startup.typed_evidence.v1") + self.assertEqual(projection["payload"][1][1]["value"], {"type": "ay", "hex": self.invocation.hex()}) + + def test_success_pending_then_done_binding_fields(self) -> None: + state = self.correlated() + binding = self.api.startup_correlation_binding(state) + self.assertEqual(state.phase, "invocation_observed") + self.assertEqual(binding.context, self.context) + self.assertEqual((binding.job_id, binding.job_path, binding.invocation_id), (17, self.job, self.invocation)) + self.assertIsNone(binding.job_result) + state = self.advance(state, self.envelope("job_removed", 5)) + binding = self.api.startup_correlation_binding(state) + self.assertEqual(state.phase, "correlated_done") + self.assertEqual(binding.job_result, "done") + for field in ("reply_evidence_sha256", "job_new_evidence_sha256", "joint_evidence_sha256", "unit_new_evidence_sha256", "job_removed_evidence_sha256"): + self.assertEqual(len(getattr(binding, field)), 64) + self.assertFalse(hasattr(binding, "pid")) + + def test_all_early_packets_before_cookie(self) -> None: + state = self.advance(self.state(cookie=False), *(self.envelope(kind, index) for index, kind in enumerate(("unit_new", "job_new", "unit_properties", "job_removed", "create_return"), 1))) + self.assertEqual(state.phase, "attempted_unknown") + self.assertIsNone(self.api.startup_correlation_binding(state)) + state = self.api.bind_startup_request_cookie(state, cookie=42, now=2.1) + self.assertEqual(state.phase, "correlated_done") + + def test_send_attempt_exactly_once(self) -> None: + state = self.state(cookie=False) + self.assert_refuses(lambda: self.api.mark_startup_send_attempt(state, now=2.0)) + self.assertEqual(state.phase, "attempted_unknown") + + def test_cookie_repeat_and_conflict(self) -> None: + state = self.api.bind_startup_request_cookie(self.state(), cookie=42, now=2.0) + self.assertEqual(state.request_cookie, 42) + state = self.api.bind_startup_request_cookie(state, cookie=43, now=2.1) + self.assertEqual((state.phase, state.reason), ("unknown", "conflicting_cookie")) + + def test_cookie_and_manager_serial_independent(self) -> None: + state = self.advance(self.state(), self.envelope("unit_new", 1, serial=42), self.envelope("job_new", 2, serial=1000), self.envelope("unit_properties", 3, serial=1500), self.envelope("create_return", 4, serial=2000)) + self.assertEqual(state.phase, "invocation_observed") + + def test_preexisting_unit_does_not_bind(self) -> None: + state = self.api.new_startup_correlation(self.context, self.metadata, deadline=10.0, now=1.0) + state = self.advance(state, self.envelope("unit_new", 1)) + self.assertEqual((state.phase, state.reason), ("unknown", "preexisting_unit")) + self.assertIsNone(self.api.startup_correlation_binding(state)) + + def test_missing_unit_new_is_not_fresh(self) -> None: + state = self.advance(self.state(), self.envelope("job_new", 1)) + self.assertEqual(state.phase, "unknown") + + def test_replacement_unit_new_and_removal_unknown(self) -> None: + for kind in ("unit_new", "unit_removed"): + with self.subTest(kind=kind): + state = self.advance(self.correlated(), self.envelope(kind, 5)) + self.assertEqual((state.phase, state.reason), ("unknown", "conflicting_unit")) + self.assertIsNone(self.api.startup_correlation_binding(state)) + + def test_foreign_sender_and_destination_unknown(self) -> None: + for packet, reason in ((self.envelope("unit_new", 1, sender=":1.99"), "manager_owner_changed"), (self.envelope("create_return", 1, destination=":1.99"), "conflicting_reply")): + state = self.advance(self.state(), packet) + self.assertEqual((state.phase, state.reason), ("unknown", reason)) + + def test_wrong_reply_cookie_unknown(self) -> None: + state = self.advance(self.state(), self.envelope("create_return", 1, reply_cookie=43)) + self.assertEqual(state.reason, "conflicting_reply") + + def test_local_ordinal_gap_unknown(self) -> None: + state = self.advance(self.state(), self.envelope("unit_new", 2)) + self.assertEqual(state.reason, "transport_gap") + + def test_native_serial_replay_even_identical_payload(self) -> None: + state = self.advance(self.state(), self.envelope("unit_new", 1, serial=101)) + state = self.advance(state, self.envelope("unit_new", 2, serial=101)) + self.assertEqual(state.reason, "replayed_native_serial") + self.assertEqual(len(state.envelopes), 1) + + def test_separate_job_and_invocation_messages_do_not_join(self) -> None: + a = self.api + first = self.envelope("unit_properties", 3, payload=("org.freedesktop.systemd1.Unit", (a.StartupProperty("Job", "(uo)", (17, self.job)),), ())) + second = self.envelope("unit_properties", 4, payload=("org.freedesktop.systemd1.Unit", (a.StartupProperty("InvocationID", "ay", self.invocation),), ())) + state = self.advance(self.state(), self.envelope("unit_new", 1), self.envelope("job_new", 2), first, second, self.envelope("create_return", 5)) + self.assertEqual(state.phase, "reply_bound_pending_invocation") + self.assertIsNone(a.startup_correlation_binding(state)) + + def test_invalidated_required_property_nonbinding(self) -> None: + packet = self.envelope("unit_properties", 3, payload=("org.freedesktop.systemd1.Unit", (self.api.StartupProperty("Job", "(uo)", (17, self.job)),), ("InvocationID",))) + state = self.advance(self.state(), self.envelope("unit_new", 1), self.envelope("job_new", 2), packet, self.envelope("create_return", 4)) + self.assertIsNone(self.api.startup_correlation_binding(state)) + + def test_changed_joint_with_invalidation_is_nonbinding(self) -> None: + packet = self.envelope("unit_properties", 3) + packet = replace(packet, payload=(packet.payload[0], packet.payload[1], ("Job",))) + state = self.advance(self.state(), self.envelope("unit_new", 1), self.envelope("job_new", 2), packet, self.envelope("create_return", 4)) + self.assertEqual(state.phase, "reply_bound_pending_invocation") + self.assertIsNone(self.api.startup_correlation_binding(state)) + + def test_done_without_joint_stays_pending(self) -> None: + state = self.advance(self.state(), self.envelope("unit_new", 1), self.envelope("job_new", 2), self.envelope("job_removed", 3), self.envelope("create_return", 4)) + self.assertEqual(state.phase, "reply_bound_pending_invocation") + self.assertIsNone(self.api.startup_correlation_binding(state)) + + def test_positive_job_conflict_unknown(self) -> None: + other = "/org/freedesktop/systemd1/job/18" + packet = self.envelope("unit_properties", 5, payload=("org.freedesktop.systemd1.Unit", (self.api.StartupProperty("Job", "(uo)", (18, other)), self.api.StartupProperty("InvocationID", "ay", self.invocation)), ())) + self.assertEqual(self.advance(self.correlated(), packet).reason, "conflicting_job") + + def test_invocation_replacement_unknown(self) -> None: + packet = self.envelope("unit_properties", 5, payload=("org.freedesktop.systemd1.Unit", (self.api.StartupProperty("Job", "(uo)", (17, self.job)), self.api.StartupProperty("InvocationID", "ay", b"z" * 16)), ())) + self.assertEqual(self.advance(self.correlated(), packet).reason, "ambiguous_invocation") + + def test_later_zero_job_preserves_historical_joint_only(self) -> None: + packet = self.envelope("unit_properties", 5, payload=("org.freedesktop.systemd1.Unit", (self.api.StartupProperty("Job", "(uo)", (0, "/")),), ())) + state = self.advance(self.correlated(), packet) + self.assertEqual(state.phase, "invocation_observed") + self.assertEqual(self.api.startup_correlation_binding(state).invocation_id, self.invocation) + + def test_manager_error_and_non_done_are_unknown(self) -> None: + self.assertEqual(self.advance(self.state(), self.envelope("create_error", 1)).reason, "manager_error_unknown") + packet = self.envelope("job_removed", 5, payload=(17, self.job, self.name, "failed")) + self.assertEqual(self.advance(self.correlated(), packet).reason, "job_outcome_non_done") + + def test_time_exact_nonnegative_finite_floats(self) -> None: + for value in (1, True, -1.0, float("inf"), float("nan")): + self.assert_refuses(lambda: self.api.new_startup_correlation(self.context, self.metadata, deadline=10.0, now=value)) + self.assert_refuses(lambda: self.api.expire_startup_correlation(self.state(), now=value)) + self.assert_refuses(lambda: self.api.new_startup_correlation(self.context, self.metadata, deadline=1.0, now=1.0)) + + def test_time_backward_refusal_preserves_state(self) -> None: + state = self.state() + self.assert_refuses(lambda: self.api.expire_startup_correlation(state, now=1.0)) + self.assertEqual(state.last_now, 1.2) + + def test_expire_at_exact_original_deadline(self) -> None: + prepared = self.api.new_startup_correlation(self.context, self.metadata, deadline=10.0, now=1.0) + for state, reason in ((prepared, "expired_unsent"), (self.correlated(), "expired_unknown")): + expired = self.api.expire_startup_correlation(state, now=10.0) + self.assertEqual(expired.reason, reason) + self.assertEqual(expired.original_deadline, 10.0) + self.assertIsNone(self.api.startup_correlation_binding(expired)) + + def test_sticky_terminal_state_no_regain(self) -> None: + state = self.api.invalidate_startup_correlation(self.correlated(), reason="transport_disconnected", now=3.0) + prior = state.envelopes + state = self.api.bind_startup_request_cookie(state, cookie=42, now=4.0) + state = self.api.advance_startup_correlation(state, self.envelope("job_removed", 5), now=5.0) + self.assertEqual(state.reason, "transport_disconnected") + self.assertEqual(state.envelopes, prior) + self.assertIsNone(self.api.startup_correlation_binding(state)) + + def test_fixed_reasons_only_no_secret_echo(self) -> None: + self.assert_refuses(lambda: self.api.invalidate_startup_correlation(self.state(), reason="synthetic-secret", now=2.0)) + self.assert_refuses(lambda: self.envelope("unit_new", 1, sender="synthetic-secret")) + + def test_count_overflow_retains_preceding_stream(self) -> None: + state = self.state() + for index in range(1, 65): + state = self.advance(state, self.envelope("unit_properties", index, payload=("org.freedesktop.systemd1.Unit", (), ()))) + self.assertEqual(len(state.envelopes), 64) + previous = state.envelopes + state = self.advance(state, self.envelope("unit_properties", 65, payload=("org.freedesktop.systemd1.Unit", (), ()))) + self.assertEqual(state.reason, "evidence_overflow") + self.assertIs(state.envelopes, previous) + + def test_single_canonical_overflow_does_not_retain_or_hash(self) -> None: + properties = tuple(self.api.StartupProperty("Field" + str(index), "s", "x" * 1024) for index in range(32)) + packet = self.envelope("unit_properties", 1, payload=("org.freedesktop.systemd1.Unit", properties, ())) + state = self.advance(self.state(), packet) + self.assertEqual(state.reason, "evidence_overflow") + self.assertEqual(state.envelopes, ()) + self.assertEqual(state.canonical_evidence_bytes, 0) + self.assert_refuses(lambda: self.api.startup_envelope_evidence_sha256(packet)) + + def test_aggregate_canonical_overflow_no_eviction(self) -> None: + state = self.state() + previous = state + for index in range(1, 20): + packet = self.envelope("unit_properties", index, payload=("org.freedesktop.systemd1.Unit", (self.api.StartupProperty("Large", "s", "x" * 1024),), ())) + previous = state + state = self.advance(state, packet) + if state.phase == "unknown": + break + if state.phase != "unknown": + for index in range(20, 65): + previous = state + state = self.advance(state, self.envelope("unit_properties", index, payload=("org.freedesktop.systemd1.Unit", (self.api.StartupProperty("Large", "s", "x" * 1024),), ()))) + if state.phase == "unknown": + break + self.assertEqual(state.reason, "evidence_overflow") + self.assertEqual(state.envelopes, previous.envelopes) + self.assertLessEqual(state.canonical_evidence_bytes, 65536) + + def test_precopy_container_and_utf8_bounds(self) -> None: + for value in ("x" * 1025, "é" * 513, "\ud800"): + self.assert_refuses(lambda: self.api.StartupProperty("Extra", "s", value)) + self.assert_refuses(lambda: self.api.StartupProperty("Extra", "as", tuple("x" for _ in range(65)))) + self.assert_refuses(lambda: self.envelope("unit_properties", 1, payload=("org.freedesktop.systemd1.Unit", tuple(self.api.StartupProperty("F" + str(index), "s", "") for index in range(65)), ()))) + + def test_state_phase_hash_size_mutations_refuse(self) -> None: + for field, value in (("phase", "invocation_observed"), ("canonical_evidence_bytes", 1), ("redacted_request_metadata_sha256", "b" * 64), ("request_cookie", True), ("send_attempted", 1)): + state = self.state() + object.__setattr__(state, field, value) + self.assert_refuses(lambda: self.api.startup_correlation_binding(state)) + + def test_binding_mutation_revalidated(self) -> None: + binding = self.api.startup_correlation_binding(self.correlated()) + object.__setattr__(binding, "invocation_id", bytes(16)) + self.assert_refuses(lambda: self.api._startup_validate_binding(binding)) + + def test_pure_diagnostics_do_not_authenticate_context(self) -> None: + supplied = replace(self.context, bus_id="b" * 32) + state = self.api.new_startup_correlation(supplied, self.metadata, deadline=10.0, now=1.0) + self.assertEqual(state.context.bus_id, "b" * 32) + self.assertFalse(hasattr(state, "owner")) + self.assertFalse(hasattr(state, "pidfd")) + + def test_selected_graph_no_native_or_clock_calls(self) -> None: + tree = ast.parse(SOURCE.read_text()) + chosen = [node for node in tree.body if isinstance(node, (ast.FunctionDef, ast.ClassDef)) and (getattr(node, "name", "").startswith("_startup_") or getattr(node, "name", "") in PUBLIC_CLASSES + PUBLIC_FUNCTIONS)] + forbidden = {"time", "os", "sys", "Path", "ctypes", "importlib", "socket", "signal", "select", "fcntl", "subprocess", "threading", "open", "exec", "eval", "__import__", "get_linux_adapter", "launch_owned_linux", "capture_owned_exit", "stop_owned_exact", "terminate_owned_exact"} + referenced = {node.id for root in chosen for node in ast.walk(root) if isinstance(node, ast.Name)} + self.assertEqual(referenced.intersection(forbidden), set()) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-linux-startup-owner-297.py b/tests/test-linux-startup-owner-297.py new file mode 100644 index 00000000..7d34badb --- /dev/null +++ b/tests/test-linux-startup-owner-297.py @@ -0,0 +1,424 @@ +"""Synthetic S1 retention controls; no application or native launcher import.""" +from __future__ import annotations + +import ast +from dataclasses import dataclass, fields, FrozenInstanceError, replace +import hashlib +import inspect +import json +import math +from pathlib import Path +import re +import sys +import threading +import types +import unittest + + +SOURCE = Path(__file__).resolve().parents[1] / "truememory_memory_bootstrap.py" +S0_CLASSES = ("StartupCreateStructure", "StartupBusContext", "StartupProperty", "StartupMessageEnvelope", "StartupCorrelationState", "StartupInvocationBinding") +S0_FUNCTIONS = ("redacted_create_metadata_sha256", "new_startup_correlation", "mark_startup_send_attempt", "bind_startup_request_cookie", "parse_startup_envelope", "startup_envelope_evidence_sha256", "advance_startup_correlation", "invalidate_startup_correlation", "expire_startup_correlation", "startup_correlation_binding") +S1_CLASSES = ("StartupRetentionRequest", "StartupRetentionObservation", "StartupRetentionOwner", "StartupRetentionSnapshot") +S1_FUNCTIONS = ("prepare_startup_retention", "startup_retention_snapshot", "publish_startup_retention", "invalidate_startup_retention", "cancel_unsent_startup_retention", "release_unsent_startup_retention") + + +def load_pure_source() -> types.ModuleType: + selected = [] + for node in ast.parse(SOURCE.read_text()).body: + if isinstance(node, ast.Assign) and all(isinstance(target, ast.Name) and target.id.startswith(("_STARTUP_", "_RETENTION_")) for target in node.targets): + selected.append(node) + elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name) and node.target.id.startswith("_RETENTION_"): + selected.append(node) + elif isinstance(node, ast.ClassDef) and node.name in S0_CLASSES + S1_CLASSES + ("EnforcementUnavailable", "_RetentionSeal", "_RetentionRecord"): + selected.append(node) + elif isinstance(node, ast.FunctionDef) and (node.name in S0_FUNCTIONS + S1_FUNCTIONS or node.name.startswith(("_startup_", "_retention_"))): + selected.append(node) + module = types.ModuleType("synthetic_startup_retention_297") + module.__dict__.update({"dataclass": dataclass, "hashlib": hashlib, "json": json, "math": math, + "re": re, "os": types.SimpleNamespace(getpid=lambda: 1000), "threading": threading}) + sys.modules[module.__name__] = module + body = [ast.ImportFrom(module="__future__", names=[ast.alias(name="annotations")], level=0)] + selected + exec(compile(ast.fix_missing_locations(ast.Module(body=body, type_ignores=[])), str(SOURCE), "exec"), module.__dict__) + return module + + +class StartupRetentionTests(unittest.TestCase): + def setUp(self) -> None: + self.api = load_pure_source() + self.context = self.api.StartupBusContext(1, "a" * 32, ":1.7", ":1.42", 1000) + self.request = self.make_request(1) + + def make_request(self, number: int) -> object: + a = self.api + generation = f"{number:032x}" + metadata = [] + for stage in ("slice", "service"): + name = ("app-truememory" if stage == "slice" else "truememory-memory") + generation + "." + stage + metadata.append(a.StartupCreateStructure(1, name, a._startup_unit_path(name), "fail", + (("Environment", "as"), ("MemoryMax", "t")), ("LANG", "PYTHONPATH"), ())) + return a.StartupRetentionRequest(1, generation, 268435456, 0, *metadata) + + def prepare(self) -> object: + return self.api.prepare_startup_retention(self.request, deadline=10.0, now=1.0) + + def observation(self, stage: str = "slice", state: object = None) -> object: + return self.api.StartupRetentionObservation(1, stage, self.context, state, None) + + def state(self, *, stage: str = "slice", attempted: bool = False, cookie: bool = False) -> object: + a = self.api + state = a.new_startup_correlation(self.context, getattr(self.request, stage + "_metadata"), deadline=10.0, now=1.1) + if attempted: + state = a.mark_startup_send_attempt(state, now=1.2) + return a.bind_startup_request_cookie(state, cookie=42, now=1.3) if cookie else state + + def envelope(self, state: object, ordinal: int = 1) -> object: + return self.api.StartupMessageEnvelope(1, ordinal, "unit_new", ":1.7", None, ordinal + 100, None, + "/org/freedesktop/systemd1", "org.freedesktop.systemd1.Manager", "UnitNew", "so", + (state.metadata.unit_name, state.metadata.unit_path), None, False) + + def assert_refuses(self, action: object, message: str = "Invalid startup retention input") -> None: + with self.assertRaises(self.api.EnforcementUnavailable) as caught: + action() + self.assertEqual(str(caught.exception), message) + self.assertTrue(caught.exception.__suppress_context__) + + def test_public_records_frozen_slots_required_fields(self) -> None: + expected = { + "StartupRetentionRequest": ("version", "generation", "requested_cap_bytes", "requested_swap_bytes", "slice_metadata", "service_metadata"), + "StartupRetentionObservation": ("version", "stage", "context", "correlation", "reason"), + "StartupRetentionOwner": ("version", "owner_id", "creator_pid"), + "StartupRetentionSnapshot": ("version", "owner", "request", "original_deadline", "last_now", "operation_sequence", "phase", "slice_context", "slice_correlation", "service_context", "service_correlation", "slice_attempted", "service_attempted", "reason"), + } + for name, order in expected.items(): + with self.subTest(name=name): + cls = getattr(self.api, name) + self.assertEqual(tuple(field.name for field in fields(cls)), order) + self.assertTrue(cls.__dataclass_params__.frozen) + self.assertTrue(hasattr(cls, "__slots__")) + self.assertTrue(all(p.default is inspect.Parameter.empty for p in inspect.signature(cls).parameters.values())) + self.assertFalse(hasattr(self.request, "__dict__")) + with self.assertRaises(FrozenInstanceError): + self.request.version = 2 + + def test_constructor_subtypes_refuse(self) -> None: + class Derived(self.api.StartupRetentionRequest): + pass + self.assert_refuses(lambda: Derived(*(getattr(self.request, f.name) for f in fields(self.request)))) + + def test_omitted_extra_constructor_fields_are_typeerror(self) -> None: + with self.assertRaises(TypeError): + self.api.StartupRetentionOwner() + with self.assertRaises(TypeError): + self.api.StartupRetentionOwner(1, 1, 1000, extra=True) + + def test_request_scalar_boundaries(self) -> None: + for changes in ({"version": True}, {"generation": "A" * 32}, {"generation": "a" * 31}, + {"requested_cap_bytes": True}, {"requested_cap_bytes": 0}, {"requested_cap_bytes": 2**63}, + {"requested_swap_bytes": True}, {"requested_swap_bytes": 1}): + with self.subTest(changes=changes): + self.assert_refuses(lambda: replace(self.request, **changes)) + self.assertEqual(replace(self.request, requested_cap_bytes=2**63 - 1).requested_cap_bytes, 2**63 - 1) + + def test_custom_scalar_equality_types_refuse(self) -> None: + class Custom(str): + pass + self.assert_refuses(lambda: replace(self.request, generation=Custom(self.request.generation))) + self.assert_refuses(lambda: self.api.StartupRetentionObservation(1, Custom("slice"), self.context, None, None)) + + def test_request_stage_metadata_matches_generation(self) -> None: + self.assert_refuses(lambda: replace(self.request, slice_metadata=self.request.service_metadata)) + self.assert_refuses(lambda: replace(self.request, service_metadata=self.make_request(2).service_metadata)) + + def test_canonical_metadata_bound_is_enforced(self) -> None: + large = replace(self.request.service_metadata, environment_names=tuple(f"N{n:03d}" + "x" * 240 for n in range(100))) + self.assert_refuses(lambda: replace(self.request, service_metadata=large)) + + def test_request_hash_names_only_domain(self) -> None: + a = self.api + projection = {"domain": "truememory.startup.retention_request.v1", "version": 1, + "generation": self.request.generation, "requested_cap_bytes": 268435456, "requested_swap_bytes": 0, + "slice_metadata_sha256": a.redacted_create_metadata_sha256(self.request.slice_metadata), + "service_metadata_sha256": a.redacted_create_metadata_sha256(self.request.service_metadata)} + raw = json.dumps(projection, sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False).encode() + self.assertEqual(a._retention_request_sha256(self.request), hashlib.sha256(raw).hexdigest()) + self.assertNotIn("environment_values", {f.name for f in fields(self.request)}) + + def test_prepare_and_read_initial_snapshot(self) -> None: + owner = self.prepare() + row = self.api.startup_retention_snapshot(owner, now=1.5) + self.assertIs(row.owner, owner) + self.assertEqual((row.phase, row.operation_sequence, row.last_now, row.original_deadline), ("prepared", 0, 1.5, 10.0)) + self.assertIsNot(row.request, self.request) + self.assertIsNone(row.reason) + + def test_prepare_finite_exact_float_times(self) -> None: + for changes in ({"now": 1}, {"now": True}, {"now": -1.0}, {"now": math.nan}, + {"deadline": 10}, {"deadline": math.inf}, {"deadline": 1.0}, {"deadline": 0.0}): + kwargs = dict(deadline=10.0, now=1.0) + kwargs.update(changes) + with self.subTest(changes=tuple(changes)): + self.assert_refuses(lambda: self.api.prepare_startup_retention(self.request, **kwargs)) + self.assertEqual(self.api._RETENTION_RECORDS, {}) + + def test_registry_capacity_and_duplicate_generation(self) -> None: + owner = self.prepare() + self.assert_refuses(lambda: self.prepare(), "Startup retention generation already retained") + for number in range(2, 17): + self.api.prepare_startup_retention(self.make_request(number), deadline=10.0, now=1.0) + self.assert_refuses(lambda: self.api.prepare_startup_retention(self.make_request(17), deadline=10.0, now=1.0), "Startup retention registry full") + self.assertEqual(len(self.api._RETENTION_RECORDS), 16) + self.assertIs(self.api._RETENTION_RECORDS[owner.owner_id].seal.owner, owner) + + def test_owner_ids_do_not_reuse_released_slot(self) -> None: + owner = self.prepare() + self.api.cancel_unsent_startup_retention(owner, expected_sequence=0, now=2.0) + self.api.release_unsent_startup_retention(owner, expected_sequence=1, now=2.1) + other = self.prepare() + self.assertEqual(other.owner_id, owner.owner_id + 1) + + def test_identifier_exhaustion_refuses_unchanged(self) -> None: + self.api._RETENTION_NEXT_OWNER_ID = 2**64 - 1 + self.assert_refuses(lambda: self.prepare(), "Startup retention identifier exhausted") + self.assertEqual(self.api._RETENTION_RECORDS, {}) + + def test_clone_and_unregistered_owner_are_stale(self) -> None: + owner = self.prepare() + clone = replace(owner) + self.assert_refuses(lambda: self.api.startup_retention_snapshot(clone, now=2.0), "Startup retention owner is stale") + self.assert_refuses(lambda: self.api.startup_retention_snapshot(self.api.StartupRetentionOwner(1, 99, 1000), now=2.0), "Startup retention owner is stale") + + def test_registered_handle_scalar_mutation_refuses(self) -> None: + owner = self.prepare() + object.__setattr__(owner, "owner_id", owner.owner_id + 1) + self.assert_refuses(lambda: self.api.startup_retention_snapshot(owner, now=2.0), "Startup retention invariant changed") + + def test_request_and_snapshot_mutations_do_not_renew_private_seal(self) -> None: + owner = self.prepare() + row = self.api.startup_retention_snapshot(owner, now=2.0) + object.__setattr__(self.request, "requested_cap_bytes", 1) + object.__setattr__(row, "original_deadline", 100.0) + object.__setattr__(row.request, "requested_cap_bytes", 2) + actual = self.api.startup_retention_snapshot(owner, now=10.0) + self.assertEqual((actual.original_deadline, actual.request.requested_cap_bytes, actual.phase), (10.0, 268435456, "expired_unsent")) + + def test_context_only_cross_stage_agreement(self) -> None: + owner = self.prepare() + first = self.api.publish_startup_retention(owner, self.observation(), expected_sequence=0, now=2.0) + second = self.api.publish_startup_retention(owner, self.observation("service"), expected_sequence=1, now=2.1) + self.assertEqual((first.phase, second.operation_sequence), ("local_observed", 2)) + self.assertEqual(second.slice_context, second.service_context) + self.assertIsNot(second.slice_context, self.context) + + def test_conflicting_context_refuses_without_time_commit(self) -> None: + owner = self.prepare() + self.api.publish_startup_retention(owner, self.observation(), expected_sequence=0, now=2.0) + before = self.api._RETENTION_RECORDS[owner.owner_id] + other = replace(self.context, client_unique_name=":1.99") + observation = self.api.StartupRetentionObservation(1, "service", other, None, None) + self.assert_refuses(lambda: self.api.publish_startup_retention(owner, observation, expected_sequence=1, now=3.0)) + self.assertIs(self.api._RETENTION_RECORDS[owner.owner_id], before) + + def test_observation_only_allowed_shapes(self) -> None: + for values in ((1, "slice", None, None, None), (True, "slice", self.context, None, None), + (1, "other", self.context, None, None), (1, "slice", self.context, None, "transport_gap"), + (1, "slice", None, None, "expired_unsent")): + with self.subTest(values=values[:2]): + self.assert_refuses(lambda: self.api.StartupRetentionObservation(*values)) + + def test_matching_correlation_attempt_latches(self) -> None: + owner = self.prepare() + state = self.state(attempted=True, cookie=True) + row = self.api.publish_startup_retention(owner, self.observation(state=state), expected_sequence=0, now=2.0) + self.assertTrue(row.slice_attempted) + self.assertFalse(row.service_attempted) + self.assertIsNot(row.slice_correlation, state) + self.assert_refuses(lambda: self.api.cancel_unsent_startup_retention(owner, expected_sequence=1, now=3.0), "Startup retention has an attempted send") + + def test_correlation_wrong_metadata_deadline_future_refuse(self) -> None: + owner = self.prepare() + states = (self.state(stage="service"), replace(self.state(), original_deadline=11.0), replace(self.state(), last_now=3.0)) + for state in states: + with self.subTest(state=state.metadata.unit_name): + self.assert_refuses(lambda: self.api.publish_startup_retention(owner, self.observation(state=state), expected_sequence=0, now=2.0)) + + def test_prefix_cookie_time_and_attempt_rollback_refuse(self) -> None: + owner = self.prepare() + state = self.state(attempted=True, cookie=True) + progressed = self.api.advance_startup_correlation(state, self.envelope(state), now=2.0) + self.api.publish_startup_retention(owner, self.observation(state=progressed), expected_sequence=0, now=2.1) + candidates = (state, replace(progressed, request_cookie=43), self.state()) + for candidate in candidates: + with self.subTest(cookie=candidate.request_cookie): + self.assert_refuses(lambda: self.api.publish_startup_retention(owner, self.observation(state=candidate), expected_sequence=1, now=3.0)) + + def test_copied_evidence_nested_graph_is_independent(self) -> None: + owner = self.prepare() + state = self.state(attempted=True) + state = self.api.advance_startup_correlation(state, self.envelope(state), now=2.0) + row = self.api.publish_startup_retention(owner, self.observation(state=state), expected_sequence=0, now=2.1) + self.assertIsNot(row.slice_correlation.envelopes[0], state.envelopes[0]) + object.__setattr__(row.slice_correlation.envelopes[0], "sender", ":1.99") + actual = self.api.startup_retention_snapshot(owner, now=2.2) + self.assertEqual(actual.slice_correlation.envelopes[0].sender, ":1.7") + + def test_local_unknown_reason_sticky_through_cancel_release(self) -> None: + owner = self.prepare() + row = self.api.publish_startup_retention(owner, self.api.StartupRetentionObservation(1, "slice", None, None, "transport_gap"), expected_sequence=0, now=2.0) + self.assertEqual((row.phase, row.reason), ("terminal_unknown", "transport_gap")) + row = self.api.cancel_unsent_startup_retention(owner, expected_sequence=1, now=11.0) + row = self.api.release_unsent_startup_retention(owner, expected_sequence=2, now=12.0) + self.assertEqual((row.phase, row.reason, row.operation_sequence), ("released_unsent", "transport_gap", 3)) + + def test_s0_unknown_retained_attempt_cannot_release(self) -> None: + owner = self.prepare() + state = self.api.invalidate_startup_correlation(self.state(attempted=True), reason="transport_disconnected", now=2.0) + row = self.api.publish_startup_retention(owner, self.observation(state=state), expected_sequence=0, now=2.1) + self.assertEqual((row.phase, row.reason), ("terminal_unknown", "transport_disconnected")) + self.assert_refuses(lambda: self.api.release_unsent_startup_retention(owner, expected_sequence=1, now=3.0), "Startup retention has an attempted send") + self.assertEqual(len(self.api._RETENTION_RECORDS), 1) + + def test_invalidation_closed_reasons(self) -> None: + owner = self.prepare() + for reason in ("private-input-value", "expired_unknown", "cancelled_unsent", None): + with self.subTest(reason=reason): + self.assert_refuses(lambda: self.api.invalidate_startup_retention(owner, reason=reason, expected_sequence=0, now=2.0)) + row = self.api.invalidate_startup_retention(owner, reason="manager_owner_changed", expected_sequence=0, now=2.0) + self.assertEqual((row.operation_sequence, row.reason), (1, "manager_owner_changed")) + + def test_publish_invalidate_late_preserve_entire_record(self) -> None: + owner = self.prepare() + before = self.api._RETENTION_RECORDS[owner.owner_id] + self.assert_refuses(lambda: self.api.publish_startup_retention(owner, self.observation(), expected_sequence=0, now=10.0), "Startup retention is terminal") + self.assert_refuses(lambda: self.api.invalidate_startup_retention(owner, reason="transport_gap", expected_sequence=0, now=11.0), "Startup retention is terminal") + self.assertIs(self.api._RETENTION_RECORDS[owner.owner_id], before) + + def test_snapshot_inclusive_expiry_commits_once(self) -> None: + owner = self.prepare() + row = self.api.startup_retention_snapshot(owner, now=10.0) + self.assertEqual((row.phase, row.reason, row.operation_sequence), ("expired_unsent", "expired_unsent", 1)) + again = self.api.startup_retention_snapshot(owner, now=11.0) + self.assertEqual((again.phase, again.reason, again.operation_sequence, again.last_now), (row.phase, row.reason, 1, 11.0)) + + def test_attempted_snapshot_expiry_retains_capacity(self) -> None: + owner = self.prepare() + self.api.publish_startup_retention(owner, self.observation(state=self.state(attempted=True)), expected_sequence=0, now=2.0) + row = self.api.startup_retention_snapshot(owner, now=10.0) + self.assertEqual((row.phase, row.reason, row.operation_sequence), ("expired_unknown", "expired_unknown", 2)) + self.assertEqual(len(self.api._RETENTION_RECORDS), 1) + self.assert_refuses(lambda: self.api.cancel_unsent_startup_retention(owner, expected_sequence=2, now=11.0), "Startup retention has an attempted send") + + def test_cancel_after_deadline_does_not_first_expire(self) -> None: + owner = self.prepare() + row = self.api.cancel_unsent_startup_retention(owner, expected_sequence=0, now=11.0) + self.assertEqual((row.phase, row.reason, row.operation_sequence), ("cancelled_unsent", "cancelled_unsent", 1)) + before = self.api._RETENTION_RECORDS[owner.owner_id] + self.assert_refuses(lambda: self.api.cancel_unsent_startup_retention(owner, expected_sequence=1, now=12.0), "Startup retention is terminal") + self.assertIs(self.api._RETENTION_RECORDS[owner.owner_id], before) + + def test_release_expired_unsent_once_without_tombstone(self) -> None: + owner = self.prepare() + self.api.startup_retention_snapshot(owner, now=10.0) + row = self.api.release_unsent_startup_retention(owner, expected_sequence=1, now=11.0) + self.assertEqual((row.phase, row.reason, row.operation_sequence), ("released_unsent", "expired_unsent", 2)) + self.assertEqual(self.api._RETENTION_RECORDS, {}) + self.assert_refuses(lambda: self.api.release_unsent_startup_retention(owner, expected_sequence=2, now=12.0), "Startup retention owner is stale") + + def test_release_open_refuses_without_mutation(self) -> None: + owner = self.prepare() + before = self.api._RETENTION_RECORDS[owner.owner_id] + self.assert_refuses(lambda: self.api.release_unsent_startup_retention(owner, expected_sequence=0, now=11.0), "Startup retention is terminal") + self.assertIs(self.api._RETENTION_RECORDS[owner.owner_id], before) + + def test_sequence_types_stale_and_backwards_preserve(self) -> None: + owner = self.prepare() + before = self.api._RETENTION_RECORDS[owner.owner_id] + for sequence in (True, -1, 2**64): + with self.subTest(sequence=sequence): + self.assert_refuses(lambda: self.api.cancel_unsent_startup_retention(owner, expected_sequence=sequence, now=2.0)) + self.assert_refuses(lambda: self.api.cancel_unsent_startup_retention(owner, expected_sequence=1, now=2.0), "Startup retention sequence mismatch") + self.assert_refuses(lambda: self.api.startup_retention_snapshot(owner, now=0.9), "Startup retention time moved backwards") + self.assertIs(self.api._RETENTION_RECORDS[owner.owner_id], before) + + def test_sequence_exhaustion_has_no_wrap(self) -> None: + owner = self.prepare() + self.api.publish_startup_retention(owner, self.observation(), expected_sequence=0, now=2.0) + record = self.api._RETENTION_RECORDS[owner.owner_id] + object.__setattr__(record, "operation_sequence", 2**64 - 1) + self.assert_refuses(lambda: self.api.cancel_unsent_startup_retention(owner, expected_sequence=2**64 - 1, now=3.0), "Startup retention sequence exhausted") + self.assert_refuses(lambda: self.api.startup_retention_snapshot(owner, now=10.0), "Startup retention sequence exhausted") + self.assertIs(self.api._RETENTION_RECORDS[owner.owner_id], record) + + def test_pid_mismatch_refuses_before_lock(self) -> None: + class ForbiddenLock: + def acquire(self, **kwargs: object) -> bool: + raise AssertionError("inherited lock used") + self.api.os.getpid = lambda: 1001 + self.api._RETENTION_LOCK = ForbiddenLock() + self.assert_refuses(lambda: self.prepare(), "Startup retention belongs to a different process") + + def test_nonblocking_busy_preserves_record(self) -> None: + owner = self.prepare() + before = self.api._RETENTION_RECORDS[owner.owner_id] + self.api._RETENTION_BUSY = True + self.assert_refuses(lambda: self.api.startup_retention_snapshot(owner, now=2.0), "Startup retention busy") + self.assertIs(self.api._RETENTION_RECORDS[owner.owner_id], before) + + def test_reentry_during_copy_refuses_without_nested_mutation(self) -> None: + owner = self.prepare() + copy = self.api._retention_copy_request + observed = [] + def copy_with_reentry(value: object) -> object: + try: + self.api.cancel_unsent_startup_retention(owner, expected_sequence=0, now=2.0) + except self.api.EnforcementUnavailable as error: + observed.append(str(error)) + return copy(value) + self.api._retention_copy_request = copy_with_reentry + row = self.api.startup_retention_snapshot(owner, now=2.0) + self.assertEqual(observed, ["Startup retention busy"]) + self.assertEqual((row.phase, row.operation_sequence), ("prepared", 0)) + + def test_registry_binding_revalidated_after_output_allocation(self) -> None: + owner = self.prepare() + original = self.api._RETENTION_RECORDS[owner.owner_id] + replacement = replace(original) + copy = self.api._retention_copy_request + def copy_with_rebinding(value: object) -> object: + self.api._RETENTION_RECORDS[owner.owner_id] = replacement + return copy(value) + self.api._retention_copy_request = copy_with_rebinding + self.assert_refuses(lambda: self.api.startup_retention_snapshot(owner, now=2.0), "Startup retention invariant changed") + self.assertIs(self.api._RETENTION_RECORDS[owner.owner_id], replacement) + self.assertEqual((replacement.last_now, replacement.operation_sequence), (1.0, 0)) + + def test_fixed_failure_does_not_echo_private_value(self) -> None: + self.assert_refuses(lambda: self.api._retention_fail("synthetic-private-environment-value")) + + def test_registry_object_revalidated_after_cloning(self) -> None: + owner = self.prepare() + original = self.api._RETENTION_RECORDS + copy = self.api._retention_copy_request + def copy_with_registry_swap(value: object) -> object: + self.api._RETENTION_RECORDS = dict(original) + return copy(value) + self.api._retention_copy_request = copy_with_registry_swap + self.assert_refuses(lambda: self.api.startup_retention_snapshot(owner, now=2.0), "Startup retention invariant changed") + self.assertEqual(original[owner.owner_id].last_now, 1.0) + + def test_identifier_rollback_cannot_overwrite_registered_owner(self) -> None: + owner = self.prepare() + original = self.api._RETENTION_RECORDS[owner.owner_id] + self.api._RETENTION_NEXT_OWNER_ID = 0 + self.assert_refuses(lambda: self.api.prepare_startup_retention(self.make_request(2), deadline=10.0, now=1.0), "Startup retention invariant changed") + self.assertIs(self.api._RETENTION_RECORDS[owner.owner_id], original) + + def test_pure_graph_has_no_native_runtime_calls(self) -> None: + forbidden = {"open", "sleep", "monotonic", "time", "Popen", "socket", "execve", "pidfd_open", "pidfd_send_signal", "launch_owned_linux", "capture_owned_exit", "stop_owned_exact", "terminate_owned_exact"} + for node in ast.parse(SOURCE.read_text()).body: + if isinstance(node, ast.FunctionDef) and (node.name in S1_FUNCTIONS or node.name.startswith("_retention_")): + names = {call.func.id if isinstance(call.func, ast.Name) else call.func.attr for call in ast.walk(node) if isinstance(call, ast.Call) and isinstance(call.func, (ast.Name, ast.Attribute))} + self.assertEqual(names & forbidden, set(), node.name) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-linux-startup-transport-owner-297.py b/tests/test-linux-startup-transport-owner-297.py new file mode 100644 index 00000000..8236ac04 --- /dev/null +++ b/tests/test-linux-startup-transport-owner-297.py @@ -0,0 +1,500 @@ +"""Synthetic inactive transport controls. No DLL, bus, socket or pointer reads.""" +from __future__ import annotations + +import importlib.util +from pathlib import Path +import stat +import sys +from types import ModuleType, SimpleNamespace +from typing import Callable +import unittest +from unittest.mock import patch + + +def load_source() -> ModuleType: + path = Path(__file__).resolve().parents[1] / "truememory_memory_bootstrap.py" + name = "truememory_transport_author_source" + spec = importlib.util.spec_from_file_location(name, path) + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module + spec.loader.exec_module(module) + return module + + +class Function: + def __init__(self, runtime: InertRuntime, name: str) -> None: + self.runtime = runtime + self.name = name + self.argtypes = None + self.restype = None + self.errcheck = None + + def __call__(self, *arguments: object) -> object: + return self.runtime.call(self.name, arguments) + + +class InertRuntime: + def __init__(self, module: ModuleType) -> None: + self.module = module + self.clock = 100.0 + self.calls = [] + self.strings = {} + self.next_string = 100000 + self.messages = {} + self.next_message = 1000 + self.next_slot = 4000 + self.next_cookie = 70 + self.next_serial = 7000 + self.bad_signature = None + self.bad_version = None + self.bad_uid = None + self.bad_reply_cookie = False + self.bad_sender = False + self.skip_reply = False + self.reuse_after_close = False + self.rebound = False + self.close_calls = [] + self.closed = False + self.raise_name = None + self.expire_name = None + self.library = SimpleNamespace() + names = ( + "new", "set_fd", "set_bus_client", "set_anonymous", "negotiate_fds", + "set_exit_on_disconnect", "start", "is_ready", "process", "get_fd", + "get_events", "get_timeout", "get_bus_id", "get_unique_name", "call_async", + "add_filter", "close", "unref", "send", "add_match_async", + ) + message_names = ( + "new_method_call", "set_auto_start", "set_allow_interactive_authorization", + "append_basic", "get_cookie", "get_reply_cookie", "get_sender", "get_destination", + "get_type", "get_signature", "get_path", "get_interface", "get_member", "read_basic", + "enter_container", "exit_container", "at_end", "unref", "ref", "open_container", "close_container", + ) + for suffix in names: + setattr(self.library, "sd_bus_" + suffix, Function(self, "sd_bus_" + suffix)) + for suffix in message_names: + setattr(self.library, "sd_bus_message_" + suffix, Function(self, "sd_bus_message_" + suffix)) + self.library.sd_bus_slot_unref = Function(self, "sd_bus_slot_unref") + + def string(self, value: str) -> int: + pointer = self.next_string + data = value.encode("ascii") + b"\x00" + self.next_string += len(data) + 16 + for index, byte in enumerate(data): + self.strings[pointer + index] = byte + return pointer + + def byte(self, address: int) -> SimpleNamespace: + if address not in self.strings: + raise AssertionError("Nonowned synthetic address") + return SimpleNamespace(value=self.strings[address]) + + def metadata(self, descriptor: int) -> SimpleNamespace: + if descriptor != 33: + raise AssertionError("Unexpected synthetic descriptor") + inode = 99 if self.rebound else 33 + return SimpleNamespace(st_dev=1, st_ino=inode, st_mode=stat.S_IFSOCK | 0o600, + st_uid=1000, st_gid=77, st_size=0, st_mtime_ns=0, + st_ctime_ns=0, st_nlink=1) + + def load_backend(self, record: object, token: object) -> None: + record.resources.library = self.library + backend = self.module._NativeStartupBackend(self.library) + record.resources.backend = record.seal.backend = backend + + def open_endpoint(self, record: object, token: object) -> None: + module = self.module + resources = record.resources + resources.raw_fd = 33 + resources.raw_pin = (1, 33, stat.S_IFSOCK | 0o600, 1000, 77) + resources.transfer = "bus_owned" + resources.peer = (200, 1000, 77) + out = module.ctypes.c_void_p(100) + reference = module._NativeStartupReference("bus", out) + reference.pointer, reference.state = 100, "owned" + resources.bus = record.seal.bus = reference + resources.references.append(reference) + module._native_startup_commit(record, peer_credentials=resources.peer, + connection_closed=False, native_objects_released=False) + + def call(self, name: str, arguments: tuple[object, ...]) -> object: + self.calls.append(name) + if name == self.raise_name: + raise RuntimeError("Synthetic lost return") + if name == self.expire_name: + self.clock = 201.0 + if name == "sd_bus_is_ready": + return 1 + if name == "sd_bus_get_bus_id": + for index in range(16): + arguments[1]._obj.bytes[index] = index + 1 + return 0 + if name == "sd_bus_get_unique_name": + arguments[1]._obj.value = self.string(":1.2") + return 0 + if name == "sd_bus_add_filter": + self.next_slot += 1 + arguments[1]._obj.value = self.next_slot + return 0 + if name == "sd_bus_message_new_method_call": + self.next_message += 1 + self.next_cookie += 1 + pointer = self.next_message + arguments[1]._obj.value = pointer + self.messages[pointer] = {"destination": arguments[2].decode(), "member": arguments[5].decode(), + "cookie": self.next_cookie, "kind": 1} + return 0 + if name == "sd_bus_call_async": + self.next_slot += 1 + arguments[1]._obj.value = self.next_slot + if not self.skip_reply: + original = self.messages[arguments[2].value] + self.next_message += 1 + self.next_serial += 1 + pointer = self.next_message + member = original["member"] + signature = {"AddMatch": "", "GetNameOwner": "s", "GetConnectionUnixUser": "u", "Get": "v", "Subscribe": ""}[member] + value = {"GetNameOwner": ":1.9", "GetConnectionUnixUser": 1000 if self.bad_uid is None else self.bad_uid, + "Get": "249.11" if self.bad_version is None else self.bad_version}.get(member) + self.messages[pointer] = {"sender": ":1.999" if self.bad_sender else original["destination"], + "destination": ":1.2", "signature": self.bad_signature if self.bad_signature is not None else signature, + "reply_cookie": original["cookie"] + (1 if self.bad_reply_cookie else 0), + "serial": self.next_serial, "kind": 2, "value": value} + arguments[3](pointer, arguments[4].value, None) + return 1 + if name == "sd_bus_message_get_cookie": + item = self.messages[arguments[0].value] + arguments[1]._obj.value = item.get("serial", item.get("cookie")) + return 0 + if name == "sd_bus_message_get_reply_cookie": + arguments[1]._obj.value = self.messages[arguments[0].value]["reply_cookie"] + return 0 + if name == "sd_bus_message_get_type": + arguments[1]._obj.value = self.messages[arguments[0].value]["kind"] + return 0 + if name in ("sd_bus_message_get_sender", "sd_bus_message_get_destination", "sd_bus_message_get_signature"): + field = name.removeprefix("sd_bus_message_get_") + return self.string(self.messages[arguments[0].value][field]) + if name == "sd_bus_message_read_basic": + value = self.messages[arguments[0].value]["value"] + arguments[2]._obj.value = self.string(value) if arguments[1] == b"s" else value + return 1 + if name in ("sd_bus_message_enter_container", "sd_bus_message_exit_container", "sd_bus_message_at_end"): + return 1 + if name in ("sd_bus_message_unref", "sd_bus_slot_unref", "sd_bus_unref"): + return None + if name == "sd_bus_close": + self.closed = True + if self.reuse_after_close: + self.rebound = True + return None + if name == "sd_bus_process": + self.clock += 100.0 + return 0 + return 0 + + +class TransportOwnerTests(unittest.TestCase): + def setUp(self) -> None: + self.module = load_source() + self.runtime = InertRuntime(self.module) + runtime = self.runtime + byte_type = type("InertByte", (), {"from_address": staticmethod(runtime.byte)}) + patches = ( + patch.object(self.module.time, "monotonic", lambda: runtime.clock), + patch.object(self.module.os, "getuid", lambda: 1000), + patch.object(self.module.os, "geteuid", lambda: 1000), + patch.object(self.module.os, "fstat", runtime.metadata), + patch.object(self.module.os, "close", lambda fd: runtime.close_calls.append(fd)), + patch.object(self.module.ctypes, "c_ubyte", byte_type), + patch.object(self.module.ctypes, "CFUNCTYPE", lambda *args: (lambda callback: callback)), + patch.object(self.module, "_native_startup_load_backend", runtime.load_backend), + patch.object(self.module, "_native_startup_open_endpoint", runtime.open_endpoint), + patch.object(self.module.ctypes, "CDLL", side_effect=AssertionError("DLL is forbidden")), + patch.object(self.module.socket, "socket", side_effect=AssertionError("Socket is forbidden")), + patch.object(self.module.select, "poll", side_effect=AssertionError("Poll is forbidden")), + patch.object(self.module.os, "open", side_effect=AssertionError("Filesystem is forbidden")), + patch.object(self.module.os, "read", side_effect=AssertionError("Filesystem is forbidden")), + ) + for item in patches: + item.start() + self.addCleanup(item.stop) + + def request(self) -> object: + module = self.module + pin = module.StartupNativeBackendPin(1, module._NATIVE_STARTUP_LIBRARY, + module._NATIVE_STARTUP_LIBRARY_SHA, + module._NATIVE_STARTUP_LIBRARY_STAT) + return module.StartupNativeRequest(1, module.LinuxDaemonLaunchPolicy(1, 268435456, 0), + "0" * 32, "/synthetic/python", pin) + + def owner(self) -> object: + return self.module.retain_native_startup(self.request(), deadline=200.0) + + def refusal(self, callback: Callable[[], object], code: str) -> None: + with self.assertRaises(self.module.EnforcementUnavailable) as caught: + callback() + self.assertEqual(str(caught.exception), "native_startup:" + code) + + def test_prepared_required_fields_and_identity(self) -> None: + owner = self.owner() + state = self.module.native_startup_snapshot(owner) + self.assertIs(state.owner, owner) + self.assertEqual((state.phase, state.operation_sequence, state.cleanup_sequence), ("prepared", 0, 0)) + self.assertIsNone(state.context) + self.assertIsNone(state.peer_credentials) + self.assertTrue(state.connection_closed) + self.assertTrue(state.native_objects_released) + self.assertFalse(state.slice_attempted) + self.assertFalse(state.service_attempted) + self.assertEqual(self.runtime.calls, []) + + def test_sequence_exact_domains_and_boundaries(self) -> None: + module = self.module + self.assertEqual(module._native_startup_sequence_pair(0), (1, 2)) + self.assertEqual(module._native_startup_sequence_pair(module._NATIVE_STARTUP_U64 - 2), + (module._NATIVE_STARTUP_U64 - 1, module._NATIVE_STARTUP_U64)) + for value in (True, False, -1, 1.0, None, module._NATIVE_STARTUP_U64 + 1): + with self.subTest(value=value): + self.refusal(lambda: module._native_startup_sequence_pair(value), "invalid_input") + for value in (module._NATIVE_STARTUP_U64 - 1, module._NATIVE_STARTUP_U64): + with self.subTest(value=value): + self.refusal(lambda: module._native_startup_sequence_pair(value), "sequence_exhausted") + self.refusal(lambda: module._native_startup_sequence_pair(value, cleanup=True), "cleanup_counter_exhausted") + self.refusal(lambda: module._native_startup_sequence_pair(0, cleanup=1), "invalid_input") + + def test_deadline_exact_float_and_future(self) -> None: + for deadline in (True, 200, None, float("inf"), float("nan")): + with self.subTest(deadline=deadline): + self.refusal(lambda: self.module.retain_native_startup(self.request(), deadline=deadline), "invalid_input") + for deadline in (99.0, 100.0): + with self.subTest(deadline=deadline): + self.refusal(lambda: self.module.retain_native_startup(self.request(), deadline=deadline), "deadline_expired") + + def test_strict_constructor_subclasses(self) -> None: + module = self.module + class OwnerSubtype(module.NativeStartupOwner): + pass + self.refusal(lambda: OwnerSubtype(1, 1, 1), "invalid_input") + class StringSubtype(str): + pass + pin = self.request().backend_pin + self.refusal(lambda: module.StartupNativeBackendPin(1, StringSubtype(pin.canonical_path), pin.sha256, pin.stat_pin), "invalid_input") + + def test_pin_boolean_and_mode_rejected(self) -> None: + module = self.module + original = self.request().backend_pin + for index in range(9): + fields = list(original.stat_pin) + fields[index] = True + with self.subTest(index=index): + self.refusal(lambda: module.StartupNativeBackendPin(1, original.canonical_path, original.sha256, tuple(fields)), "invalid_input") + fields = list(original.stat_pin) + fields[2] |= 0o002 + self.refusal(lambda: module.StartupNativeBackendPin(1, original.canonical_path, original.sha256, tuple(fields)), "invalid_input") + + def test_request_copy_independent(self) -> None: + request = self.request() + owner = self.module.retain_native_startup(request, deadline=200.0) + object.__setattr__(request.policy, "cap_bytes", 1) + state = self.module.native_startup_snapshot(owner) + self.assertEqual(state.request.policy.cap_bytes, 268435456) + object.__setattr__(state.request.policy, "cap_bytes", 2) + self.assertEqual(self.module.native_startup_snapshot(owner).request.policy.cap_bytes, 268435456) + + def test_reflected_owner_is_not_authoritative(self) -> None: + owner = self.owner() + reflected = self.module.NativeStartupOwner(owner.version, owner.owner_id, owner.creator_pid) + self.refusal(lambda: self.module.native_startup_snapshot(reflected), "invalid_owner") + self.assertEqual(len(self.module._NATIVE_STARTUP_RECORDS), 1) + + def test_isolated_state_mutation_refuses(self) -> None: + owner = self.owner() + record = self.module._NATIVE_STARTUP_RECORDS[owner.owner_id] + object.__setattr__(record.state, "original_deadline", 300.0) + self.refusal(lambda: self.module.native_startup_snapshot(owner), "invalid_input") + self.assertEqual(self.runtime.calls, []) + + def test_capacity_and_release_no_reused_ids(self) -> None: + owners = [self.owner() for _ in range(16)] + self.refusal(self.owner, "capacity_exhausted") + self.module.close_precreate_startup(owners[0], expected_sequence=0, expected_cleanup_sequence=0, cleanup_deadline=102.0) + released = self.module.release_closed_native_startup(owners[0], expected_sequence=0, expected_cleanup_sequence=2) + self.assertEqual(released.phase, "released") + self.assertGreater(self.owner().owner_id, owners[-1].owner_id) + self.refusal(lambda: self.module.native_startup_snapshot(owners[0]), "invalid_owner") + + def test_inclusive_expiry_keeps_counters(self) -> None: + owner = self.owner() + self.runtime.clock = 200.0 + state = self.module.native_startup_snapshot(owner) + self.assertEqual((state.phase, state.reason), ("terminal_unknown", "deadline_expired")) + self.assertEqual((state.operation_sequence, state.cleanup_sequence), (0, 0)) + self.assertEqual(len(self.module._NATIVE_STARTUP_RECORDS), 1) + self.assertEqual(self.runtime.calls, []) + + def test_clock_regression_refuses_without_authority(self) -> None: + owner = self.owner() + self.runtime.clock = 99.0 + self.refusal(lambda: self.module.native_startup_snapshot(owner), "clock_regressed") + self.assertEqual(self.module._NATIVE_STARTUP_RECORDS[owner.owner_id].state.operation_sequence, 0) + + def test_fork_refuses_before_lock_clock(self) -> None: + owner = self.owner() + with patch.object(self.module.os, "getpid", lambda: owner.creator_pid + 1): + with patch.object(self.module.time, "monotonic", side_effect=AssertionError("Clock must not run")): + self.refusal(lambda: self.module.native_startup_snapshot(owner), "forked_owner") + + def test_complete_authentication_fixed_protocol(self) -> None: + owner = self.owner() + state = self.module.authenticate_startup_bus(owner, expected_sequence=0) + self.assertEqual((state.phase, state.operation_sequence), ("subscribed", 2)) + self.assertEqual(state.manager_version, "249.11") + self.assertEqual(state.peer_credentials, (200, 1000, 77)) + self.assertEqual(state.context.manager_unique_name, ":1.9") + self.assertTrue(state.owner_match_confirmed) + self.assertTrue(state.subscription_confirmed) + self.assertEqual(self.runtime.calls.count("sd_bus_call_async"), 5) + self.assertNotIn("sd_bus_send", self.runtime.calls) + self.assertNotIn("sd_bus_add_match_async", self.runtime.calls) + + def test_stale_sequence_precedes_native_effect(self) -> None: + owner = self.owner() + self.refusal(lambda: self.module.authenticate_startup_bus(owner, expected_sequence=1), "stale_sequence") + self.assertEqual(self.runtime.calls, []) + + def test_lost_native_return_retains_active_operation(self) -> None: + owner = self.owner() + self.runtime.raise_name = "sd_bus_is_ready" + state = self.module.authenticate_startup_bus(owner, expected_sequence=0) + record = self.module._NATIVE_STARTUP_RECORDS[owner.owner_id] + self.assertEqual(state.phase, "terminal_unknown") + self.assertEqual(record.resources.active_calls, 1) + self.assertIs(self.module._NATIVE_STARTUP_OPERATION, record) + frame = record.resources.frames[-1] + self.assertTrue(frame.entered) + self.assertFalse(frame.returned) + before = list(self.runtime.calls) + self.refusal(lambda: self.module.native_startup_snapshot(owner), "busy") + self.refusal(lambda: self.module.close_precreate_startup(owner, expected_sequence=2, expected_cleanup_sequence=0, cleanup_deadline=102.0), "busy") + self.refusal(lambda: self.module.release_closed_native_startup(owner, expected_sequence=2, expected_cleanup_sequence=0), "busy") + self.assertEqual(self.runtime.calls, before) + self.assertEqual(self.runtime.close_calls, []) + + def test_known_return_after_deadline_keeps_history_but_not_active_c(self) -> None: + owner = self.owner() + self.runtime.expire_name = "sd_bus_get_bus_id" + state = self.module.authenticate_startup_bus(owner, expected_sequence=0) + record = self.module._NATIVE_STARTUP_RECORDS[owner.owner_id] + self.assertEqual((state.phase, state.reason), ("terminal_unknown", "deadline_expired")) + self.assertEqual(record.resources.active_calls, 0) + self.assertIsNone(self.module._NATIVE_STARTUP_OPERATION) + self.assertTrue(record.resources.frames[-1].returned) + self.assertEqual(record.operation.outcome[0], 2) + self.runtime.expire_name = None + closed = self.module.close_precreate_startup(owner, expected_sequence=2, expected_cleanup_sequence=0, cleanup_deadline=202.0) + self.assertEqual(closed.phase, "closed_precreate") + self.assertEqual(closed.original_deadline, 200.0) + + def test_invalid_authentication_replies_ungraded_unknown(self) -> None: + for field, value in (("bad_signature", "ss"), ("bad_reply_cookie", True), ("bad_sender", True), ("bad_uid", 1001), ("bad_version", "2490")): + with self.subTest(field=field): + setattr(self.runtime, field, value) + owner = self.owner() + state = self.module.authenticate_startup_bus(owner, expected_sequence=0) + self.assertEqual(state.phase, "terminal_unknown") + self.assertIsNotNone(state.reason) + self.assertEqual(state.operation_sequence, 2) + self.assertFalse(state.subscription_confirmed) + setattr(self.runtime, field, None if field in ("bad_signature", "bad_uid", "bad_version") else False) + + def test_strict_observed_version_grammar(self) -> None: + for value in ("249", "249.11", "249.11-0ubuntu3.16pop0~synthetic"): + with self.subTest(value=value): + self.runtime.bad_version = value + state = self.module.authenticate_startup_bus(self.owner(), expected_sequence=0) + self.assertEqual(state.phase, "subscribed") + self.assertEqual(state.manager_version, value) + + def test_cleanup_deadline_separate_exact_boundary(self) -> None: + owner = self.owner() + for value, code in ((102, "invalid_input"), (True, "invalid_input"), (100.0, "cleanup_deadline_invalid"), (102.000000001, "cleanup_deadline_invalid")): + with self.subTest(value=value): + self.refusal(lambda: self.module.close_precreate_startup(owner, expected_sequence=0, expected_cleanup_sequence=0, cleanup_deadline=value), code) + self.assertEqual(self.module.close_precreate_startup(owner, expected_sequence=0, expected_cleanup_sequence=0, cleanup_deadline=102.0).phase, "closed_precreate") + + def test_expired_auth_cleanup_does_not_renew_original(self) -> None: + owner = self.owner() + self.runtime.clock = 201.0 + state = self.module.close_precreate_startup(owner, expected_sequence=0, expected_cleanup_sequence=0, cleanup_deadline=202.0) + self.assertEqual(state.phase, "closed_precreate") + self.assertEqual(state.original_deadline, 200.0) + self.assertEqual((state.operation_sequence, state.cleanup_sequence), (0, 2)) + self.assertEqual(self.runtime.calls, []) + + def test_bus_close_before_reference_releases(self) -> None: + owner = self.owner() + self.module.authenticate_startup_bus(owner, expected_sequence=0) + before = len(self.runtime.calls) + state = self.module.close_precreate_startup(owner, expected_sequence=2, expected_cleanup_sequence=0, cleanup_deadline=102.0) + self.assertEqual(state.phase, "closed_precreate") + cleanup_calls = self.runtime.calls[before:] + self.assertEqual(cleanup_calls[0], "sd_bus_close") + self.assertEqual(cleanup_calls[-1], "sd_bus_unref") + self.assertNotIn("sd_bus_process", cleanup_calls) + self.assertNotIn("sd_bus_call_async", cleanup_calls) + self.assertEqual(self.runtime.close_calls, []) + + def test_observed_close_then_reused_number_not_closed(self) -> None: + owner = self.owner() + self.module.authenticate_startup_bus(owner, expected_sequence=0) + self.runtime.reuse_after_close = True + state = self.module.close_precreate_startup(owner, expected_sequence=2, expected_cleanup_sequence=0, cleanup_deadline=102.0) + self.assertEqual(state.phase, "closed_precreate") + self.assertTrue(state.connection_closed) + self.assertEqual(self.runtime.close_calls, []) + + def test_preclose_rebound_quarantines(self) -> None: + owner = self.owner() + self.module.authenticate_startup_bus(owner, expected_sequence=0) + self.runtime.rebound = True + before = len(self.runtime.calls) + state = self.module.close_precreate_startup(owner, expected_sequence=2, expected_cleanup_sequence=0, cleanup_deadline=102.0) + self.assertEqual(state.phase, "terminal_unknown") + self.assertFalse(state.connection_closed) + self.assertEqual(self.runtime.calls[before:], []) + self.assertEqual(self.runtime.close_calls, []) + + def test_release_pure_preserves_final_counters(self) -> None: + owner = self.owner() + self.module.authenticate_startup_bus(owner, expected_sequence=0) + self.module.close_precreate_startup(owner, expected_sequence=2, expected_cleanup_sequence=0, cleanup_deadline=102.0) + before = len(self.runtime.calls) + with patch.object(self.module.time, "monotonic", side_effect=AssertionError("Release is pure")): + state = self.module.release_closed_native_startup(owner, expected_sequence=2, expected_cleanup_sequence=2) + self.assertEqual((state.operation_sequence, state.cleanup_sequence), (2, 2)) + self.assertEqual(state.phase, "released") + self.assertEqual(len(self.runtime.calls), before) + + def test_direct_evidence_boundary_checked_before_copy(self) -> None: + owner = self.owner() + record = self.module._NATIVE_STARTUP_RECORDS[owner.owner_id] + event = {"sender": "org.freedesktop.DBus", "serial": 1, "value": "synthetic"} + size = self.module._native_startup_evidence_size(event) + record.resources.evidence_bytes = 65536 - size + self.module._native_startup_evidence(record, event) + self.assertEqual(record.resources.evidence_bytes, 65536) + self.refusal(lambda: self.module._native_startup_evidence(record, {"sender": "org.freedesktop.DBus", "serial": 2}), "evidence_overflow") + self.assertEqual(len(record.resources.events), 1) + + def test_direct_event_boundary_checked_before_copy(self) -> None: + owner = self.owner() + record = self.module._NATIVE_STARTUP_RECORDS[owner.owner_id] + for serial in range(64): + self.module._native_startup_evidence(record, {"sender": "org.freedesktop.DBus", "serial": serial + 1}) + self.refusal(lambda: self.module._native_startup_evidence(record, {"sender": "org.freedesktop.DBus", "serial": 65}), "evidence_overflow") + self.assertEqual(len(record.resources.events), 64) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-mac-native-acceptance-v1.py b/tests/test-mac-native-acceptance-v1.py new file mode 100644 index 00000000..517de737 --- /dev/null +++ b/tests/test-mac-native-acceptance-v1.py @@ -0,0 +1,341 @@ +"""Pure Stage A policy tests. Never launch a child, signal, sensor or fixture.""" +from __future__ import annotations + +import copy +import os +import stat +import sys +import types +import unittest +from pathlib import Path +from unittest import mock + + +def load_source(name: str, filename: str) -> types.ModuleType: + path = Path(__file__).parent / "native" / filename + module = types.ModuleType(name) + module.__file__ = str(path) + sys.modules[name] = module + exec(compile(path.read_bytes(), str(path), "exec", dont_inherit=True), module.__dict__) + return module + + +POLICY = load_source("native_acceptance_policy_test_v1", "mac-native-acceptance-v1.py") +BOOT = load_source("native_acceptance_bootstrap_test_v1", "mac-native-bootstrap-v1.py") + + +def case(kind: str = "capture-a1") -> dict: + name = {"capture-revision": "capture-a2", "invalid-state": "negative-a3"}.get(kind, kind) + database = "capture-a2" if kind == "capture-revision" else "negative-a3" if kind == "invalid-state" else kind + row = {"id": kind, "source": str(POLICY.RUN_ROOT / "home/.claude/projects/native-acceptance-v1" / (name + ".jsonl")), + "source_sha256": "a" * 64, "source_bytes": 24, "source_identity": [1, 2, 24, 3, 4], + "session": "synthetic-native-" + name, "database": str(POLICY.RUN_ROOT / (database + "-v1.db")), "previous": None} + if kind == "flock-a4": + for field in ("source", "source_sha256", "source_bytes", "source_identity"): + row[field] = None + if kind == "capture-revision": + row["previous"] = {"key": "b" * 64, "sha256": "c" * 64, "byte_count": 12} + return row + + +def manifest() -> dict: + return {"version": 1, "stage": "A", "synthetic": True, "root_execution_go": True, + "run_root": str(POLICY.RUN_ROOT), "home": str(POLICY.RUN_ROOT / "home"), + "source_root": str(POLICY.SOURCE_ROOT), "source_commit": "a" * 40, + "sources": {name: "a" * 64 for name in POLICY.SOURCE_FILES}, + "supervisor_sha256": "b" * 64, "bootstrap_sha256": "c" * 64, + "limits": dict(POLICY.LIMITS), "cases": [case()]} + + +def mutation(seq: int, event: str, path: Path, flags: int = 0, source: Path | None = None) -> dict: + return {"type": "mutation", "version": 1, "seq": seq, "event": event, + "path": str(path), "flags": flags, "source": str(source) if source is not None else None} + + +class FakeProtocol: + def __init__(self) -> None: + self.sent: list[object] = [] + self.answers: list[dict] = [] + + def emit(self, value: object) -> None: + self.sent.append(value) + + def receive(self, maximum: int, seconds: float) -> dict: + if maximum != 2048 or seconds != 0.1: + raise AssertionError("Changed frame or ACK bound") + return self.answers.pop(0) + + +class FakeSelector: + def close(self) -> None: + return None + + +class FakePsutilError(Exception): + pass + + +class PurePolicyTests(unittest.TestCase): + def test_fixed_limits_and_proof_scope(self) -> None: + self.assertEqual(POLICY.LIMITS, BOOT.LIMITS) + self.assertEqual(POLICY.FREE_MIN - POLICY.LIMITS["rss_abort_bytes"], 201326592) + self.assertEqual(POLICY.PROOF_SCOPE, BOOT.PROOF_SCOPE) + self.assertEqual(set(POLICY.SOURCE_FILES), {path for _, path in BOOT.SOURCE_MODULES}) + self.assertNotIn("installed", POLICY.PROOF_SCOPE) + + def test_exact_integer_and_hash(self) -> None: + for value in (True, False, 1.0, None, -1, 1 << 80): + with self.subTest(value=value), self.assertRaises(POLICY.SafetyError): + POLICY.integer(value) + for value in (True, "A" * 64, "a" * 63, "a" * 65, None): + with self.subTest(value=value), self.assertRaises(POLICY.SafetyError): + POLICY.hash_text(value) + self.assertEqual(POLICY.integer(0), 0) + self.assertEqual(POLICY.hash_text("a" * 64), "a" * 64) + + def test_sensor_admission_exact_floor_and_abi(self) -> None: + samples = [POLICY.Sensor(1_000_000_000 + n * 1_000_000_000, 1, POLICY.FREE_MIN, 100) for n in range(3)] + POLICY.admission(samples, 3_500_000_000) + changes = [("pressure", 0), ("pressure", 2), ("pressure", 3), ("pressure", 4), + ("pressure", True), ("free_bytes", POLICY.FREE_MIN - 1), + ("free_bytes", True), ("swap_used_bytes", 101), ("monotonic_ns", 1_000_000_000)] + for field, value in changes: + altered = [POLICY.asdict(row) for row in samples] + altered[-1][field] = value + with self.subTest(field=field, value=value), self.assertRaises(POLICY.SafetyError): + POLICY.admission([POLICY.Sensor(**row) for row in altered], 3_500_000_000) + for now in (2_999_999_999, 3_500_000_001): + with self.subTest(now=now), self.assertRaises(POLICY.SafetyError): + POLICY.admission(samples, now) + with self.assertRaises(POLICY.SafetyError): + POLICY.admission(samples[:2], 3_500_000_000) + with self.assertRaises(POLICY.SafetyError): + POLICY.admission([POLICY.Sensor(1 + n, 1, POLICY.FREE_MIN, 0) for n in range(3)], 3) + + def test_manifest_and_case_fences(self) -> None: + self.assertEqual(POLICY.validate_manifest(manifest()), manifest()) + for kind in POLICY.CASES: + POLICY.validate_case(case(kind)) + changes = [("version", True), ("stage", "B"), ("synthetic", False), ("root_execution_go", False), + ("run_root", "/tmp"), ("source_root", "/Users/j/dev/TrueMemory"), + ("sources", {}), ("cases", []), ("cases", [case(), case()])] + for field, value in changes: + altered = manifest() + altered[field] = value + with self.subTest(field=field), self.assertRaises(POLICY.SafetyError): + POLICY.validate_manifest(altered) + for field, value in (("id", []), ("session", "unknown"), ("database", "/tmp/personal.db"), + ("source", "/Users/j/dev/TrueMemory/transcript.jsonl"), ("source_bytes", True), + ("source_identity", [1, 2, 23, 3, 4])): + altered = case() + altered[field] = value + with self.subTest(field=field), self.assertRaises(POLICY.SafetyError): + POLICY.validate_case(altered) + altered = manifest() + altered["limits"]["wall_seconds"] = 6 + with self.assertRaises(POLICY.SafetyError): + POLICY.validate_manifest(altered) + altered = manifest() + altered["limits"]["core_bytes"] = False + with self.assertRaises(POLICY.SafetyError): + POLICY.validate_manifest(altered) + + def test_paths_and_versioned_results(self) -> None: + for name in ("v1", "v2"): + self.assertEqual(POLICY.evidence_paths(POLICY.RUN_ROOT / ("manifest-" + name + ".json")), + (POLICY.RUN_ROOT / ("results-" + name + ".json"), name)) + for text in ("relative", str(POLICY.RUN_ROOT) + "/../outside", str(POLICY.RUN_ROOT) + "//escape", "/tmp/file"): + with self.subTest(text=text), self.assertRaises(POLICY.SafetyError): + POLICY.canonical_path(text, POLICY.RUN_ROOT) + with self.assertRaises(POLICY.SafetyError): + POLICY.evidence_paths(POLICY.RUN_ROOT / "manifest-v3.json") + + def test_identity_is_current_owned_incarnation(self) -> None: + row = POLICY.Identity(72887, 42, 12345, 72887) + self.assertTrue(POLICY.same_owned_identity(row, row, 42)) + for changed in (POLICY.Identity(72887, 42, 12346, 72887), POLICY.Identity(72887, 43, 12345, 72887), + POLICY.Identity(72887, 42, 12345, 1), POLICY.Identity(True, 42, 12345, 1)): + with self.subTest(changed=changed): + self.assertFalse(POLICY.same_owned_identity(row, changed, 42)) + + def test_ready_and_result_exact_protocol(self) -> None: + ready = {"type": "ready", "version": 1, "pid": 100, "audit_installed": True, + "limits": {"cpu": [2, 3], "core": [0, 0], "nofile": [64, 64], "fsize": [1048576, 1048576]}} + POLICY.validate_ready(ready, 100) + for field, value in (("pid", 101), ("version", True), ("audit_installed", 1), ("limits", {})): + altered = copy.deepcopy(ready) + altered[field] = value + with self.subTest(field=field), self.assertRaises(POLICY.SafetyError): + POLICY.validate_ready(altered, 100) + altered = copy.deepcopy(ready) + altered["limits"]["core"] = [False, False] + with self.assertRaises(POLICY.SafetyError): + POLICY.validate_ready(altered, 100) + done = {"type": "result", "version": 1, "case": "capture-a1", "passed": True, + "proof_scope": POLICY.PROOF_SCOPE, "import_mode": "isolated_source_module", "sources": manifest()["sources"], "details": {}} + POLICY.validate_result(done, case(), manifest()["sources"]) + for field, value in (("passed", 1), ("version", True), ("proof_scope", "installed_host"), + ("case", "capture-a2"), ("sources", {}), ("details", [])): + altered = dict(done) + altered[field] = value + with self.subTest(field=field), self.assertRaises(POLICY.SafetyError): + POLICY.validate_result(altered, case(), manifest()["sources"]) + + def test_mutation_positive_and_one_pending_slot(self) -> None: + policy = POLICY.MutationPolicy(case()) + policy.decide(mutation(1, "os.mkdir", policy.account, 0o700)) + lock = policy.account / "state.lock" + policy.decide(mutation(2, "open", lock, os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW)) + pending = policy.account / ("state-" + "a" * 32 + ".pending") + policy.decide(mutation(3, "open", pending, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW)) + grant = policy.decide(mutation(4, "os.rename", policy.account / "state.json", source=pending)) + self.assertEqual(grant["path"], str(policy.account / "state.json")) + with self.assertRaises(POLICY.SafetyError): + policy.decide(mutation(5, "open", policy.account / ("state-" + "b" * 32 + ".pending"), os.O_WRONLY | os.O_CREAT | os.O_EXCL)) + + def test_mutation_negative_controls(self) -> None: + for event, filename, flags, source in (("open", "state.json", os.O_WRONLY | os.O_CREAT, None), + ("open", "state.lock", os.O_RDWR | os.O_CREAT | os.O_TRUNC, None), + ("open", "unknown", os.O_WRONLY | os.O_CREAT | os.O_EXCL, None), + ("os.mkdir", "foreign", 0o700, None), + ("os.mkdir", "captures", 0o755, None), + ("os.rename", "state.json", 0, POLICY.RUN_ROOT / "arbitrary"), + ("unlink", "state.json", 0, None)): + policy = POLICY.MutationPolicy(case()) + with self.subTest(event=event, filename=filename), self.assertRaises(POLICY.SafetyError): + policy.decide(mutation(1, event, policy.account / filename, flags, source)) + for sequence in (0, 2, True, 33): + policy = POLICY.MutationPolicy(case()) + with self.subTest(sequence=sequence), self.assertRaises(POLICY.SafetyError): + policy.decide(mutation(sequence, "os.mkdir", policy.account, 0o700)) + policy = POLICY.MutationPolicy(case("negative-a3")) + with self.assertRaises(POLICY.SafetyError): + policy.decide(mutation(1, "os.mkdir", policy.account, 0o700)) + + def test_fd_wrappers_bind_latest_consumed_intent(self) -> None: + import fcntl + row = case() + guard = BOOT.AuditGuard(FakeProtocol(), {"case": row}, POLICY) + path = Path(row["source"]) + info = types.SimpleNamespace(st_dev=1, st_ino=2, st_uid=42, st_mode=stat.S_IFREG | 0o600) + with mock.patch.object(BOOT.os, "fstat", return_value=info), mock.patch.object(BOOT.os, "getuid", return_value=42), mock.patch.object(fcntl, "fcntl", return_value=os.O_RDONLY), mock.patch.object(Path, "lstat", return_value=info), mock.patch.object(Path, "is_symlink", return_value=False): + guard.open_intents[:] = [(path, os.O_RDONLY)] + guard.descriptor(7, "r") + self.assertEqual(guard.known_fds, {7: (1, 2)}) + with self.assertRaises(BOOT.BootstrapError): + guard.descriptor(7, "rb") + info.st_ino = 3 + guard.open_intents[:] = [(path, os.O_RDONLY)] + guard.descriptor(7, "rb") + self.assertEqual(guard.known_fds, {7: (1, 3)}) + guard.open_intents[:] = [(path, os.O_WRONLY)] + with self.assertRaises(BOOT.BootstrapError): + guard.descriptor(7, "rb") + self.assertFalse(guard.open_intents) + for fd, mode in ((0, "r"), (7, "a"), (7, "r+"), (True, "r")): + with self.subTest(fd=fd, mode=mode), self.assertRaises(BOOT.BootstrapError): + guard.descriptor(fd, mode) + + def test_audit_preopen_grant_and_denials(self) -> None: + protocol = FakeProtocol() + guard = BOOT.AuditGuard(protocol, {"case": case()}, POLICY) + path = POLICY.account_directory(case()) / "state.lock" + protocol.answers.append({"type": "grant", "version": 1, "seq": 1, "path": str(path), "purpose": "Owned fixed lock"}) + guard.path = lambda value: Path(value) + guard("open", (str(path), None, os.O_RDWR | os.O_CREAT)) + self.assertEqual(len(protocol.sent), 1) + self.assertEqual(guard.open_intents, [(path, os.O_RDWR)]) + guard("truememory.native.audit_probe", ("fixed-stage-a-v1",)) + self.assertTrue(guard.activated) + for event, args in (("socket.connect", ()), ("subprocess.Popen", ()), ("os.remove", (str(path), -1)), + ("sqlite3.connect", ("memory",)), ("resource.setrlimit", (0, (1, 1))), + ("import", ("truememory.engine", None)), ("import", ("torch", None)), + ("os.mkdir", (str(path), 0o700, 1)), ("os.rename", (str(path), str(path), 1, -1))): + with self.subTest(event=event), self.assertRaises(BOOT.BootstrapError): + guard(event, args) + + def test_prelaunch_refusals_retain_nonexecuted_result(self) -> None: + fake = types.ModuleType("fake_psutil") + fake.Error = FakePsutilError + with mock.patch.object(POLICY.selectors, "DefaultSelector", return_value=FakeSelector()), mock.patch.object(POLICY, "sample", side_effect=POLICY.SafetyError("pressure_or_headroom")), mock.patch.object(POLICY.subprocess, "Popen") as launch: + result = POLICY.execute_case(manifest(), case(), fake, None) + self.assertFalse(result["executed"]) + self.assertFalse(result["passed"]) + self.assertTrue(result["reaped"]) + self.assertIn("pressure_or_headroom", result["blocked"]) + launch.assert_not_called() + samples = [POLICY.Sensor(1_000_000_000 + n * 1_000_000_000, 1, POLICY.FREE_MIN, 100) for n in range(3)] + with mock.patch.object(POLICY.selectors, "DefaultSelector", return_value=FakeSelector()), mock.patch.object(POLICY, "sample", side_effect=samples), mock.patch.object(POLICY.time, "sleep"), mock.patch.object(POLICY.time, "monotonic_ns", return_value=3_000_000_001), mock.patch.object(POLICY, "owned_path", side_effect=POLICY.SafetyError("fixture_changed")), mock.patch.object(POLICY.subprocess, "Popen") as launch: + result = POLICY.execute_case(manifest(), case(), fake, None) + self.assertFalse(result["executed"]) + self.assertFalse(result["passed"]) + self.assertIn("fixture_changed", result["blocked"]) + self.assertEqual(len(result["sensors"]), 3) + launch.assert_not_called() + + def test_pipe_deadline_and_partial_writes_are_pure(self) -> None: + with mock.patch.object(POLICY.time, "monotonic", return_value=1), mock.patch.object(POLICY.os, "write", side_effect=[2, 3]) as writer: + POLICY.write_pipe(7, b"12345", 1.1) + self.assertEqual(writer.call_count, 2) + with mock.patch.object(POLICY.time, "monotonic", return_value=1), mock.patch.object(POLICY.os, "write", side_effect=[BlockingIOError(), 3]), mock.patch.object(POLICY.select, "select", return_value=([], [7], [])) as poll: + POLICY.write_pipe(7, b"123", 1.1) + self.assertEqual(poll.call_count, 1) + with mock.patch.object(POLICY.time, "monotonic", return_value=1), mock.patch.object(POLICY.os, "write") as writer: + with self.assertRaises(POLICY.SafetyError): + POLICY.write_pipe(7, b"123", 0.9) + writer.assert_not_called() + + def test_owned_popen_fallback_and_group_probe_are_pure(self) -> None: + fake = types.ModuleType("fake_psutil") + fake.NoSuchProcess = FakePsutilError + child = mock.Mock() + child.poll.return_value = None + child.wait.return_value = 0 + child.returncode = 0 + result = {"reaped": False, "descendants": [], "descendant_containment": "not_observed"} + with mock.patch.object(POLICY.time, "monotonic", return_value=1): + POLICY.terminate_owned(child, None, [], fake, result) + child.send_signal.assert_called_once_with(POLICY.signal.SIGTERM) + self.assertTrue(result["reaped"]) + self.assertEqual(result["termination_authority"], "unreaped_direct_popen_child") + identity = POLICY.Identity(100, 42, 123, 100) + changed = POLICY.Identity(100, 42, 124, 100) + child = mock.Mock() + child.poll.return_value = None + result = {"reaped": False, "descendants": [], "descendant_containment": "not_observed"} + with mock.patch.object(POLICY.time, "monotonic", return_value=1), mock.patch.object(POLICY, "observe_identity", return_value=changed), mock.patch.object(POLICY.os, "getuid", return_value=42), mock.patch.object(POLICY.os, "killpg") as signal_group: + with self.assertRaises(POLICY.SafetyError): + POLICY.terminate_owned(child, identity, [], fake, result) + child.send_signal.assert_not_called() + signal_group.assert_not_called() + with mock.patch.object(POLICY.os, "killpg", side_effect=ProcessLookupError()): + self.assertTrue(POLICY.group_gone(100)) + with mock.patch.object(POLICY.os, "killpg", return_value=None): + self.assertFalse(POLICY.group_gone(100)) + with mock.patch.object(POLICY.os, "killpg", side_effect=PermissionError()): + self.assertFalse(POLICY.group_gone(100)) + member = POLICY.Identity(101, 42, 123, 100) + self.assertTrue(POLICY.member_identity_matches(member, member, 100, 42)) + self.assertFalse(POLICY.member_identity_matches(member, member, 101, 42)) + + def test_protocol_bounds_use_no_real_stdio(self) -> None: + protocol = BOOT.Protocol() + with mock.patch.object(BOOT.os, "write", side_effect=lambda fd, data: len(data)) as writer: + protocol.emit({"type": "synthetic"}) + self.assertEqual(writer.call_count, 1) + with self.assertRaises(BOOT.BootstrapError): + protocol.emit({"payload": "x" * 2048}) + protocol.written_bytes = BOOT.LIMITS["output_bytes"] + with self.assertRaises(BOOT.BootstrapError): + protocol.emit({"type": "synthetic"}) + with mock.patch.object(BOOT.select, "select", return_value=([], [], [])): + with self.assertRaises(BOOT.BootstrapError): + BOOT.Protocol().receive(2048, 0.1) + protocol = BOOT.Protocol() + with mock.patch.object(BOOT.select, "select", return_value=([0], [], [])), mock.patch.object(BOOT.os, "read", return_value=b"x" * 2049): + with self.assertRaises(BOOT.BootstrapError): + protocol.receive(2048, 0.1) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-mac-native-acceptance-v2.py b/tests/test-mac-native-acceptance-v2.py new file mode 100644 index 00000000..ff7c4822 --- /dev/null +++ b/tests/test-mac-native-acceptance-v2.py @@ -0,0 +1,525 @@ +"""Pure Stage A policy tests. Never launch a child, signal, sensor or fixture.""" +from __future__ import annotations + +import ast +import copy +import json +import os +import stat +import sys +import types +import unittest +from contextlib import ExitStack, contextmanager +from pathlib import Path +from typing import Iterator +from unittest import mock + + +def load_source(name: str, filename: str) -> types.ModuleType: + path = Path(__file__).parent / "native" / filename + module = types.ModuleType(name) + module.__file__ = str(path) + sys.modules[name] = module + exec(compile(path.read_bytes(), str(path), "exec", dont_inherit=True), module.__dict__) + return module + + +POLICY = load_source("native_acceptance_policy_test_v2", "mac-native-acceptance-v2.py") +BOOT = load_source("native_acceptance_bootstrap_test_v2", "mac-native-bootstrap-v2.py") + + +def case(kind: str = "capture-a1") -> dict: + name = {"capture-revision": "capture-a2", "invalid-state": "negative-a3"}.get(kind, kind) + database = "capture-a2" if kind == "capture-revision" else "negative-a3" if kind == "invalid-state" else kind + row = {"id": kind, "source": str(POLICY.RUN_ROOT / "home/.claude/projects/native-acceptance-v1" / (name + ".jsonl")), + "source_sha256": "a" * 64, "source_bytes": 24, "source_identity": [1, 2, 24, 3, 4], + "session": "synthetic-native-" + name, "database": str(POLICY.RUN_ROOT / (database + "-v1.db")), "previous": None} + if kind == "flock-a4": + for field in ("source", "source_sha256", "source_bytes", "source_identity"): + row[field] = None + if kind == "capture-revision": + row["previous"] = {"key": "b" * 64, "sha256": "c" * 64, "byte_count": 12} + return row + + +def manifest() -> dict: + return {"version": 1, "stage": "A", "synthetic": True, "root_execution_go": True, + "run_root": str(POLICY.RUN_ROOT), "home": str(POLICY.RUN_ROOT / "home"), + "source_root": str(POLICY.SOURCE_ROOT), "source_commit": "a" * 40, + "sources": {name: "a" * 64 for name in POLICY.SOURCE_FILES}, + "supervisor_sha256": "b" * 64, "bootstrap_sha256": "c" * 64, + "limits": dict(POLICY.LIMITS), "cases": [case()]} + + +def mutation(seq: int, event: str, path: Path, flags: int = 0, source: Path | None = None) -> dict: + return {"type": "mutation", "version": 1, "seq": seq, "event": event, + "path": str(path), "flags": flags, "source": str(source) if source is not None else None} + + +class FakeProtocol: + def __init__(self) -> None: + self.sent: list[object] = [] + self.answers: list[dict] = [] + + def emit(self, value: object) -> None: + self.sent.append(value) + + def receive(self, maximum: int, seconds: float) -> dict: + if maximum != 2048 or seconds != 0.1: + raise AssertionError("Changed frame or ACK bound") + return self.answers.pop(0) + + +class FakeSelector: + def close(self) -> None: + return None + + +class FakePsutilError(Exception): + pass + + +@contextmanager +def main_scope(version: str = "v3", fixture: dict | None = None) -> Iterator[dict[str, mock.Mock]]: + row = copy.deepcopy(fixture) if fixture is not None else manifest() + parser = mock.Mock() + parser.parse_args.return_value = types.SimpleNamespace(manifest=str(POLICY.RUN_ROOT / ("manifest-" + version + ".json")), execute_root_authorized=True) + fake = types.ModuleType("fake_psutil") + fake.Error = FakePsutilError + with ExitStack() as patches: + patches.enter_context(mock.patch.object(POLICY.argparse, "ArgumentParser", return_value=parser)) + patches.enter_context(mock.patch.object(POLICY.sys, "platform", "darwin")) + patches.enter_context(mock.patch.object(POLICY.sys, "flags", types.SimpleNamespace(isolated=True, no_site=True, dont_write_bytecode=True))) + patches.enter_context(mock.patch.object(POLICY.sys, "dont_write_bytecode", True)) + patches.enter_context(mock.patch.object(POLICY.os, "umask", return_value=0o077)) + calls = { + "owned_path": patches.enter_context(mock.patch.object(POLICY, "owned_path")), + "read_bounded": patches.enter_context(mock.patch.object(POLICY, "read_bounded", return_value=POLICY.encoded(row))), + "digest": patches.enter_context(mock.patch.object(POLICY, "digest", return_value="b" * 64)), + "new_log": patches.enter_context(mock.patch.object(POLICY, "new_log", return_value=7)), + "pinned_runtime": patches.enter_context(mock.patch.object(POLICY, "pinned_runtime", return_value=fake)), + "PressureReader": patches.enter_context(mock.patch.object(POLICY, "PressureReader")), + "execute_case": patches.enter_context(mock.patch.object(POLICY, "execute_case", return_value={"passed": False, "reaped": True, "executed": False, "blocked": "pressure_or_headroom"})), + "write_all": patches.enter_context(mock.patch.object(POLICY, "write_all")), + "fsync": patches.enter_context(mock.patch.object(POLICY.os, "fsync")), + "close": patches.enter_context(mock.patch.object(POLICY.os, "close")), + } + yield calls + + +class PurePolicyTests(unittest.TestCase): + def test_fixed_limits_and_proof_scope(self) -> None: + self.assertEqual(POLICY.LIMITS, BOOT.LIMITS) + self.assertEqual(POLICY.FREE_MIN - POLICY.LIMITS["rss_abort_bytes"], 201326592) + self.assertEqual(POLICY.PROOF_SCOPE, BOOT.PROOF_SCOPE) + self.assertEqual(set(POLICY.SOURCE_FILES), {path for _, path in BOOT.SOURCE_MODULES}) + self.assertNotIn("installed", POLICY.PROOF_SCOPE) + + def test_exact_integer_and_hash(self) -> None: + for value in (True, False, 1.0, None, -1, 1 << 80): + with self.subTest(value=value), self.assertRaises(POLICY.SafetyError): + POLICY.integer(value) + for value in (True, "A" * 64, "a" * 63, "a" * 65, None): + with self.subTest(value=value), self.assertRaises(POLICY.SafetyError): + POLICY.hash_text(value) + self.assertEqual(POLICY.integer(0), 0) + self.assertEqual(POLICY.hash_text("a" * 64), "a" * 64) + + def test_sensor_admission_exact_floor_and_abi(self) -> None: + samples = [POLICY.Sensor(1_000_000_000 + n * 1_000_000_000, 1, POLICY.FREE_MIN, 100) for n in range(3)] + POLICY.admission(samples, 3_500_000_000) + changes = [("pressure", 0), ("pressure", 2), ("pressure", 3), ("pressure", 4), + ("pressure", True), ("free_bytes", POLICY.FREE_MIN - 1), + ("free_bytes", True), ("swap_used_bytes", 101), ("monotonic_ns", 1_000_000_000)] + for field, value in changes: + altered = [POLICY.asdict(row) for row in samples] + altered[-1][field] = value + with self.subTest(field=field, value=value), self.assertRaises(POLICY.SafetyError): + POLICY.admission([POLICY.Sensor(**row) for row in altered], 3_500_000_000) + for now in (2_999_999_999, 3_500_000_001): + with self.subTest(now=now), self.assertRaises(POLICY.SafetyError): + POLICY.admission(samples, now) + with self.assertRaises(POLICY.SafetyError): + POLICY.admission(samples[:2], 3_500_000_000) + with self.assertRaises(POLICY.SafetyError): + POLICY.admission([POLICY.Sensor(1 + n, 1, POLICY.FREE_MIN, 0) for n in range(3)], 3) + + def test_manifest_and_case_fences(self) -> None: + self.assertEqual(POLICY.validate_manifest(manifest()), manifest()) + for kind in POLICY.CASES: + POLICY.validate_case(case(kind)) + changes = [("version", True), ("stage", "B"), ("synthetic", False), ("root_execution_go", False), + ("run_root", "/tmp"), ("source_root", "/Users/j/dev/TrueMemory"), + ("sources", {}), ("cases", []), ("cases", [case(), case()])] + for field, value in changes: + altered = manifest() + altered[field] = value + with self.subTest(field=field), self.assertRaises(POLICY.SafetyError): + POLICY.validate_manifest(altered) + for field, value in (("id", []), ("session", "unknown"), ("database", "/tmp/personal.db"), + ("source", "/Users/j/dev/TrueMemory/transcript.jsonl"), ("source_bytes", True), + ("source_identity", [1, 2, 23, 3, 4])): + altered = case() + altered[field] = value + with self.subTest(field=field), self.assertRaises(POLICY.SafetyError): + POLICY.validate_case(altered) + altered = manifest() + altered["limits"]["wall_seconds"] = 6 + with self.assertRaises(POLICY.SafetyError): + POLICY.validate_manifest(altered) + altered = manifest() + altered["limits"]["core_bytes"] = False + with self.assertRaises(POLICY.SafetyError): + POLICY.validate_manifest(altered) + + def test_paths_and_versioned_results(self) -> None: + for name in ("v" + str(number) for number in range(1, 100)): + self.assertEqual(POLICY.evidence_paths(POLICY.RUN_ROOT / ("manifest-" + name + ".json")), + (POLICY.RUN_ROOT / ("results-" + name + ".json"), name)) + for text in ("relative", str(POLICY.RUN_ROOT) + "/../outside", str(POLICY.RUN_ROOT) + "//escape", "/tmp/file"): + with self.subTest(text=text), self.assertRaises(POLICY.SafetyError): + POLICY.canonical_path(text, POLICY.RUN_ROOT) + for name in ("v0", "v01", "v100", "v3x", "v3\n", "v3", "v٣"): + with self.subTest(name=name), self.assertRaises(POLICY.SafetyError): + POLICY.evidence_paths(POLICY.RUN_ROOT / ("manifest-" + name + ".json")) + with self.assertRaises(POLICY.SafetyError): + POLICY.evidence_paths(POLICY.RUN_ROOT.parent / "manifest-v3.json") + + def test_identity_is_current_owned_incarnation(self) -> None: + row = POLICY.Identity(72887, 42, 12345, 72887) + self.assertTrue(POLICY.same_owned_identity(row, row, 42)) + for changed in (POLICY.Identity(72887, 42, 12346, 72887), POLICY.Identity(72887, 43, 12345, 72887), + POLICY.Identity(72887, 42, 12345, 1), POLICY.Identity(True, 42, 12345, 1)): + with self.subTest(changed=changed): + self.assertFalse(POLICY.same_owned_identity(row, changed, 42)) + + def test_ready_and_result_exact_protocol(self) -> None: + ready = {"type": "ready", "version": 1, "pid": 100, "audit_installed": True, + "limits": {"cpu": [2, 3], "core": [0, 0], "nofile": [64, 64], "fsize": [1048576, 1048576]}} + POLICY.validate_ready(ready, 100) + for field, value in (("pid", 101), ("version", True), ("audit_installed", 1), ("limits", {})): + altered = copy.deepcopy(ready) + altered[field] = value + with self.subTest(field=field), self.assertRaises(POLICY.SafetyError): + POLICY.validate_ready(altered, 100) + altered = copy.deepcopy(ready) + altered["limits"]["core"] = [False, False] + with self.assertRaises(POLICY.SafetyError): + POLICY.validate_ready(altered, 100) + done = {"type": "result", "version": 1, "case": "capture-a1", "passed": True, + "proof_scope": POLICY.PROOF_SCOPE, "import_mode": "isolated_source_module", "sources": manifest()["sources"], "details": {}} + POLICY.validate_result(done, case(), manifest()["sources"]) + for field, value in (("passed", 1), ("version", True), ("proof_scope", "installed_host"), + ("case", "capture-a2"), ("sources", {}), ("details", [])): + altered = dict(done) + altered[field] = value + with self.subTest(field=field), self.assertRaises(POLICY.SafetyError): + POLICY.validate_result(altered, case(), manifest()["sources"]) + + def test_mutation_positive_and_one_pending_slot(self) -> None: + policy = POLICY.MutationPolicy(case()) + policy.decide(mutation(1, "os.mkdir", policy.account, 0o700)) + lock = policy.account / "state.lock" + policy.decide(mutation(2, "open", lock, os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW)) + pending = policy.account / ("state-" + "a" * 32 + ".pending") + policy.decide(mutation(3, "open", pending, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW)) + grant = policy.decide(mutation(4, "os.rename", policy.account / "state.json", source=pending)) + self.assertEqual(grant["path"], str(policy.account / "state.json")) + with self.assertRaises(POLICY.SafetyError): + policy.decide(mutation(5, "open", policy.account / ("state-" + "b" * 32 + ".pending"), os.O_WRONLY | os.O_CREAT | os.O_EXCL)) + + def test_mutation_negative_controls(self) -> None: + for event, filename, flags, source in (("open", "state.json", os.O_WRONLY | os.O_CREAT, None), + ("open", "state.lock", os.O_RDWR | os.O_CREAT | os.O_TRUNC, None), + ("open", "unknown", os.O_WRONLY | os.O_CREAT | os.O_EXCL, None), + ("os.mkdir", "foreign", 0o700, None), + ("os.mkdir", "captures", 0o755, None), + ("os.rename", "state.json", 0, POLICY.RUN_ROOT / "arbitrary"), + ("unlink", "state.json", 0, None)): + policy = POLICY.MutationPolicy(case()) + with self.subTest(event=event, filename=filename), self.assertRaises(POLICY.SafetyError): + policy.decide(mutation(1, event, policy.account / filename, flags, source)) + for sequence in (0, 2, True, 33): + policy = POLICY.MutationPolicy(case()) + with self.subTest(sequence=sequence), self.assertRaises(POLICY.SafetyError): + policy.decide(mutation(sequence, "os.mkdir", policy.account, 0o700)) + policy = POLICY.MutationPolicy(case("negative-a3")) + with self.assertRaises(POLICY.SafetyError): + policy.decide(mutation(1, "os.mkdir", policy.account, 0o700)) + + def test_fd_wrappers_bind_latest_consumed_intent(self) -> None: + import fcntl + row = case() + guard = BOOT.AuditGuard(FakeProtocol(), {"case": row}, POLICY) + path = Path(row["source"]) + info = types.SimpleNamespace(st_dev=1, st_ino=2, st_uid=42, st_mode=stat.S_IFREG | 0o600) + with mock.patch.object(BOOT.os, "fstat", return_value=info), mock.patch.object(BOOT.os, "getuid", return_value=42), mock.patch.object(fcntl, "fcntl", return_value=os.O_RDONLY), mock.patch.object(Path, "lstat", return_value=info), mock.patch.object(Path, "is_symlink", return_value=False): + guard.open_intents[:] = [(path, os.O_RDONLY)] + guard.descriptor(7, "r") + self.assertEqual(guard.known_fds, {7: (1, 2)}) + with self.assertRaises(BOOT.BootstrapError): + guard.descriptor(7, "rb") + info.st_ino = 3 + guard.open_intents[:] = [(path, os.O_RDONLY)] + guard.descriptor(7, "rb") + self.assertEqual(guard.known_fds, {7: (1, 3)}) + guard.open_intents[:] = [(path, os.O_WRONLY)] + with self.assertRaises(BOOT.BootstrapError): + guard.descriptor(7, "rb") + self.assertFalse(guard.open_intents) + for fd, mode in ((0, "r"), (7, "a"), (7, "r+"), (True, "r")): + with self.subTest(fd=fd, mode=mode), self.assertRaises(BOOT.BootstrapError): + guard.descriptor(fd, mode) + + def test_audit_preopen_grant_and_denials(self) -> None: + protocol = FakeProtocol() + guard = BOOT.AuditGuard(protocol, {"case": case()}, POLICY) + path = POLICY.account_directory(case()) / "state.lock" + protocol.answers.append({"type": "grant", "version": 1, "seq": 1, "path": str(path), "purpose": "Owned fixed lock"}) + guard.path = lambda value: Path(value) + guard("open", (str(path), None, os.O_RDWR | os.O_CREAT)) + self.assertEqual(len(protocol.sent), 1) + self.assertEqual(guard.open_intents, [(path, os.O_RDWR)]) + guard("truememory.native.audit_probe", ("fixed-stage-a-v1",)) + self.assertTrue(guard.activated) + for event, args in (("socket.connect", ()), ("subprocess.Popen", ()), ("os.remove", (str(path), -1)), + ("sqlite3.connect", ("memory",)), ("resource.setrlimit", (0, (1, 1))), + ("import", ("truememory.engine", None)), ("import", ("torch", None)), + ("os.mkdir", (str(path), 0o700, 1)), ("os.rename", (str(path), str(path), 1, -1))): + with self.subTest(event=event), self.assertRaises(BOOT.BootstrapError): + guard(event, args) + + def test_prelaunch_refusals_retain_nonexecuted_result(self) -> None: + fake = types.ModuleType("fake_psutil") + fake.Error = FakePsutilError + with mock.patch.object(POLICY.selectors, "DefaultSelector", return_value=FakeSelector()), mock.patch.object(POLICY, "sample", side_effect=POLICY.SafetyError("pressure_or_headroom")), mock.patch.object(POLICY.subprocess, "Popen") as launch: + result = POLICY.execute_case(manifest(), case(), fake, None) + self.assertFalse(result["executed"]) + self.assertFalse(result["passed"]) + self.assertTrue(result["reaped"]) + self.assertIn("pressure_or_headroom", result["blocked"]) + launch.assert_not_called() + samples = [POLICY.Sensor(1_000_000_000 + n * 1_000_000_000, 1, POLICY.FREE_MIN, 100) for n in range(3)] + with mock.patch.object(POLICY.selectors, "DefaultSelector", return_value=FakeSelector()), mock.patch.object(POLICY, "sample", side_effect=samples), mock.patch.object(POLICY.time, "sleep"), mock.patch.object(POLICY.time, "monotonic_ns", return_value=3_000_000_001), mock.patch.object(POLICY, "owned_path", side_effect=POLICY.SafetyError("fixture_changed")), mock.patch.object(POLICY.subprocess, "Popen") as launch: + result = POLICY.execute_case(manifest(), case(), fake, None) + self.assertFalse(result["executed"]) + self.assertFalse(result["passed"]) + self.assertIn("fixture_changed", result["blocked"]) + self.assertEqual(len(result["sensors"]), 3) + launch.assert_not_called() + + def test_pipe_deadline_and_partial_writes_are_pure(self) -> None: + with mock.patch.object(POLICY.time, "monotonic", return_value=1), mock.patch.object(POLICY.os, "write", side_effect=[2, 3]) as writer: + POLICY.write_pipe(7, b"12345", 1.1) + self.assertEqual(writer.call_count, 2) + with mock.patch.object(POLICY.time, "monotonic", return_value=1), mock.patch.object(POLICY.os, "write", side_effect=[BlockingIOError(), 3]), mock.patch.object(POLICY.select, "select", return_value=([], [7], [])) as poll: + POLICY.write_pipe(7, b"123", 1.1) + self.assertEqual(poll.call_count, 1) + with mock.patch.object(POLICY.time, "monotonic", return_value=1), mock.patch.object(POLICY.os, "write") as writer: + with self.assertRaises(POLICY.SafetyError): + POLICY.write_pipe(7, b"123", 0.9) + writer.assert_not_called() + + def test_owned_popen_fallback_and_group_probe_are_pure(self) -> None: + fake = types.ModuleType("fake_psutil") + fake.NoSuchProcess = FakePsutilError + child = mock.Mock() + child.poll.return_value = None + child.wait.return_value = 0 + child.returncode = 0 + result = {"reaped": False, "descendants": [], "descendant_containment": "not_observed"} + with mock.patch.object(POLICY.time, "monotonic", return_value=1): + POLICY.terminate_owned(child, None, [], fake, result) + child.send_signal.assert_called_once_with(POLICY.signal.SIGTERM) + self.assertTrue(result["reaped"]) + self.assertEqual(result["termination_authority"], "unreaped_direct_popen_child") + identity = POLICY.Identity(100, 42, 123, 100) + changed = POLICY.Identity(100, 42, 124, 100) + child = mock.Mock() + child.poll.return_value = None + result = {"reaped": False, "descendants": [], "descendant_containment": "not_observed"} + with mock.patch.object(POLICY.time, "monotonic", return_value=1), mock.patch.object(POLICY, "observe_identity", return_value=changed), mock.patch.object(POLICY.os, "getuid", return_value=42), mock.patch.object(POLICY.os, "killpg") as signal_group: + with self.assertRaises(POLICY.SafetyError): + POLICY.terminate_owned(child, identity, [], fake, result) + child.send_signal.assert_not_called() + signal_group.assert_not_called() + with mock.patch.object(POLICY.os, "killpg", side_effect=ProcessLookupError()): + self.assertTrue(POLICY.group_gone(100)) + with mock.patch.object(POLICY.os, "killpg", return_value=None): + self.assertFalse(POLICY.group_gone(100)) + with mock.patch.object(POLICY.os, "killpg", side_effect=PermissionError()): + self.assertFalse(POLICY.group_gone(100)) + member = POLICY.Identity(101, 42, 123, 100) + self.assertTrue(POLICY.member_identity_matches(member, member, 100, 42)) + self.assertFalse(POLICY.member_identity_matches(member, member, 101, 42)) + + def test_protocol_bounds_use_no_real_stdio(self) -> None: + protocol = BOOT.Protocol() + with mock.patch.object(BOOT.os, "write", side_effect=lambda fd, data: len(data)) as writer: + protocol.emit({"type": "synthetic"}) + self.assertEqual(writer.call_count, 1) + with self.assertRaises(BOOT.BootstrapError): + protocol.emit({"payload": "x" * 2048}) + protocol.written_bytes = BOOT.LIMITS["output_bytes"] + with self.assertRaises(BOOT.BootstrapError): + protocol.emit({"type": "synthetic"}) + with mock.patch.object(BOOT.select, "select", return_value=([], [], [])): + with self.assertRaises(BOOT.BootstrapError): + BOOT.Protocol().receive(2048, 0.1) + protocol = BOOT.Protocol() + with mock.patch.object(BOOT.select, "select", return_value=([0], [], [])), mock.patch.object(BOOT.os, "read", return_value=b"x" * 2049): + with self.assertRaises(BOOT.BootstrapError): + protocol.receive(2048, 0.1) + + + def test_attempt_versions_are_canonical_before_io(self) -> None: + for number in range(1, 100): + self.assertEqual(POLICY.validate_attempt_version("v" + str(number)), "v" + str(number)) + fake = types.ModuleType("fake_psutil") + fake.Error = FakePsutilError + invalid = (True, False, None, 3, b"v3", [], "v0", "v00", "v01", "v100", "V3", "v3\n", "v3", "v٣", "../v3", "v3.log") + for version in invalid: + with self.subTest(version=version), mock.patch.object(POLICY.selectors, "DefaultSelector") as selector, mock.patch.object(POLICY, "sample") as sensor, mock.patch.object(POLICY, "new_log") as writer, mock.patch.object(POLICY.subprocess, "Popen") as launch: + with self.assertRaises(POLICY.SafetyError): + POLICY.validate_attempt_version(version) + with self.assertRaises(POLICY.SafetyError): + POLICY.execute_case(manifest(), case(), fake, None, attempt_version=version) + selector.assert_not_called() + sensor.assert_not_called() + writer.assert_not_called() + launch.assert_not_called() + + def test_actual_retry_logs_and_legacy_default(self) -> None: + fake = types.ModuleType("fake_psutil") + fake.Error = FakePsutilError + info = types.SimpleNamespace(st_dev=1, st_ino=2, st_size=24, st_mtime_ns=3, st_ctime_ns=4) + samples = [POLICY.Sensor(1_000_000_000 + n * 1_000_000_000, 1, POLICY.FREE_MIN, 100) for n in range(3)] + fixture = case() + fixture_bytes = b"x" * 24 + fixture["source_sha256"] = POLICY.digest(fixture_bytes) + plan = manifest() + source_bytes = {POLICY.SOURCE_ROOT / name: ("synthetic:" + name).encode("utf-8") for name in POLICY.SOURCE_FILES} + plan["sources"] = {name: POLICY.digest(source_bytes[POLICY.SOURCE_ROOT / name]) for name in POLICY.SOURCE_FILES} + bootstrap_path = POLICY.SOURCE_ROOT / "tests/native/mac-native-bootstrap-v2.py" + bootstrap_bytes = b"fixed-bootstrap-v2" + plan["bootstrap_sha256"] = POLICY.digest(bootstrap_bytes) + + def read_fixture(path: Path, maximum: int, *, private: bool = True) -> bytes: + if path == Path(fixture["source"]): + self.assertEqual((maximum, private), (4096, True)) + return fixture_bytes + self.assertEqual((maximum, private), (262144, False)) + if path == bootstrap_path: + return bootstrap_bytes + if path in source_bytes: + return source_bytes[path] + raise AssertionError("Unplanned synthetic source read") + + for version in (None, "v3", "v99"): + expected = version if version is not None else "v1" + with self.subTest(version=version), ExitStack() as patches: + patches.enter_context(mock.patch.object(POLICY.selectors, "DefaultSelector", return_value=FakeSelector())) + patches.enter_context(mock.patch.object(POLICY, "sample", side_effect=samples)) + patches.enter_context(mock.patch.object(POLICY.time, "sleep")) + patches.enter_context(mock.patch.object(POLICY.time, "monotonic_ns", return_value=3_000_000_001)) + patches.enter_context(mock.patch.object(POLICY, "owned_path", return_value=info)) + reader = patches.enter_context(mock.patch.object(POLICY, "read_bounded", side_effect=read_fixture)) + writer = patches.enter_context(mock.patch.object(POLICY, "new_log", side_effect=[7, 8, POLICY.SafetyError("stop_before_launch")])) + patches.enter_context(mock.patch.object(POLICY.os, "fsync")) + patches.enter_context(mock.patch.object(POLICY.os, "close")) + launch = patches.enter_context(mock.patch.object(POLICY.subprocess, "Popen")) + result = POLICY.execute_case(plan, fixture, fake, None) if version is None else POLICY.execute_case(plan, fixture, fake, None, attempt_version=version) + self.assertEqual([call.args[0] for call in writer.call_args_list], [POLICY.RUN_ROOT / "logs" / ("capture-a1-" + channel + "-" + expected + ".log") for channel in ("stdout", "stderr", "events")]) + self.assertEqual(reader.call_args_list[-1].args[0], POLICY.SOURCE_ROOT / "tests/native/mac-native-bootstrap-v2.py") + self.assertFalse(result["executed"]) + self.assertFalse(result["passed"]) + self.assertTrue(result["reaped"]) + self.assertIn("stop_before_launch", result["blocked"]) + launch.assert_not_called() + + def test_main_reserves_result_before_runtime_and_case(self) -> None: + with main_scope("v3") as calls: + def runtime() -> types.ModuleType: + self.assertEqual(calls["new_log"].call_count, 1) + calls["execute_case"].assert_not_called() + fake = types.ModuleType("fake_psutil") + fake.Error = FakePsutilError + return fake + calls["pinned_runtime"].side_effect = runtime + self.assertEqual(POLICY.main(), 1) + self.assertEqual(calls["new_log"].call_args.args[0], POLICY.RUN_ROOT / "results-v3.json") + calls["new_log"].assert_called_once() + self.assertEqual(calls["execute_case"].call_args.kwargs, {"attempt_version": "v3"}) + payload = json.loads(calls["write_all"].call_args.args[1]) + self.assertEqual(payload["manifest_version"], "v3") + self.assertFalse(payload["complete"]) + self.assertFalse(payload["cases"][0]["executed"]) + calls["fsync"].assert_called_once_with(7) + calls["close"].assert_called_once_with(7) + + def test_main_existing_result_refuses_without_runtime(self) -> None: + with main_scope("v3") as calls: + calls["new_log"].side_effect = FileExistsError("retained_attempt") + with self.assertRaises(FileExistsError): + POLICY.main() + for name in ("pinned_runtime", "PressureReader", "execute_case", "write_all", "fsync", "close"): + calls[name].assert_not_called() + + def test_main_runtime_refusal_retains_reserved_result(self) -> None: + with main_scope("v99") as calls: + calls["pinned_runtime"].side_effect = POLICY.SafetyError("runtime_refusal") + self.assertEqual(POLICY.main(), 1) + payload = json.loads(calls["write_all"].call_args.args[1]) + self.assertEqual(payload["manifest_version"], "v99") + self.assertEqual(payload["cases"], []) + self.assertIn("runtime_refusal", payload["setup_failure"]) + self.assertFalse(payload["complete"]) + calls["execute_case"].assert_not_called() + calls["fsync"].assert_called_once_with(7) + calls["close"].assert_called_once_with(7) + + def test_main_fatal_retains_incomplete_reserved_result(self) -> None: + with main_scope("v3") as calls: + calls["pinned_runtime"].side_effect = RuntimeError("fatal_inert_fixture") + with self.assertRaisesRegex(RuntimeError, "fatal_inert_fixture"): + POLICY.main() + calls["new_log"].assert_called_once() + calls["write_all"].assert_not_called() + calls["execute_case"].assert_not_called() + calls["fsync"].assert_called_once_with(7) + calls["close"].assert_called_once_with(7) + + def test_main_revision_only_reserved_for_v2(self) -> None: + revision = manifest() + revised = case("capture-revision") + revised["previous"]["key"] = "d" * 64 + revision["cases"] = [revised] + for version in ("v1", "v3", "v99"): + with self.subTest(version=version), main_scope(version, revision) as calls: + with self.assertRaisesRegex(POLICY.SafetyError, "unexpected_manifest_case_stage"): + POLICY.main() + calls["new_log"].assert_not_called() + calls["pinned_runtime"].assert_not_called() + with main_scope("v2") as calls: + with self.assertRaisesRegex(POLICY.SafetyError, "unexpected_manifest_case_stage"): + POLICY.main() + calls["new_log"].assert_not_called() + with main_scope("v2", revision) as calls: + calls["pinned_runtime"].side_effect = POLICY.SafetyError("controlled_stage_stop") + self.assertEqual(POLICY.main(), 1) + self.assertEqual(calls["new_log"].call_args.args[0], POLICY.RUN_ROOT / "results-v2.json") + + def test_bootstrap_uses_actual_v2_policy(self) -> None: + guard = BOOT.AuditGuard(FakeProtocol(), {"case": case()}, POLICY) + self.assertIn(POLICY.SOURCE_ROOT / "tests/native/mac-native-acceptance-v2.py", guard.read_paths) + self.assertNotIn(POLICY.SOURCE_ROOT / "tests/native/mac-native-acceptance-v1.py", guard.read_paths) + tree = ast.parse(Path(BOOT.__file__).read_bytes()) + calls = [node for node in ast.walk(tree) if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "compile_module"] + policy = [node for node in calls if node.args and isinstance(node.args[0], ast.Constant) and node.args[0].value == "mac_native_policy_v2"] + self.assertEqual(len(policy), 1) + self.assertIsInstance(policy[0].args[1], ast.BinOp) + self.assertEqual(policy[0].args[1].right.value, "tests/native/mac-native-acceptance-v2.py") + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-mac-native-acceptance-v4.py b/tests/test-mac-native-acceptance-v4.py new file mode 100644 index 00000000..84fdd4ac --- /dev/null +++ b/tests/test-mac-native-acceptance-v4.py @@ -0,0 +1,1273 @@ +"""Pure Stage A policy tests. Never launch a child, signal, sensor or fixture.""" + +from __future__ import annotations + +import ast +import copy +import json +import os +import stat +import sys +import types +import unittest +from contextlib import ExitStack, contextmanager +from pathlib import Path +from typing import Callable, Iterator +from unittest import mock + + +def load_source(name: str, filename: str) -> types.ModuleType: + path = Path(__file__).parent / "native" / filename + module = types.ModuleType(name) + module.__file__ = str(path) + sys.modules[name] = module + exec( + compile(path.read_bytes(), str(path), "exec", dont_inherit=True), + module.__dict__, + ) + return module + + +POLICY = load_source("native_acceptance_policy_test_v4", "mac-native-acceptance-v4.py") +BOOT = load_source("native_acceptance_bootstrap_test_v4", "mac-native-bootstrap-v4.py") + + +def case(kind: str = "capture-a1") -> dict: + name = {"capture-revision": "capture-a2", "invalid-state": "negative-a3"}.get( + kind, kind + ) + database = ( + "capture-a2" + if kind == "capture-revision" + else "negative-a3" + if kind == "invalid-state" + else kind + ) + row = { + "id": kind, + "source": str( + POLICY.RUN_ROOT + / "home/.claude/projects/native-acceptance-v1" + / (name + ".jsonl") + ), + "source_sha256": "a" * 64, + "source_bytes": 24, + "source_identity": [1, 2, 24, 3, 4], + "session": "synthetic-native-" + name, + "database": str(POLICY.RUN_ROOT / (database + "-v1.db")), + "previous": None, + } + if kind == "flock-a4": + for field in ("source", "source_sha256", "source_bytes", "source_identity"): + row[field] = None + if kind == "capture-revision": + row["previous"] = {"key": "b" * 64, "sha256": "c" * 64, "byte_count": 12} + return row + + +def manifest() -> dict: + return { + "version": 1, + "stage": "A", + "synthetic": True, + "root_execution_go": True, + "run_root": str(POLICY.RUN_ROOT), + "home": str(POLICY.RUN_ROOT / "home"), + "source_root": str(POLICY.SOURCE_ROOT), + "source_commit": "a" * 40, + "sources": {name: "a" * 64 for name in POLICY.SOURCE_FILES}, + "supervisor_sha256": "b" * 64, + "bootstrap_sha256": "c" * 64, + "limits": dict(POLICY.LIMITS), + "cases": [case()], + } + + +def mutation( + seq: int, event: str, path: Path, flags: int = 0, source: Path | None = None +) -> dict: + return { + "type": "mutation", + "version": 1, + "seq": seq, + "event": event, + "path": str(path), + "flags": flags, + "source": str(source) if source is not None else None, + } + + +class FakeProtocol: + def __init__(self) -> None: + self.sent: list[object] = [] + self.answers: list[dict] = [] + + def emit(self, value: object) -> None: + self.sent.append(value) + + def receive(self, maximum: int, seconds: float) -> dict: + if maximum != 2048 or seconds != 0.1: + raise AssertionError("Changed frame or ACK bound") + return self.answers.pop(0) + + +class FakeSelector: + def close(self) -> None: + return None + + +class DirectoryProtocol(FakeProtocol): + def __init__(self, row: dict) -> None: + super().__init__() + self.parent = POLICY.MutationPolicy(row) + + def emit(self, value: object) -> None: + self.sent.append(value) + grant = self.parent.decide(value) + POLICY.mutation_target(grant, value) + self.answers.append(grant) + + +def actual_sync_module() -> types.ModuleType: + path = Path(__file__).parent.parent / "truememory/ingest/completion.py" + text = path.read_text() + node = next( + node + for node in ast.parse(text).body + if isinstance(node, ast.FunctionDef) and node.name == "_sync_directory" + ) + source = ast.get_source_segment(text, node) + if source is None: + raise AssertionError("Actual directory helper missing") + module = types.ModuleType("truememory.ingest.completion") + module.__file__ = str(BOOT.SOURCE_ROOT / "truememory/ingest/completion.py") + module.Path = Path + module.os = types.SimpleNamespace( + name="posix", O_RDONLY=os.O_RDONLY, O_DIRECTORY=os.O_DIRECTORY + ) + exec(compile(source, module.__file__, "exec", dont_inherit=True), module.__dict__) + return module + + +@contextmanager +def directory_scope( + kind: str = "capture-a1", after_open: Callable[[], None] | None = None +) -> Iterator[dict[str, object]]: + import fcntl + + row = case(kind) + protocol = DirectoryProtocol(row) + guard = BOOT.AuditGuard(protocol, {"case": row}, POLICY) + module = actual_sync_module() + adapter = BOOT.DirectorySyncAdapter(guard, module) + guard.directory_sync = adapter + target_info = types.SimpleNamespace( + st_dev=1, st_ino=2, st_uid=42, st_mode=stat.S_IFDIR | 0o700 + ) + fd_info = types.SimpleNamespace( + st_dev=1, st_ino=2, st_uid=42, st_mode=stat.S_IFDIR | 0o700 + ) + adapter.raw_fsync = mock.Mock() + adapter.raw_close = mock.Mock() + + def raw_open(path: object, flags: int, mode: int = 0o777) -> int: + guard("open", (os.fspath(path), None, flags | getattr(os, "O_CLOEXEC", 0))) + if after_open is not None: + after_open() + return 87 + + adapter.raw_open = mock.Mock(side_effect=raw_open) + module.os.open, module.os.fsync, module.os.close = ( + adapter.open, + adapter.fsync, + adapter.close, + ) + with ExitStack() as patches: + patches.enter_context(mock.patch.object(Path, "resolve", lambda value: value)) + patches.enter_context(mock.patch.object(Path, "is_symlink", return_value=False)) + patches.enter_context(mock.patch.object(BOOT.os, "getuid", return_value=42)) + patches.enter_context( + mock.patch.object(POLICY, "owned_path", return_value=target_info) + ) + fstat = patches.enter_context( + mock.patch.object(BOOT.os, "fstat", return_value=fd_info) + ) + access = patches.enter_context( + mock.patch.object(fcntl, "fcntl", return_value=os.O_RDONLY) + ) + yield { + "row": row, + "protocol": protocol, + "guard": guard, + "module": module, + "adapter": adapter, + "target_info": target_info, + "fd_info": fd_info, + "fstat": fstat, + "access": access, + } + + +class FakePsutilError(Exception): + pass + + +@contextmanager +def main_scope( + version: str = "v3", fixture: dict | None = None +) -> Iterator[dict[str, mock.Mock]]: + row = copy.deepcopy(fixture) if fixture is not None else manifest() + parser = mock.Mock() + parser.parse_args.return_value = types.SimpleNamespace( + manifest=str(POLICY.RUN_ROOT / ("manifest-" + version + ".json")), + execute_root_authorized=True, + ) + fake = types.ModuleType("fake_psutil") + fake.Error = FakePsutilError + with ExitStack() as patches: + patches.enter_context( + mock.patch.object(POLICY.argparse, "ArgumentParser", return_value=parser) + ) + patches.enter_context(mock.patch.object(POLICY.sys, "platform", "darwin")) + patches.enter_context( + mock.patch.object( + POLICY.sys, + "flags", + types.SimpleNamespace( + isolated=True, no_site=True, dont_write_bytecode=True + ), + ) + ) + patches.enter_context( + mock.patch.object(POLICY.sys, "dont_write_bytecode", True) + ) + patches.enter_context(mock.patch.object(POLICY.os, "umask", return_value=0o077)) + calls = { + "owned_path": patches.enter_context( + mock.patch.object(POLICY, "owned_path") + ), + "read_bounded": patches.enter_context( + mock.patch.object( + POLICY, "read_bounded", return_value=POLICY.encoded(row) + ) + ), + "digest": patches.enter_context( + mock.patch.object(POLICY, "digest", return_value="b" * 64) + ), + "new_log": patches.enter_context( + mock.patch.object(POLICY, "new_log", return_value=7) + ), + "pinned_runtime": patches.enter_context( + mock.patch.object(POLICY, "pinned_runtime", return_value=fake) + ), + "PressureReader": patches.enter_context( + mock.patch.object(POLICY, "PressureReader") + ), + "execute_case": patches.enter_context( + mock.patch.object( + POLICY, + "execute_case", + return_value={ + "passed": False, + "reaped": True, + "executed": False, + "blocked": "pressure_or_headroom", + }, + ) + ), + "write_all": patches.enter_context(mock.patch.object(POLICY, "write_all")), + "fsync": patches.enter_context(mock.patch.object(POLICY.os, "fsync")), + "close": patches.enter_context(mock.patch.object(POLICY.os, "close")), + } + yield calls + + +class PurePolicyTests(unittest.TestCase): + def test_directory_sync_live_bound_return_never_closes_or_replaces_prior_binding( + self, + ) -> None: + for original_identity in ((1, 2), (1, 99)): + with ( + self.subTest(original_identity=original_identity), + directory_scope() as scope, + ): + adapter = scope["adapter"] + account = POLICY.account_directory(scope["row"]) + previous = (account / "captures", original_identity, 1) + adapter.bindings[87] = previous + retained = {88: (1, 55, stat.S_IFDIR, 42)} + adapter.failed_open_bindings.update(retained) + with self.assertRaises(BOOT.BootstrapError): + scope["module"]._sync_directory(account) + adapter.raw_fsync.assert_not_called() + adapter.raw_close.assert_not_called() + self.assertEqual({87: previous}, adapter.bindings) + self.assertIs(adapter.bindings[87], previous) + self.assertEqual(retained, adapter.failed_open_bindings) + self.assertIsNone(adapter.pending) + self.assertEqual(1, len(adapter.failures)) + self.assertEqual(87, adapter.failures[0]["fd"]) + self.assertFalse(adapter.failures[0]["closed"]) + self.assertIn("bound_conflict", adapter.failures[0]["reason"]) + + def test_directory_sync_bound_conflict_retains_bounded_failure_even_without_observed_identity( + self, + ) -> None: + with directory_scope() as scope: + adapter = scope["adapter"] + previous = (POLICY.account_directory(scope["row"]), (1, 2), 1) + adapter.bindings[87] = previous + for observed in (None, scope["fd_info"]): + adapter.close_failed_open( + 87, observed, BOOT.BootstrapError("Synthetic conflict") + ) + adapter.raw_close.assert_not_called() + self.assertEqual({87: previous}, adapter.bindings) + self.assertFalse(adapter.failed_open_bindings) + self.assertEqual(2, len(adapter.failures)) + self.assertTrue( + all( + not row["closed"] and "bound_conflict" in row["reason"] + for row in adapter.failures + ) + ) + adapter.failures[:] = [ + { + "fd": 87, + "reason": "Retained synthetic bound conflict", + "closed": False, + } + for _ in range(BOOT.LIMITS["events"]) + ] + original = copy.deepcopy(adapter.failures) + with self.assertRaises(BOOT.BootstrapError): + adapter.close_failed_open( + 87, None, BOOT.BootstrapError("Over-limit conflict") + ) + self.assertEqual(original, adapter.failures) + self.assertEqual({87: previous}, adapter.bindings) + adapter.raw_close.assert_not_called() + + def test_directory_sync_exact_account_and_captures_parent_grants(self) -> None: + for kind in ("capture-a1", "capture-a2", "capture-revision"): + for child in (None, "captures"): + with ( + self.subTest(kind=kind, child=child), + directory_scope(kind) as scope, + ): + guard = scope["guard"] + adapter = scope["adapter"] + account = POLICY.account_directory(scope["row"]) + target = account if child is None else account / child + regular_intent = (Path(scope["row"]["source"]), os.O_RDONLY) + guard.open_intents[:] = [regular_intent] + scope["module"]._sync_directory(target) + adapter.raw_open.assert_called_once_with( + target, os.O_RDONLY | os.O_DIRECTORY, 0o777 + ) + adapter.raw_fsync.assert_called_once_with(87) + adapter.raw_close.assert_called_once_with(87) + self.assertEqual({}, adapter.bindings) + self.assertEqual({}, adapter.failed_open_bindings) + self.assertEqual([], adapter.failures) + self.assertEqual([regular_intent], guard.open_intents) + self.assertNotIn(target, guard.approved) + self.assertFalse(guard.readable(target)) + self.assertEqual(1, scope["protocol"].parent.sequence) + request = scope["protocol"].sent[0] + self.assertEqual(str(target), request["path"]) + self.assertEqual( + os.O_RDONLY | os.O_DIRECTORY, + request["flags"] & ~getattr(os, "O_CLOEXEC", 0), + ) + + def test_directory_sync_refuses_other_cases_and_paths_before_open(self) -> None: + for kind in ("negative-a3", "invalid-state", "parser-a5", "flock-a4"): + with self.subTest(kind=kind), directory_scope(kind) as scope: + with self.assertRaises(BOOT.BootstrapError): + scope["module"]._sync_directory( + POLICY.account_directory(scope["row"]) + ) + scope["adapter"].raw_open.assert_not_called() + scope["adapter"].raw_fsync.assert_not_called() + with directory_scope() as scope: + account = POLICY.account_directory(scope["row"]) + targets = ( + account / "receipts", + account / "logs", + account.parent, + account.parent / ("b" * 64), + POLICY.RUN_ROOT / "completion" / "foreign" / "captures", + POLICY.RUN_ROOT / "logs", + POLICY.RUN_ROOT.parent / "outside", + ) + for target in targets: + with ( + self.subTest(target=target), + self.assertRaises(BOOT.BootstrapError), + ): + scope["module"]._sync_directory(target) + scope["adapter"].raw_open.assert_not_called() + scope["adapter"].raw_fsync.assert_not_called() + + def test_directory_sync_strict_flags_owner_type_and_ungranted_reads(self) -> None: + with directory_scope() as scope: + target = POLICY.account_directory(scope["row"]) + for extra in ( + os.O_CREAT, + os.O_WRONLY, + os.O_RDWR, + os.O_TRUNC, + os.O_NOFOLLOW, + os.O_NONBLOCK, + ): + with self.subTest(extra=extra), self.assertRaises(BOOT.BootstrapError): + scope["adapter"].target(target, os.O_DIRECTORY | extra) + for flags in (True, os.O_RDONLY): + with self.assertRaises(BOOT.BootstrapError): + scope["adapter"].target(target, flags) + for field, value in ( + ("st_uid", 43), + ("st_mode", stat.S_IFDIR | 0o755), + ("st_mode", stat.S_IFREG | 0o600), + ): + previous = getattr(scope["target_info"], field) + setattr(scope["target_info"], field, value) + with ( + self.subTest(field=field, value=value), + self.assertRaises(BOOT.BootstrapError), + ): + scope["module"]._sync_directory(target) + setattr(scope["target_info"], field, previous) + with self.assertRaises(BOOT.BootstrapError): + scope["guard"]("open", (str(target), None, os.O_DIRECTORY)) + with self.assertRaises(BOOT.BootstrapError): + scope["adapter"].open(target, os.O_DIRECTORY) + scope["adapter"].raw_open.assert_not_called() + + def test_directory_sync_postopen_identity_failure_closes_matching_return( + self, + ) -> None: + for mode in ( + "path_swap", + "returned_identity", + "returned_type", + "returned_uid", + "access", + ): + with self.subTest(mode=mode), directory_scope() as scope: + if mode == "path_swap": + original = scope["adapter"].raw_open.side_effect + + def swap_path(path: object, flags: int, mode: int = 0o777) -> int: + fd = original(path, flags, mode) + scope["target_info"].st_ino = 3 + return fd + + scope["adapter"].raw_open.side_effect = swap_path + elif mode == "returned_identity": + scope["fd_info"].st_ino = 3 + elif mode == "returned_type": + scope["fd_info"].st_mode = stat.S_IFREG | 0o600 + elif mode == "returned_uid": + scope["fd_info"].st_uid = 43 + else: + scope["access"].return_value = os.O_RDWR + with self.assertRaises(BOOT.BootstrapError): + scope["module"]._sync_directory( + POLICY.account_directory(scope["row"]) + ) + scope["adapter"].raw_fsync.assert_not_called() + scope["adapter"].raw_close.assert_called_once_with(87) + self.assertFalse(scope["adapter"].bindings) + self.assertIsNone(scope["adapter"].pending) + self.assertTrue(scope["adapter"].failures[0]["closed"]) + + def test_directory_sync_reused_fd_before_fsync_refuses_foreign_close(self) -> None: + with directory_scope() as scope: + original = scope["fd_info"] + reused = types.SimpleNamespace( + st_dev=1, st_ino=99, st_uid=42, st_mode=stat.S_IFDIR | 0o700 + ) + scope["fstat"].side_effect = [original, original, reused, reused] + with self.assertRaises(BOOT.BootstrapError): + scope["module"]._sync_directory(POLICY.account_directory(scope["row"])) + scope["adapter"].raw_fsync.assert_not_called() + scope["adapter"].raw_close.assert_not_called() + self.assertIn(87, scope["adapter"].bindings) + self.assertFalse(scope["adapter"].failures[0]["closed"]) + + def test_directory_sync_failure_uses_original_finally_and_retains_close_failure( + self, + ) -> None: + for mode in ("fsync", "close", "postopen_close"): + with self.subTest(mode=mode), directory_scope() as scope: + if mode == "fsync": + scope["adapter"].raw_fsync.side_effect = OSError( + "Synthetic fsync failure" + ) + else: + scope["adapter"].raw_close.side_effect = OSError( + "Synthetic close failure" + ) + if mode == "postopen_close": + scope["fd_info"].st_ino = 3 + with self.assertRaises((OSError, BOOT.BootstrapError)): + scope["module"]._sync_directory( + POLICY.account_directory(scope["row"]) + ) + scope["adapter"].raw_close.assert_called_once_with(87) + if mode == "fsync": + self.assertFalse(scope["adapter"].bindings) + elif mode == "close": + self.assertIn(87, scope["adapter"].bindings) + else: + self.assertIn(87, scope["adapter"].failed_open_bindings) + + def test_directory_sync_windows_preservation_and_regular_fdopen_refusal( + self, + ) -> None: + with directory_scope() as scope: + scope["module"].os.name = "nt" + scope["module"]._sync_directory(POLICY.account_directory(scope["row"])) + scope["adapter"].raw_open.assert_not_called() + self.assertEqual([], scope["protocol"].sent) + scope["guard"].open_intents[:] = [ + (Path(scope["row"]["source"]), os.O_RDONLY) + ] + with self.assertRaises(BOOT.BootstrapError): + scope["guard"].descriptor(87, "rb") + self.assertFalse(scope["guard"].known_fds) + + def test_directory_sync_parent_exact_existing_directory_proof(self) -> None: + row = case() + account = POLICY.account_directory(row) + for target in (account, account / "captures"): + policy = POLICY.MutationPolicy(row) + request = mutation( + 1, + "open", + target, + os.O_RDONLY | os.O_DIRECTORY | getattr(os, "O_CLOEXEC", 0), + ) + grant = policy.decide(request) + with mock.patch.object(POLICY, "owned_path") as owned: + POLICY.mutation_target(grant, request) + self.assertEqual( + [ + mock.call(target.parent, directory=True), + mock.call(target, directory=True), + ], + owned.call_args_list, + ) + for kind in ("invalid-state", "negative-a3", "parser-a5", "flock-a4"): + policy = POLICY.MutationPolicy(case(kind)) + with self.assertRaises(POLICY.SafetyError): + policy.decide( + mutation(1, "open", policy.account, os.O_RDONLY | os.O_DIRECTORY) + ) + + def test_fixed_limits_and_proof_scope(self) -> None: + self.assertEqual(POLICY.LIMITS, BOOT.LIMITS) + self.assertEqual(POLICY.FREE_MIN - POLICY.LIMITS["rss_abort_bytes"], 201326592) + self.assertEqual(POLICY.PROOF_SCOPE, BOOT.PROOF_SCOPE) + self.assertEqual( + set(POLICY.SOURCE_FILES), {path for _, path in BOOT.SOURCE_MODULES} + ) + self.assertNotIn("installed", POLICY.PROOF_SCOPE) + + def test_exact_integer_and_hash(self) -> None: + for value in (True, False, 1.0, None, -1, 1 << 80): + with self.subTest(value=value), self.assertRaises(POLICY.SafetyError): + POLICY.integer(value) + for value in (True, "A" * 64, "a" * 63, "a" * 65, None): + with self.subTest(value=value), self.assertRaises(POLICY.SafetyError): + POLICY.hash_text(value) + self.assertEqual(POLICY.integer(0), 0) + self.assertEqual(POLICY.hash_text("a" * 64), "a" * 64) + + def test_sensor_admission_exact_floor_and_abi(self) -> None: + samples = [ + POLICY.Sensor(1_000_000_000 + n * 1_000_000_000, 1, POLICY.FREE_MIN, 100) + for n in range(3) + ] + POLICY.admission(samples, 3_500_000_000) + changes = [ + ("pressure", 0), + ("pressure", 2), + ("pressure", 3), + ("pressure", 4), + ("pressure", True), + ("free_bytes", POLICY.FREE_MIN - 1), + ("free_bytes", True), + ("swap_used_bytes", 101), + ("monotonic_ns", 1_000_000_000), + ] + for field, value in changes: + altered = [POLICY.asdict(row) for row in samples] + altered[-1][field] = value + with ( + self.subTest(field=field, value=value), + self.assertRaises(POLICY.SafetyError), + ): + POLICY.admission( + [POLICY.Sensor(**row) for row in altered], 3_500_000_000 + ) + for now in (2_999_999_999, 3_500_000_001): + with self.subTest(now=now), self.assertRaises(POLICY.SafetyError): + POLICY.admission(samples, now) + with self.assertRaises(POLICY.SafetyError): + POLICY.admission(samples[:2], 3_500_000_000) + with self.assertRaises(POLICY.SafetyError): + POLICY.admission( + [POLICY.Sensor(1 + n, 1, POLICY.FREE_MIN, 0) for n in range(3)], 3 + ) + + def test_manifest_and_case_fences(self) -> None: + self.assertEqual(POLICY.validate_manifest(manifest()), manifest()) + for kind in POLICY.CASES: + POLICY.validate_case(case(kind)) + changes = [ + ("version", True), + ("stage", "B"), + ("synthetic", False), + ("root_execution_go", False), + ("run_root", "/tmp"), + ("source_root", "/Users/j/dev/TrueMemory"), + ("sources", {}), + ("cases", []), + ("cases", [case(), case()]), + ] + for field, value in changes: + altered = manifest() + altered[field] = value + with self.subTest(field=field), self.assertRaises(POLICY.SafetyError): + POLICY.validate_manifest(altered) + for field, value in ( + ("id", []), + ("session", "unknown"), + ("database", "/tmp/personal.db"), + ("source", "/Users/j/dev/TrueMemory/transcript.jsonl"), + ("source_bytes", True), + ("source_identity", [1, 2, 23, 3, 4]), + ): + altered = case() + altered[field] = value + with self.subTest(field=field), self.assertRaises(POLICY.SafetyError): + POLICY.validate_case(altered) + altered = manifest() + altered["limits"]["wall_seconds"] = 6 + with self.assertRaises(POLICY.SafetyError): + POLICY.validate_manifest(altered) + altered = manifest() + altered["limits"]["core_bytes"] = False + with self.assertRaises(POLICY.SafetyError): + POLICY.validate_manifest(altered) + + def test_paths_and_versioned_results(self) -> None: + for name in ("v" + str(number) for number in range(1, 100)): + self.assertEqual( + POLICY.evidence_paths(POLICY.RUN_ROOT / ("manifest-" + name + ".json")), + (POLICY.RUN_ROOT / ("results-" + name + ".json"), name), + ) + for text in ( + "relative", + str(POLICY.RUN_ROOT) + "/../outside", + str(POLICY.RUN_ROOT) + "//escape", + "/tmp/file", + ): + with self.subTest(text=text), self.assertRaises(POLICY.SafetyError): + POLICY.canonical_path(text, POLICY.RUN_ROOT) + for name in ("v0", "v01", "v100", "v3x", "v3\n", "v3", "v٣"): + with self.subTest(name=name), self.assertRaises(POLICY.SafetyError): + POLICY.evidence_paths(POLICY.RUN_ROOT / ("manifest-" + name + ".json")) + with self.assertRaises(POLICY.SafetyError): + POLICY.evidence_paths(POLICY.RUN_ROOT.parent / "manifest-v3.json") + + def test_identity_is_current_owned_incarnation(self) -> None: + row = POLICY.Identity(72887, 42, 12345, 72887) + self.assertTrue(POLICY.same_owned_identity(row, row, 42)) + for changed in ( + POLICY.Identity(72887, 42, 12346, 72887), + POLICY.Identity(72887, 43, 12345, 72887), + POLICY.Identity(72887, 42, 12345, 1), + POLICY.Identity(True, 42, 12345, 1), + ): + with self.subTest(changed=changed): + self.assertFalse(POLICY.same_owned_identity(row, changed, 42)) + + def test_ready_and_result_exact_protocol(self) -> None: + ready = { + "type": "ready", + "version": 1, + "pid": 100, + "audit_installed": True, + "limits": { + "cpu": [2, 3], + "core": [0, 0], + "nofile": [64, 64], + "fsize": [1048576, 1048576], + }, + } + POLICY.validate_ready(ready, 100) + for field, value in ( + ("pid", 101), + ("version", True), + ("audit_installed", 1), + ("limits", {}), + ): + altered = copy.deepcopy(ready) + altered[field] = value + with self.subTest(field=field), self.assertRaises(POLICY.SafetyError): + POLICY.validate_ready(altered, 100) + altered = copy.deepcopy(ready) + altered["limits"]["core"] = [False, False] + with self.assertRaises(POLICY.SafetyError): + POLICY.validate_ready(altered, 100) + done = { + "type": "result", + "version": 1, + "case": "capture-a1", + "passed": True, + "proof_scope": POLICY.PROOF_SCOPE, + "import_mode": "isolated_source_module", + "sources": manifest()["sources"], + "details": {}, + } + POLICY.validate_result(done, case(), manifest()["sources"]) + for field, value in ( + ("passed", 1), + ("version", True), + ("proof_scope", "installed_host"), + ("case", "capture-a2"), + ("sources", {}), + ("details", []), + ): + altered = dict(done) + altered[field] = value + with self.subTest(field=field), self.assertRaises(POLICY.SafetyError): + POLICY.validate_result(altered, case(), manifest()["sources"]) + + def test_mutation_positive_and_one_pending_slot(self) -> None: + policy = POLICY.MutationPolicy(case()) + policy.decide(mutation(1, "os.mkdir", policy.account, 0o700)) + lock = policy.account / "state.lock" + policy.decide(mutation(2, "open", lock, os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW)) + pending = policy.account / ("state-" + "a" * 32 + ".pending") + policy.decide( + mutation( + 3, "open", pending, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW + ) + ) + grant = policy.decide( + mutation(4, "os.rename", policy.account / "state.json", source=pending) + ) + self.assertEqual(grant["path"], str(policy.account / "state.json")) + with self.assertRaises(POLICY.SafetyError): + policy.decide( + mutation( + 5, + "open", + policy.account / ("state-" + "b" * 32 + ".pending"), + os.O_WRONLY | os.O_CREAT | os.O_EXCL, + ) + ) + + def test_mutation_negative_controls(self) -> None: + for event, filename, flags, source in ( + ("open", "state.json", os.O_WRONLY | os.O_CREAT, None), + ("open", "state.lock", os.O_RDWR | os.O_CREAT | os.O_TRUNC, None), + ("open", "unknown", os.O_WRONLY | os.O_CREAT | os.O_EXCL, None), + ("os.mkdir", "foreign", 0o700, None), + ("os.mkdir", "captures", 0o755, None), + ("os.rename", "state.json", 0, POLICY.RUN_ROOT / "arbitrary"), + ("unlink", "state.json", 0, None), + ): + policy = POLICY.MutationPolicy(case()) + with ( + self.subTest(event=event, filename=filename), + self.assertRaises(POLICY.SafetyError), + ): + policy.decide( + mutation(1, event, policy.account / filename, flags, source) + ) + for sequence in (0, 2, True, 33): + policy = POLICY.MutationPolicy(case()) + with self.subTest(sequence=sequence), self.assertRaises(POLICY.SafetyError): + policy.decide(mutation(sequence, "os.mkdir", policy.account, 0o700)) + policy = POLICY.MutationPolicy(case("negative-a3")) + with self.assertRaises(POLICY.SafetyError): + policy.decide(mutation(1, "os.mkdir", policy.account, 0o700)) + + def test_fd_wrappers_bind_latest_consumed_intent(self) -> None: + import fcntl + + row = case() + guard = BOOT.AuditGuard(FakeProtocol(), {"case": row}, POLICY) + path = Path(row["source"]) + info = types.SimpleNamespace( + st_dev=1, st_ino=2, st_uid=42, st_mode=stat.S_IFREG | 0o600 + ) + with ( + mock.patch.object(BOOT.os, "fstat", return_value=info), + mock.patch.object(BOOT.os, "getuid", return_value=42), + mock.patch.object(fcntl, "fcntl", return_value=os.O_RDONLY), + mock.patch.object(Path, "lstat", return_value=info), + mock.patch.object(Path, "is_symlink", return_value=False), + ): + guard.open_intents[:] = [(path, os.O_RDONLY)] + guard.descriptor(7, "r") + self.assertEqual(guard.known_fds, {7: (1, 2)}) + with self.assertRaises(BOOT.BootstrapError): + guard.descriptor(7, "rb") + info.st_ino = 3 + guard.open_intents[:] = [(path, os.O_RDONLY)] + guard.descriptor(7, "rb") + self.assertEqual(guard.known_fds, {7: (1, 3)}) + guard.open_intents[:] = [(path, os.O_WRONLY)] + with self.assertRaises(BOOT.BootstrapError): + guard.descriptor(7, "rb") + self.assertFalse(guard.open_intents) + for fd, mode in ((0, "r"), (7, "a"), (7, "r+"), (True, "r")): + with ( + self.subTest(fd=fd, mode=mode), + self.assertRaises(BOOT.BootstrapError), + ): + guard.descriptor(fd, mode) + + def test_audit_preopen_grant_and_denials(self) -> None: + protocol = FakeProtocol() + guard = BOOT.AuditGuard(protocol, {"case": case()}, POLICY) + path = POLICY.account_directory(case()) / "state.lock" + protocol.answers.append( + { + "type": "grant", + "version": 1, + "seq": 1, + "path": str(path), + "purpose": "Owned fixed lock", + } + ) + guard.path = lambda value: Path(value) + guard("open", (str(path), None, os.O_RDWR | os.O_CREAT)) + self.assertEqual(len(protocol.sent), 1) + self.assertEqual(guard.open_intents, [(path, os.O_RDWR)]) + guard("truememory.native.audit_probe", ("fixed-stage-a-v1",)) + self.assertTrue(guard.activated) + for event, args in ( + ("socket.connect", ()), + ("subprocess.Popen", ()), + ("os.remove", (str(path), -1)), + ("sqlite3.connect", ("memory",)), + ("resource.setrlimit", (0, (1, 1))), + ("import", ("truememory.engine", None)), + ("import", ("torch", None)), + ("os.mkdir", (str(path), 0o700, 1)), + ("os.rename", (str(path), str(path), 1, -1)), + ): + with self.subTest(event=event), self.assertRaises(BOOT.BootstrapError): + guard(event, args) + + def test_prelaunch_refusals_retain_nonexecuted_result(self) -> None: + fake = types.ModuleType("fake_psutil") + fake.Error = FakePsutilError + with ( + mock.patch.object( + POLICY.selectors, "DefaultSelector", return_value=FakeSelector() + ), + mock.patch.object( + POLICY, "sample", side_effect=POLICY.SafetyError("pressure_or_headroom") + ), + mock.patch.object(POLICY.subprocess, "Popen") as launch, + ): + result = POLICY.execute_case(manifest(), case(), fake, None) + self.assertFalse(result["executed"]) + self.assertFalse(result["passed"]) + self.assertTrue(result["reaped"]) + self.assertIn("pressure_or_headroom", result["blocked"]) + launch.assert_not_called() + samples = [ + POLICY.Sensor(1_000_000_000 + n * 1_000_000_000, 1, POLICY.FREE_MIN, 100) + for n in range(3) + ] + with ( + mock.patch.object( + POLICY.selectors, "DefaultSelector", return_value=FakeSelector() + ), + mock.patch.object(POLICY, "sample", side_effect=samples), + mock.patch.object(POLICY.time, "sleep"), + mock.patch.object(POLICY.time, "monotonic_ns", return_value=3_000_000_001), + mock.patch.object( + POLICY, "owned_path", side_effect=POLICY.SafetyError("fixture_changed") + ), + mock.patch.object(POLICY.subprocess, "Popen") as launch, + ): + result = POLICY.execute_case(manifest(), case(), fake, None) + self.assertFalse(result["executed"]) + self.assertFalse(result["passed"]) + self.assertIn("fixture_changed", result["blocked"]) + self.assertEqual(len(result["sensors"]), 3) + launch.assert_not_called() + + def test_pipe_deadline_and_partial_writes_are_pure(self) -> None: + with ( + mock.patch.object(POLICY.time, "monotonic", return_value=1), + mock.patch.object(POLICY.os, "write", side_effect=[2, 3]) as writer, + ): + POLICY.write_pipe(7, b"12345", 1.1) + self.assertEqual(writer.call_count, 2) + with ( + mock.patch.object(POLICY.time, "monotonic", return_value=1), + mock.patch.object(POLICY.os, "write", side_effect=[BlockingIOError(), 3]), + mock.patch.object( + POLICY.select, "select", return_value=([], [7], []) + ) as poll, + ): + POLICY.write_pipe(7, b"123", 1.1) + self.assertEqual(poll.call_count, 1) + with ( + mock.patch.object(POLICY.time, "monotonic", return_value=1), + mock.patch.object(POLICY.os, "write") as writer, + ): + with self.assertRaises(POLICY.SafetyError): + POLICY.write_pipe(7, b"123", 0.9) + writer.assert_not_called() + + def test_owned_popen_fallback_and_group_probe_are_pure(self) -> None: + fake = types.ModuleType("fake_psutil") + fake.NoSuchProcess = FakePsutilError + child = mock.Mock() + child.poll.return_value = None + child.wait.return_value = 0 + child.returncode = 0 + result = { + "reaped": False, + "descendants": [], + "descendant_containment": "not_observed", + } + with mock.patch.object(POLICY.time, "monotonic", return_value=1): + POLICY.terminate_owned(child, None, [], fake, result) + child.send_signal.assert_called_once_with(POLICY.signal.SIGTERM) + self.assertTrue(result["reaped"]) + self.assertEqual(result["termination_authority"], "unreaped_direct_popen_child") + identity = POLICY.Identity(100, 42, 123, 100) + changed = POLICY.Identity(100, 42, 124, 100) + child = mock.Mock() + child.poll.return_value = None + result = { + "reaped": False, + "descendants": [], + "descendant_containment": "not_observed", + } + with ( + mock.patch.object(POLICY.time, "monotonic", return_value=1), + mock.patch.object(POLICY, "observe_identity", return_value=changed), + mock.patch.object(POLICY.os, "getuid", return_value=42), + mock.patch.object(POLICY.os, "killpg") as signal_group, + ): + with self.assertRaises(POLICY.SafetyError): + POLICY.terminate_owned(child, identity, [], fake, result) + child.send_signal.assert_not_called() + signal_group.assert_not_called() + with mock.patch.object(POLICY.os, "killpg", side_effect=ProcessLookupError()): + self.assertTrue(POLICY.group_gone(100)) + with mock.patch.object(POLICY.os, "killpg", return_value=None): + self.assertFalse(POLICY.group_gone(100)) + with mock.patch.object(POLICY.os, "killpg", side_effect=PermissionError()): + self.assertFalse(POLICY.group_gone(100)) + member = POLICY.Identity(101, 42, 123, 100) + self.assertTrue(POLICY.member_identity_matches(member, member, 100, 42)) + self.assertFalse(POLICY.member_identity_matches(member, member, 101, 42)) + + def test_protocol_bounds_use_no_real_stdio(self) -> None: + protocol = BOOT.Protocol() + with mock.patch.object( + BOOT.os, "write", side_effect=lambda fd, data: len(data) + ) as writer: + protocol.emit({"type": "synthetic"}) + self.assertEqual(writer.call_count, 1) + with self.assertRaises(BOOT.BootstrapError): + protocol.emit({"payload": "x" * 2048}) + protocol.written_bytes = BOOT.LIMITS["output_bytes"] + with self.assertRaises(BOOT.BootstrapError): + protocol.emit({"type": "synthetic"}) + with mock.patch.object(BOOT.select, "select", return_value=([], [], [])): + with self.assertRaises(BOOT.BootstrapError): + BOOT.Protocol().receive(2048, 0.1) + protocol = BOOT.Protocol() + with ( + mock.patch.object(BOOT.select, "select", return_value=([0], [], [])), + mock.patch.object(BOOT.os, "read", return_value=b"x" * 2049), + ): + with self.assertRaises(BOOT.BootstrapError): + protocol.receive(2048, 0.1) + + def test_attempt_versions_are_canonical_before_io(self) -> None: + for number in range(1, 100): + self.assertEqual( + POLICY.validate_attempt_version("v" + str(number)), "v" + str(number) + ) + fake = types.ModuleType("fake_psutil") + fake.Error = FakePsutilError + invalid = ( + True, + False, + None, + 3, + b"v3", + [], + "v0", + "v00", + "v01", + "v100", + "V3", + "v3\n", + "v3", + "v٣", + "../v3", + "v3.log", + ) + for version in invalid: + with ( + self.subTest(version=version), + mock.patch.object(POLICY.selectors, "DefaultSelector") as selector, + mock.patch.object(POLICY, "sample") as sensor, + mock.patch.object(POLICY, "new_log") as writer, + mock.patch.object(POLICY.subprocess, "Popen") as launch, + ): + with self.assertRaises(POLICY.SafetyError): + POLICY.validate_attempt_version(version) + with self.assertRaises(POLICY.SafetyError): + POLICY.execute_case( + manifest(), case(), fake, None, attempt_version=version + ) + selector.assert_not_called() + sensor.assert_not_called() + writer.assert_not_called() + launch.assert_not_called() + + def test_actual_retry_logs_and_legacy_default(self) -> None: + fake = types.ModuleType("fake_psutil") + fake.Error = FakePsutilError + info = types.SimpleNamespace( + st_dev=1, st_ino=2, st_size=24, st_mtime_ns=3, st_ctime_ns=4 + ) + samples = [ + POLICY.Sensor(1_000_000_000 + n * 1_000_000_000, 1, POLICY.FREE_MIN, 100) + for n in range(3) + ] + fixture = case() + fixture_bytes = b"x" * 24 + fixture["source_sha256"] = POLICY.digest(fixture_bytes) + plan = manifest() + source_bytes = { + POLICY.SOURCE_ROOT / name: ("synthetic:" + name).encode("utf-8") + for name in POLICY.SOURCE_FILES + } + plan["sources"] = { + name: POLICY.digest(source_bytes[POLICY.SOURCE_ROOT / name]) + for name in POLICY.SOURCE_FILES + } + bootstrap_path = POLICY.SOURCE_ROOT / "tests/native/mac-native-bootstrap-v4.py" + bootstrap_bytes = b"fixed-bootstrap-v2" + plan["bootstrap_sha256"] = POLICY.digest(bootstrap_bytes) + + def read_fixture(path: Path, maximum: int, *, private: bool = True) -> bytes: + if path == Path(fixture["source"]): + self.assertEqual((maximum, private), (4096, True)) + return fixture_bytes + self.assertEqual((maximum, private), (262144, False)) + if path == bootstrap_path: + return bootstrap_bytes + if path in source_bytes: + return source_bytes[path] + raise AssertionError("Unplanned synthetic source read") + + for version in (None, "v3", "v99"): + expected = version if version is not None else "v1" + with self.subTest(version=version), ExitStack() as patches: + patches.enter_context( + mock.patch.object( + POLICY.selectors, "DefaultSelector", return_value=FakeSelector() + ) + ) + patches.enter_context( + mock.patch.object(POLICY, "sample", side_effect=samples) + ) + patches.enter_context(mock.patch.object(POLICY.time, "sleep")) + patches.enter_context( + mock.patch.object( + POLICY.time, "monotonic_ns", return_value=3_000_000_001 + ) + ) + patches.enter_context( + mock.patch.object(POLICY, "owned_path", return_value=info) + ) + reader = patches.enter_context( + mock.patch.object(POLICY, "read_bounded", side_effect=read_fixture) + ) + writer = patches.enter_context( + mock.patch.object( + POLICY, + "new_log", + side_effect=[7, 8, POLICY.SafetyError("stop_before_launch")], + ) + ) + patches.enter_context(mock.patch.object(POLICY.os, "fsync")) + patches.enter_context(mock.patch.object(POLICY.os, "close")) + launch = patches.enter_context( + mock.patch.object(POLICY.subprocess, "Popen") + ) + result = ( + POLICY.execute_case(plan, fixture, fake, None) + if version is None + else POLICY.execute_case( + plan, fixture, fake, None, attempt_version=version + ) + ) + self.assertEqual( + [call.args[0] for call in writer.call_args_list], + [ + POLICY.RUN_ROOT + / "logs" + / ("capture-a1-" + channel + "-" + expected + ".log") + for channel in ("stdout", "stderr", "events") + ], + ) + self.assertEqual( + reader.call_args_list[-1].args[0], + POLICY.SOURCE_ROOT / "tests/native/mac-native-bootstrap-v4.py", + ) + self.assertFalse(result["executed"]) + self.assertFalse(result["passed"]) + self.assertTrue(result["reaped"]) + self.assertIn("stop_before_launch", result["blocked"]) + launch.assert_not_called() + + def test_main_reserves_result_before_runtime_and_case(self) -> None: + with main_scope("v3") as calls: + + def runtime() -> types.ModuleType: + self.assertEqual(calls["new_log"].call_count, 1) + calls["execute_case"].assert_not_called() + fake = types.ModuleType("fake_psutil") + fake.Error = FakePsutilError + return fake + + calls["pinned_runtime"].side_effect = runtime + self.assertEqual(POLICY.main(), 1) + self.assertEqual( + calls["new_log"].call_args.args[0], POLICY.RUN_ROOT / "results-v3.json" + ) + calls["new_log"].assert_called_once() + self.assertEqual( + calls["execute_case"].call_args.kwargs, {"attempt_version": "v3"} + ) + payload = json.loads(calls["write_all"].call_args.args[1]) + self.assertEqual(payload["manifest_version"], "v3") + self.assertFalse(payload["complete"]) + self.assertFalse(payload["cases"][0]["executed"]) + calls["fsync"].assert_called_once_with(7) + calls["close"].assert_called_once_with(7) + + def test_main_existing_result_refuses_without_runtime(self) -> None: + with main_scope("v3") as calls: + calls["new_log"].side_effect = FileExistsError("retained_attempt") + with self.assertRaises(FileExistsError): + POLICY.main() + for name in ( + "pinned_runtime", + "PressureReader", + "execute_case", + "write_all", + "fsync", + "close", + ): + calls[name].assert_not_called() + + def test_main_runtime_refusal_retains_reserved_result(self) -> None: + with main_scope("v99") as calls: + calls["pinned_runtime"].side_effect = POLICY.SafetyError("runtime_refusal") + self.assertEqual(POLICY.main(), 1) + payload = json.loads(calls["write_all"].call_args.args[1]) + self.assertEqual(payload["manifest_version"], "v99") + self.assertEqual(payload["cases"], []) + self.assertIn("runtime_refusal", payload["setup_failure"]) + self.assertFalse(payload["complete"]) + calls["execute_case"].assert_not_called() + calls["fsync"].assert_called_once_with(7) + calls["close"].assert_called_once_with(7) + + def test_main_fatal_retains_incomplete_reserved_result(self) -> None: + with main_scope("v3") as calls: + calls["pinned_runtime"].side_effect = RuntimeError("fatal_inert_fixture") + with self.assertRaisesRegex(RuntimeError, "fatal_inert_fixture"): + POLICY.main() + calls["new_log"].assert_called_once() + calls["write_all"].assert_not_called() + calls["execute_case"].assert_not_called() + calls["fsync"].assert_called_once_with(7) + calls["close"].assert_called_once_with(7) + + def test_main_revision_only_reserved_for_v2(self) -> None: + revision = manifest() + revised = case("capture-revision") + revised["previous"]["key"] = "d" * 64 + revision["cases"] = [revised] + for version in ("v1", "v3", "v99"): + with self.subTest(version=version), main_scope(version, revision) as calls: + with self.assertRaisesRegex( + POLICY.SafetyError, "unexpected_manifest_case_stage" + ): + POLICY.main() + calls["new_log"].assert_not_called() + calls["pinned_runtime"].assert_not_called() + with main_scope("v2") as calls: + with self.assertRaisesRegex( + POLICY.SafetyError, "unexpected_manifest_case_stage" + ): + POLICY.main() + calls["new_log"].assert_not_called() + with main_scope("v2", revision) as calls: + calls["pinned_runtime"].side_effect = POLICY.SafetyError( + "controlled_stage_stop" + ) + self.assertEqual(POLICY.main(), 1) + self.assertEqual( + calls["new_log"].call_args.args[0], POLICY.RUN_ROOT / "results-v2.json" + ) + + def test_bootstrap_uses_actual_v2_policy(self) -> None: + guard = BOOT.AuditGuard(FakeProtocol(), {"case": case()}, POLICY) + self.assertIn( + POLICY.SOURCE_ROOT / "tests/native/mac-native-acceptance-v4.py", + guard.read_paths, + ) + self.assertNotIn( + POLICY.SOURCE_ROOT / "tests/native/mac-native-acceptance-v1.py", + guard.read_paths, + ) + tree = ast.parse(Path(BOOT.__file__).read_bytes()) + calls = [ + node + for node in ast.walk(tree) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "compile_module" + ] + policy = [ + node + for node in calls + if node.args + and isinstance(node.args[0], ast.Constant) + and node.args[0].value == "mac_native_policy_v4" + ] + self.assertEqual(len(policy), 1) + self.assertIsInstance(policy[0].args[1], ast.BinOp) + self.assertEqual( + policy[0].args[1].right.value, "tests/native/mac-native-acceptance-v4.py" + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-mac-native-acceptance-v6.py b/tests/test-mac-native-acceptance-v6.py new file mode 100644 index 00000000..a70d183a --- /dev/null +++ b/tests/test-mac-native-acceptance-v6.py @@ -0,0 +1,1987 @@ +"""Pure Stage A policy tests. Never launch a child, signal, sensor or fixture.""" + +from __future__ import annotations + +import ast +import copy +import json +import os +import stat +import sys +import types +import unittest +from contextlib import ExitStack, contextmanager +from pathlib import Path +from typing import Callable, Iterator +from unittest import mock + + +def load_source(name: str, filename: str) -> types.ModuleType: + path = Path(__file__).parent / "native" / filename + module = types.ModuleType(name) + module.__file__ = str(path) + sys.modules[name] = module + exec( + compile(path.read_bytes(), str(path), "exec", dont_inherit=True), + module.__dict__, + ) + return module + + +POLICY = load_source("native_acceptance_policy_test_v6", "mac-native-acceptance-v6.py") +BOOT = load_source("native_acceptance_bootstrap_test_v6", "mac-native-bootstrap-v6.py") + + +def case(kind: str = "capture-a1") -> dict: + name = {"capture-revision": "capture-a2", "invalid-state": "negative-a3"}.get( + kind, kind + ) + database = ( + "capture-a2" + if kind == "capture-revision" + else "negative-a3" + if kind == "invalid-state" + else kind + ) + row = { + "id": kind, + "source": str( + POLICY.RUN_ROOT + / "home/.claude/projects/native-acceptance-v1" + / (name + ".jsonl") + ), + "source_sha256": "a" * 64, + "source_bytes": 24, + "source_identity": [1, 2, 24, 3, 4], + "session": "synthetic-native-" + name, + "database": str(POLICY.RUN_ROOT / (database + "-v1.db")), + "previous": None, + } + if kind == "flock-a4": + for field in ("source", "source_sha256", "source_bytes", "source_identity"): + row[field] = None + if kind == "capture-revision": + row["previous"] = {"key": "b" * 64, "sha256": "c" * 64, "byte_count": 12} + return row + + +def manifest() -> dict: + return { + "version": 1, + "stage": "A", + "synthetic": True, + "root_execution_go": True, + "run_root": str(POLICY.RUN_ROOT), + "home": str(POLICY.RUN_ROOT / "home"), + "source_root": str(POLICY.SOURCE_ROOT), + "source_commit": "a" * 40, + "sources": {name: "a" * 64 for name in POLICY.SOURCE_FILES}, + "supervisor_sha256": "b" * 64, + "bootstrap_sha256": "c" * 64, + "limits": dict(POLICY.LIMITS), + "cases": [case()], + } + + +def mutation( + seq: int, event: str, path: Path, flags: int = 0, source: Path | None = None +) -> dict: + return { + "type": "mutation", + "version": 1, + "seq": seq, + "event": event, + "path": str(path), + "flags": flags, + "source": str(source) if source is not None else None, + } + + +class FakeProtocol: + def __init__(self) -> None: + self.sent: list[object] = [] + self.answers: list[dict] = [] + + def emit(self, value: object) -> None: + self.sent.append(value) + + def receive(self, maximum: int, seconds: float) -> dict: + if maximum != 2048 or seconds != 0.1: + raise AssertionError("Changed frame or ACK bound") + return self.answers.pop(0) + + +class FakeSelector: + def close(self) -> None: + return None + + +class DirectoryProtocol(FakeProtocol): + def __init__(self, row: dict) -> None: + super().__init__() + self.parent = POLICY.MutationPolicy(row) + + def emit(self, value: object) -> None: + self.sent.append(value) + grant = self.parent.decide(value) + POLICY.mutation_target(grant, value) + self.answers.append(grant) + + +def actual_sync_module() -> types.ModuleType: + path = Path(__file__).parent.parent / "truememory/ingest/completion.py" + text = path.read_text() + node = next( + node + for node in ast.parse(text).body + if isinstance(node, ast.FunctionDef) and node.name == "_sync_directory" + ) + source = ast.get_source_segment(text, node) + if source is None: + raise AssertionError("Actual directory helper missing") + module = types.ModuleType("truememory.ingest.completion") + module.__file__ = str(BOOT.SOURCE_ROOT / "truememory/ingest/completion.py") + module.Path = Path + module.os = types.SimpleNamespace( + name="posix", O_RDONLY=os.O_RDONLY, O_DIRECTORY=os.O_DIRECTORY + ) + exec(compile(source, module.__file__, "exec", dont_inherit=True), module.__dict__) + return module + + +@contextmanager +def directory_scope( + kind: str = "capture-a1", after_open: Callable[[], None] | None = None +) -> Iterator[dict[str, object]]: + import fcntl + + row = case(kind) + protocol = DirectoryProtocol(row) + guard = BOOT.AuditGuard(protocol, {"case": row}, POLICY) + module = actual_sync_module() + adapter = BOOT.DirectorySyncAdapter(guard, module) + guard.directory_sync = adapter + target_info = types.SimpleNamespace( + st_dev=1, st_ino=2, st_uid=42, st_mode=stat.S_IFDIR | 0o700 + ) + fd_info = types.SimpleNamespace( + st_dev=1, st_ino=2, st_uid=42, st_mode=stat.S_IFDIR | 0o700 + ) + adapter.raw_fsync = mock.Mock() + adapter.raw_close = mock.Mock() + + def raw_open(path: object, flags: int, mode: int = 0o777) -> int: + guard("open", (os.fspath(path), None, flags | getattr(os, "O_CLOEXEC", 0))) + if after_open is not None: + after_open() + return 87 + + adapter.raw_open = mock.Mock(side_effect=raw_open) + module.os.open, module.os.fsync, module.os.close = ( + adapter.open, + adapter.fsync, + adapter.close, + ) + with ExitStack() as patches: + patches.enter_context(mock.patch.object(Path, "resolve", lambda value: value)) + patches.enter_context(mock.patch.object(Path, "is_symlink", return_value=False)) + patches.enter_context(mock.patch.object(BOOT.os, "getuid", return_value=42)) + patches.enter_context( + mock.patch.object(POLICY, "owned_path", return_value=target_info) + ) + fstat = patches.enter_context( + mock.patch.object(BOOT.os, "fstat", return_value=fd_info) + ) + access = patches.enter_context( + mock.patch.object(fcntl, "fcntl", return_value=os.O_RDONLY) + ) + yield { + "row": row, + "protocol": protocol, + "guard": guard, + "module": module, + "adapter": adapter, + "target_info": target_info, + "fd_info": fd_info, + "fstat": fstat, + "access": access, + } + + +class FakePsutilError(Exception): + pass + + +class CaptureProtocol(FakeProtocol): + def __init__(self, row: dict) -> None: + super().__init__() + self.parent = POLICY.MutationPolicy(row) + + def emit(self, value: object) -> None: + self.sent.append(value) + self.answers.append(self.parent.decide(value)) + + +def actual_capture_module() -> types.ModuleType: + path = Path(__file__).parent.parent / "truememory/ingest/completion.py" + tree = ast.parse(path.read_text()) + names = { + "_open_private", + "_read_private", + "_capture_inventory", + "_immutable", + "verify_capture", + "_sync_directory", + } + selected = [ + node + for node in tree.body + if isinstance(node, ast.FunctionDef) and node.name in names + ] + if {node.name for node in selected} != names: + raise AssertionError("Actual capture helpers missing") + module = types.ModuleType("truememory.ingest.completion") + module.__file__ = str(BOOT.SOURCE_ROOT / "truememory/ingest/completion.py") + module.Path, module.Capture = Path, object + module.CompletionError = type("CompletionError", (OSError,), {}) + module.MAX_CAPTURES, module.MAX_STATE_BYTES, module.MAX_CAPTURE_BYTES = ( + 1024, + 8388608, + 16777216, + ) + module.hashlib, module.stat = BOOT.hashlib, stat + module.os = types.SimpleNamespace( + name="posix", + O_RDONLY=os.O_RDONLY, + O_CREAT=os.O_CREAT, + O_EXCL=os.O_EXCL, + O_WRONLY=os.O_WRONLY, + O_DIRECTORY=os.O_DIRECTORY, + O_NOFOLLOW=os.O_NOFOLLOW, + getuid=lambda: 42, + fstat=lambda fd: BOOT.os.fstat(fd), + fdopen=BOOT.os.fdopen, + ) + exec( + compile( + ast.Module(body=selected, type_ignores=[]), + module.__file__, + "exec", + dont_inherit=True, + ), + module.__dict__, + ) + return module + + +@contextmanager +def capture_scope() -> Iterator[dict[str, object]]: + import fcntl + + data = b"Retained synthetic capture\n" + row = case() + row["source_bytes"] = len(data) + row["source_identity"][2] = len(data) + row["source_sha256"] = BOOT.hashlib.sha256(data).hexdigest() + protocol = CaptureProtocol(row) + guard = BOOT.AuditGuard(protocol, {"case": row}, POLICY) + module = actual_capture_module() + directory = BOOT.DirectorySyncAdapter(guard, module) + directory.installed = True + guard.directory_sync = directory + adapter = BOOT.RetainedCaptureReadAdapter(guard, module) + guard.capture_reads = adapter + directory.raw_open, directory.raw_close = adapter.open, adapter.close + module.os.open, module.os.close = directory.open, directory.close + adapter.raw_close = mock.Mock() + target = next(iter(adapter.catalog)) + info = types.SimpleNamespace( + st_dev=1, + st_ino=2, + st_uid=42, + st_mode=stat.S_IFREG | 0o600, + st_nlink=1, + st_size=len(data), + st_mtime_ns=3, + st_ctime_ns=4, + ) + directory_info = types.SimpleNamespace( + st_dev=1, st_ino=99, st_uid=42, st_mode=stat.S_IFDIR | 0o700 + ) + + def raw_open(path: object, flags: int, mode: int = 0o777) -> int: + guard("open", (os.fspath(path), None, flags | getattr(os, "O_CLOEXEC", 0))) + return 87 + + def owned(path: Path, *, directory: bool = False) -> types.SimpleNamespace: + return directory_info if directory else info + + def pread(fd: int, count: int, offset: int) -> bytes: + if fd != 87 or not 0 < count <= 4097: + raise AssertionError("Changed bounded descriptor read") + return data[offset : offset + count] + + adapter.raw_open = mock.Mock(side_effect=raw_open) + with ExitStack() as patches: + patches.enter_context(mock.patch.object(Path, "resolve", lambda value: value)) + patches.enter_context(mock.patch.object(Path, "is_symlink", return_value=False)) + patches.enter_context(mock.patch.object(BOOT.os, "getuid", return_value=42)) + patches.enter_context( + mock.patch.object(POLICY, "owned_path", side_effect=owned) + ) + patches.enter_context(mock.patch.object(BOOT.os, "fstat", return_value=info)) + patches.enter_context( + mock.patch.object(fcntl, "fcntl", return_value=os.O_RDONLY) + ) + cursor = patches.enter_context( + mock.patch.object(BOOT.os, "lseek", return_value=0) + ) + reader = patches.enter_context( + mock.patch.object(BOOT.os, "pread", side_effect=pread) + ) + yield { + "row": row, + "protocol": protocol, + "guard": guard, + "module": module, + "adapter": adapter, + "directory": directory, + "target": target, + "info": info, + "data": data, + "cursor": cursor, + "reader": reader, + } + + +@contextmanager +def main_scope( + version: str = "v3", fixture: dict | None = None +) -> Iterator[dict[str, mock.Mock]]: + row = copy.deepcopy(fixture) if fixture is not None else manifest() + parser = mock.Mock() + parser.parse_args.return_value = types.SimpleNamespace( + manifest=str(POLICY.RUN_ROOT / ("manifest-" + version + ".json")), + execute_root_authorized=True, + ) + fake = types.ModuleType("fake_psutil") + fake.Error = FakePsutilError + with ExitStack() as patches: + patches.enter_context( + mock.patch.object(POLICY.argparse, "ArgumentParser", return_value=parser) + ) + patches.enter_context(mock.patch.object(POLICY.sys, "platform", "darwin")) + patches.enter_context( + mock.patch.object( + POLICY.sys, + "flags", + types.SimpleNamespace( + isolated=True, no_site=True, dont_write_bytecode=True + ), + ) + ) + patches.enter_context( + mock.patch.object(POLICY.sys, "dont_write_bytecode", True) + ) + patches.enter_context(mock.patch.object(POLICY.os, "umask", return_value=0o077)) + calls = { + "owned_path": patches.enter_context( + mock.patch.object(POLICY, "owned_path") + ), + "read_bounded": patches.enter_context( + mock.patch.object( + POLICY, "read_bounded", return_value=POLICY.encoded(row) + ) + ), + "digest": patches.enter_context( + mock.patch.object(POLICY, "digest", return_value="b" * 64) + ), + "new_log": patches.enter_context( + mock.patch.object(POLICY, "new_log", return_value=7) + ), + "pinned_runtime": patches.enter_context( + mock.patch.object(POLICY, "pinned_runtime", return_value=fake) + ), + "PressureReader": patches.enter_context( + mock.patch.object(POLICY, "PressureReader") + ), + "execute_case": patches.enter_context( + mock.patch.object( + POLICY, + "execute_case", + return_value={ + "passed": False, + "reaped": True, + "executed": False, + "blocked": "pressure_or_headroom", + }, + ) + ), + "write_all": patches.enter_context(mock.patch.object(POLICY, "write_all")), + "fsync": patches.enter_context(mock.patch.object(POLICY.os, "fsync")), + "close": patches.enter_context(mock.patch.object(POLICY.os, "close")), + } + yield calls + + +class ForeignDirectorySyncAdapter(BOOT.DirectorySyncAdapter): + def open( + self, path: object, flags: int, mode: int = 0o777, *, dir_fd: int | None = None + ) -> int: + if dir_fd is None: + return self.raw_open(path, flags, mode) + return self.raw_open(path, flags, mode, dir_fd=dir_fd) + + def close(self, fd: int) -> None: + self.raw_close(fd) + + +def directory_wrapper_frame( + directory: BOOT.DirectorySyncAdapter, method: str, outer: types.SimpleNamespace +) -> types.SimpleNamespace: + function = getattr(type(directory), method) + return types.SimpleNamespace( + f_code=function.__code__, + f_globals=function.__globals__, + f_locals={"self": directory}, + f_back=outer, + ) + + +class PurePolicyTests(unittest.TestCase): + def test_capture_constructor_requires_exact_installed_directory_adapter( + self, + ) -> None: + for mode in ( + "missing", + "foreign", + "subclass", + "not_installed", + "truthy_int", + "truthy_text", + ): + with self.subTest(mode=mode), capture_scope() as scope: + original = scope["directory"] + if mode == "missing": + directory = None + elif mode == "foreign": + directory = types.SimpleNamespace(installed=True) + elif mode == "subclass": + directory = ForeignDirectorySyncAdapter.__new__( + ForeignDirectorySyncAdapter + ) + directory.__dict__.update(original.__dict__) + else: + directory = original + directory.installed = { + "not_installed": False, + "truthy_int": 1, + "truthy_text": "installed", + }[mode] + scope["guard"].directory_sync = directory + with self.assertRaisesRegex( + BOOT.BootstrapError, "capture_read_directory_chain_missing" + ): + BOOT.RetainedCaptureReadAdapter(scope["guard"], scope["module"]) + scope["adapter"].raw_open.assert_not_called() + self.assertEqual(0, scope["guard"].sequence) + + def test_capture_fixed_known_directory_frame_and_delegate_positive(self) -> None: + with capture_scope() as scope: + adapter, directory = scope["adapter"], scope["directory"] + outer = types.SimpleNamespace() + for method in ("open", "close"): + with self.subTest(method=method): + frame = directory_wrapper_frame(directory, method, outer) + self.assertIs(adapter.producer_frame(frame, method), outer) + adapter.raw_open.assert_not_called() + adapter.raw_close.assert_not_called() + self.assertEqual(0, scope["guard"].sequence) + + def test_capture_postconstruction_subclass_inventory_refuses_before_grant( + self, + ) -> None: + with capture_scope() as scope: + adapter, guard, module = scope["adapter"], scope["guard"], scope["module"] + directory = ForeignDirectorySyncAdapter.__new__(ForeignDirectorySyncAdapter) + directory.__dict__.update(scope["directory"].__dict__) + adapter.directory, guard.directory_sync = directory, directory + module.os.open, module.os.close = directory.open, directory.close + entries = mock.MagicMock() + entries.__enter__.return_value = iter( + [types.SimpleNamespace(path=str(scope["target"]))] + ) + module.os.scandir = mock.Mock(return_value=entries) + with self.assertRaisesRegex( + BOOT.BootstrapError, "capture_read_directory_chain_changed" + ): + module._capture_inventory(scope["target"].parent) + adapter.raw_open.assert_not_called() + adapter.raw_close.assert_not_called() + self.assertEqual(0, guard.sequence) + self.assertEqual({}, adapter.bindings) + + def test_capture_postconstruction_foreign_or_changed_reference_refuses( + self, + ) -> None: + for mode in ("subclass", "foreign", "reference"): + with self.subTest(mode=mode), capture_scope() as scope: + adapter, directory, guard = ( + scope["adapter"], + scope["directory"], + scope["guard"], + ) + frame = directory_wrapper_frame( + directory, "open", types.SimpleNamespace() + ) + if mode == "reference": + guard.directory_sync = copy.copy(directory) + elif mode == "foreign": + adapter.directory = types.SimpleNamespace(**directory.__dict__) + guard.directory_sync = adapter.directory + else: + foreign = ForeignDirectorySyncAdapter.__new__( + ForeignDirectorySyncAdapter + ) + foreign.__dict__.update(directory.__dict__) + adapter.directory, guard.directory_sync = foreign, foreign + frame = directory_wrapper_frame( + foreign, "open", types.SimpleNamespace() + ) + with self.assertRaisesRegex( + BOOT.BootstrapError, "capture_read_directory_chain_changed" + ): + adapter.producer_frame(frame, "open") + self.assertEqual(0, guard.sequence) + adapter.raw_open.assert_not_called() + + def test_capture_chain_requires_known_code_globals_self_and_bound_delegate( + self, + ) -> None: + for method in ("open", "close"): + for mode in ( + "code", + "globals", + "self", + "outer_missing", + "delegate_foreign", + "delegate_other_self", + "delegate_unbound", + "delegate_capture_subclass", + ): + with self.subTest(method=method, mode=mode), capture_scope() as scope: + adapter, directory = scope["adapter"], scope["directory"] + outer = types.SimpleNamespace() + frame = directory_wrapper_frame(directory, method, outer) + if mode == "code": + frame.f_code = getattr( + ForeignDirectorySyncAdapter, method + ).__code__ + elif mode == "globals": + frame.f_globals = dict(frame.f_globals) + elif mode == "self": + frame.f_locals["self"] = copy.copy(directory) + elif mode == "outer_missing": + frame.f_back = None + elif mode == "delegate_foreign": + setattr(directory, "raw_" + method, mock.Mock()) + elif mode == "delegate_other_self": + other = copy.copy(adapter) + setattr(directory, "raw_" + method, getattr(other, method)) + elif mode == "delegate_unbound": + setattr( + directory, + "raw_" + method, + getattr(BOOT.RetainedCaptureReadAdapter, method), + ) + else: + foreign = type( + "ForeignRetainedCaptureReadAdapter", + (BOOT.RetainedCaptureReadAdapter,), + {method: getattr(ForeignDirectorySyncAdapter, method)}, + ) + adapter.__class__ = foreign + setattr(directory, "raw_" + method, getattr(adapter, method)) + self.assertIs(adapter.producer_frame(frame, method), frame) + self.assertEqual(0, scope["guard"].sequence) + adapter.raw_open.assert_not_called() + adapter.raw_close.assert_not_called() + + def test_capture_directory_live_close_requires_exact_current_known_chain( + self, + ) -> None: + for mode in ( + "valid", + "subclass", + "foreign_code", + "foreign_globals", + "foreign_self", + "changed_reference", + ): + with self.subTest(mode=mode), capture_scope() as scope: + adapter, directory, guard, module = ( + scope["adapter"], + scope["directory"], + scope["guard"], + scope["module"], + ) + directory.bindings[87] = (scope["target"].parent, (1, 99), 1) + if mode == "subclass": + foreign = ForeignDirectorySyncAdapter.__new__( + ForeignDirectorySyncAdapter + ) + foreign.__dict__.update(directory.__dict__) + adapter.directory, guard.directory_sync = foreign, foreign + directory = foreign + outer = types.SimpleNamespace( + f_code=module._sync_directory.__code__, f_globals=module.__dict__ + ) + frame = directory_wrapper_frame(directory, "close", outer) + if mode == "foreign_code": + frame.f_code = ForeignDirectorySyncAdapter.close.__code__ + elif mode == "foreign_globals": + frame.f_globals = dict(frame.f_globals) + elif mode == "foreign_self": + frame.f_locals["self"] = copy.copy(directory) + elif mode == "changed_reference": + guard.directory_sync = copy.copy(directory) + with mock.patch.object(BOOT.sys, "_getframe", return_value=frame): + if mode == "valid": + adapter.close(87) + adapter.raw_close.assert_called_once_with(87) + self.assertEqual([], adapter.failures) + else: + with self.assertRaisesRegex( + BOOT.BootstrapError, "capture_read_foreign_live_close" + ): + adapter.close(87) + adapter.raw_close.assert_not_called() + self.assertEqual( + "foreign_close_live_bound_descriptor", + adapter.failures[-1]["reason"], + ) + self.assertIn(87, directory.bindings) + self.assertEqual(0, guard.sequence) + + def test_capture_chain_source_has_no_dynamic_expected_wrapper_code(self) -> None: + tree = ast.parse( + (Path(__file__).parent / "native/mac-native-bootstrap-v6.py").read_text() + ) + adapter = next( + node + for node in tree.body + if isinstance(node, ast.ClassDef) + and node.name == "RetainedCaptureReadAdapter" + ) + methods = { + node.name: node + for node in adapter.body + if isinstance(node, ast.FunctionDef) + } + for name in ("producer_frame", "close"): + dynamic = [ + node + for node in ast.walk(methods[name]) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "type" + and len(node.args) == 1 + and isinstance(node.args[0], ast.Attribute) + and node.args[0].attr == "directory" + ] + self.assertTrue(dynamic) + for call in dynamic: + parent = next( + node + for node in ast.walk(methods[name]) + if isinstance(node, ast.Compare) and node.left is call + ) + self.assertEqual( + [ast.dump(ast.Name(id="DirectorySyncAdapter", ctx=ast.Load()))], + [ast.dump(node) for node in parent.comparators], + ) + expected = next( + node + for node in ast.walk(methods["producer_frame"]) + if isinstance(node, ast.Assign) + and any( + isinstance(target, ast.Name) and target.id == "expected_delegate" + for target in node.targets + ) + ) + self.assertEqual( + ast.dump( + ast.parse("getattr(RetainedCaptureReadAdapter,method)").body[0].value + ), + ast.dump(expected.value), + ) + + def test_capture_catalog_is_exact_positive_current_and_explicit_previous( + self, + ) -> None: + for kind in ("capture-a1", "capture-a2", "capture-revision"): + row = case(kind) + catalog = POLICY.capture_read_catalog(row) + current = ( + POLICY.account_directory(row) + / "captures" + / ( + POLICY.digest(POLICY.encoded(POLICY.capture_metadata(row))) + + ".jsonl" + ) + ) + self.assertEqual( + (row["source_bytes"], row["source_sha256"]), catalog[current] + ) + self.assertEqual(2 if kind == "capture-revision" else 1, len(catalog)) + if kind == "capture-revision": + old = row["previous"] + self.assertEqual( + (old["byte_count"], old["sha256"]), + catalog[ + POLICY.account_directory(row) + / "captures" + / (old["key"] + ".jsonl") + ], + ) + for kind in ("negative-a3", "invalid-state", "flock-a4", "parser-a5"): + self.assertEqual({}, POLICY.capture_read_catalog(case(kind))) + + def test_capture_parent_read_grants_preserve_exact_wire_and_flags(self) -> None: + row = case("capture-revision") + for target in POLICY.capture_read_catalog(row): + for flags in (os.O_RDONLY, os.O_RDONLY | os.O_NOFOLLOW): + parent = POLICY.MutationPolicy(row) + answer = parent.decide( + mutation(1, "open", target, flags | getattr(os, "O_CLOEXEC", 0)) + ) + self.assertEqual( + {"type", "version", "seq", "path", "purpose"}, set(answer) + ) + self.assertEqual(str(target), answer["path"]) + self.assertEqual(1, parent.sequence) + for target in ( + Path(row["source"]), + POLICY.account_directory(row) / "captures/unknown.jsonl", + POLICY.account_directory(row) / "receipts/unknown.json", + ): + with self.subTest(target=target), self.assertRaises(POLICY.SafetyError): + POLICY.MutationPolicy(row).decide( + mutation(1, "open", target, os.O_RDONLY | os.O_NOFOLLOW) + ) + target = next(iter(POLICY.capture_read_catalog(row))) + for flags in ( + os.O_APPEND, + os.O_NONBLOCK | os.O_NOFOLLOW, + os.O_TRUNC, + os.O_RDWR, + ): + with self.subTest(flags=flags), self.assertRaises(POLICY.SafetyError): + POLICY.MutationPolicy(row).decide(mutation(1, "open", target, flags)) + + def test_actual_inventory_capture_gets_fresh_grant_hash_and_owned_close( + self, + ) -> None: + with capture_scope() as scope: + module, adapter, guard = scope["module"], scope["adapter"], scope["guard"] + entries = mock.MagicMock() + entries.__enter__.return_value = iter( + [types.SimpleNamespace(path=str(scope["target"]))] + ) + module.os.scandir = mock.Mock(return_value=entries) + self.assertEqual( + (1, len(scope["data"])), + module._capture_inventory(scope["target"].parent), + ) + adapter.raw_open.assert_called_once_with( + scope["target"], os.O_RDONLY | os.O_NOFOLLOW, 0o600 + ) + adapter.raw_close.assert_called_once_with(87) + self.assertEqual(1, guard.sequence) + self.assertFalse(guard.approved) + self.assertFalse(guard.readable(scope["target"])) + self.assertFalse(adapter.bindings) + self.assertEqual([], adapter.failures) + adapter.terminal() + self.assertEqual( + [0, len(scope["data"])], + [call.args[2] for call in scope["reader"].call_args_list], + ) + self.assertTrue( + all( + call.args == (87, 0, os.SEEK_CUR) + for call in scope["cursor"].call_args_list + ) + ) + + def test_capture_parent_target_hash_and_close_use_only_fresh_owned_descriptor( + self, + ) -> None: + with capture_scope() as scope: + with ( + mock.patch.object(POLICY.os, "open", return_value=87) as opened, + mock.patch.object(POLICY.os, "close") as closed, + ): + POLICY.verify_capture_read_target(scope["row"], scope["target"]) + opened.assert_called_once_with( + scope["target"], os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK + ) + closed.assert_called_once_with(87) + closed.reset_mock() + wrong = b"x" * len(scope["data"]) + with ( + mock.patch.object( + POLICY.os, + "pread", + side_effect=lambda fd, count, offset: wrong[ + offset : offset + count + ], + ), + self.assertRaises(POLICY.SafetyError), + ): + POLICY.verify_capture_read_target(scope["row"], scope["target"]) + closed.assert_called_once_with(87) + closed.reset_mock() + scope["info"].st_uid = 99 + with self.assertRaises(POLICY.SafetyError): + POLICY.verify_capture_read_target(scope["row"], scope["target"]) + closed.assert_not_called() + + def test_capture_verify_consumer_is_checked_before_fdopen_and_failure_stays_nonterminal( + self, + ) -> None: + with capture_scope() as scope: + module, adapter = scope["module"], scope["adapter"] + module.os.fdopen = mock.Mock( + side_effect=BOOT.BootstrapError( + "Synthetic stop after verified raw open" + ) + ) + capture = types.SimpleNamespace( + path=scope["target"], + metadata={ + "byte_count": len(scope["data"]), + "sha256": scope["row"]["source_sha256"], + }, + ) + with self.assertRaisesRegex(BOOT.BootstrapError, "Synthetic stop"): + module.verify_capture(capture) + self.assertEqual(1, scope["guard"].sequence) + self.assertIn(87, adapter.bindings) + module.os.fdopen.assert_called_once_with(87, "rb") + adapter.raw_close.assert_not_called() + with self.assertRaises(BOOT.BootstrapError): + adapter.terminal() + + def test_capture_foreign_code_raw_wrapper_and_generic_write_authority_refuse( + self, + ) -> None: + with capture_scope() as scope: + adapter, guard, target = scope["adapter"], scope["guard"], scope["target"] + guard.approved[target] = os.O_WRONLY + self.assertFalse(guard.readable(target)) + with self.assertRaises(BOOT.BootstrapError): + guard("open", (str(target), None, os.O_RDONLY | os.O_NOFOLLOW)) + with self.assertRaises(BOOT.BootstrapError): + adapter.open(target, os.O_RDONLY | os.O_NOFOLLOW, 0o600) + with self.assertRaises(BOOT.BootstrapError): + adapter.io_open(target, "rb") + with self.assertRaises(BOOT.BootstrapError): + adapter.worker_context(sys._getframe(), target) + clone = types.FunctionType( + scope["module"]._open_private.__code__, dict(scope["module"].__dict__) + ) + with self.assertRaises(BOOT.BootstrapError): + clone(target, os.O_RDONLY) + with self.assertRaises(BOOT.BootstrapError): + adapter.descriptor(87, "rb") + adapter.raw_open.assert_not_called() + + def test_capture_bounded_eof_hash_identity_and_access_failures(self) -> None: + import fcntl + + with capture_scope() as scope: + adapter, target, info = scope["adapter"], scope["target"], scope["info"] + identity = tuple(POLICY.file_identity(info)) + self.assertIs(info, adapter.verify(87, target, identity)) + for data in ( + scope["data"][:-1], + scope["data"] + b"x", + b"x" * len(scope["data"]), + ): + with ( + self.subTest(data=data), + mock.patch.object( + BOOT.os, + "pread", + side_effect=lambda fd, count, offset: data[ + offset : offset + count + ], + ), + self.assertRaises(BOOT.BootstrapError), + ): + adapter.verify(87, target, identity) + with ( + mock.patch.object(fcntl, "fcntl", return_value=os.O_RDWR), + self.assertRaises(BOOT.BootstrapError), + ): + adapter.verify(87, target, identity) + original = info.st_ino + info.st_ino = 999 + with self.assertRaises(BOOT.BootstrapError): + adapter.verify(87, target, identity) + info.st_ino = original + with ( + mock.patch.object(BOOT.os, "lseek", side_effect=[0, 1]), + self.assertRaises(BOOT.BootstrapError), + ): + adapter.verify(87, target, identity) + + def test_capture_live_bound_or_foreign_failed_return_is_never_closed_or_rebound( + self, + ) -> None: + with capture_scope() as scope: + adapter, target, info = scope["adapter"], scope["target"], scope["info"] + identity = tuple(POLICY.file_identity(info)) + original = (target, identity, 1) + adapter.bindings[87] = original + adapter.close_failed_open( + 87, target, identity, BOOT.BootstrapError("Synthetic live return") + ) + self.assertIs(original, adapter.bindings[87]) + self.assertFalse(adapter.failures[0]["closed"]) + adapter.raw_close.assert_not_called() + info.st_ino = 999 + with self.assertRaises(BOOT.BootstrapError): + adapter.close_failed_open( + 88, + target, + identity, + BOOT.BootstrapError("Synthetic foreign return"), + ) + adapter.raw_close.assert_not_called() + self.assertIs(original, adapter.bindings[87]) + with self.assertRaises(BOOT.BootstrapError): + adapter.terminal() + + def test_capture_failed_exclusive_grant_cannot_supply_following_read_authority( + self, + ) -> None: + with capture_scope() as scope: + module, adapter, guard = scope["module"], scope["adapter"], scope["guard"] + + def raw_open(path: object, flags: int, mode: int = 0o777) -> int: + guard( + "open", (os.fspath(path), None, flags | getattr(os, "O_CLOEXEC", 0)) + ) + if flags & os.O_EXCL: + raise FileExistsError("Retained immutable evidence") + return 87 + + adapter.raw_open.side_effect = raw_open + module.os.fdopen = mock.Mock( + side_effect=BOOT.BootstrapError( + "Synthetic stop after fresh readonly grant" + ) + ) + with self.assertRaisesRegex(BOOT.BootstrapError, "Synthetic stop"): + module._immutable(scope["target"], scope["data"]) + self.assertEqual(2, guard.sequence) + self.assertEqual(2, len(scope["protocol"].sent)) + self.assertTrue(scope["protocol"].sent[0]["flags"] & os.O_EXCL) + self.assertEqual( + os.O_RDONLY | os.O_NOFOLLOW, + scope["protocol"].sent[1]["flags"] & ~getattr(os, "O_CLOEXEC", 0), + ) + self.assertNotIn(scope["target"], guard.approved) + self.assertIn(87, adapter.bindings) + adapter.raw_close.assert_not_called() + + def test_directory_sync_live_bound_return_never_closes_or_replaces_prior_binding( + self, + ) -> None: + for original_identity in ((1, 2), (1, 99)): + with ( + self.subTest(original_identity=original_identity), + directory_scope() as scope, + ): + adapter = scope["adapter"] + account = POLICY.account_directory(scope["row"]) + previous = (account / "captures", original_identity, 1) + adapter.bindings[87] = previous + retained = {88: (1, 55, stat.S_IFDIR, 42)} + adapter.failed_open_bindings.update(retained) + with self.assertRaises(BOOT.BootstrapError): + scope["module"]._sync_directory(account) + adapter.raw_fsync.assert_not_called() + adapter.raw_close.assert_not_called() + self.assertEqual({87: previous}, adapter.bindings) + self.assertIs(adapter.bindings[87], previous) + self.assertEqual(retained, adapter.failed_open_bindings) + self.assertIsNone(adapter.pending) + self.assertEqual(1, len(adapter.failures)) + self.assertEqual(87, adapter.failures[0]["fd"]) + self.assertFalse(adapter.failures[0]["closed"]) + self.assertIn("bound_conflict", adapter.failures[0]["reason"]) + + def test_directory_sync_bound_conflict_retains_bounded_failure_even_without_observed_identity( + self, + ) -> None: + with directory_scope() as scope: + adapter = scope["adapter"] + previous = (POLICY.account_directory(scope["row"]), (1, 2), 1) + adapter.bindings[87] = previous + for observed in (None, scope["fd_info"]): + adapter.close_failed_open( + 87, observed, BOOT.BootstrapError("Synthetic conflict") + ) + adapter.raw_close.assert_not_called() + self.assertEqual({87: previous}, adapter.bindings) + self.assertFalse(adapter.failed_open_bindings) + self.assertEqual(2, len(adapter.failures)) + self.assertTrue( + all( + not row["closed"] and "bound_conflict" in row["reason"] + for row in adapter.failures + ) + ) + adapter.failures[:] = [ + { + "fd": 87, + "reason": "Retained synthetic bound conflict", + "closed": False, + } + for _ in range(BOOT.LIMITS["events"]) + ] + original = copy.deepcopy(adapter.failures) + with self.assertRaises(BOOT.BootstrapError): + adapter.close_failed_open( + 87, None, BOOT.BootstrapError("Over-limit conflict") + ) + self.assertEqual(original, adapter.failures) + self.assertEqual({87: previous}, adapter.bindings) + adapter.raw_close.assert_not_called() + + def test_directory_sync_exact_account_and_captures_parent_grants(self) -> None: + for kind in ("capture-a1", "capture-a2", "capture-revision"): + for child in (None, "captures"): + with ( + self.subTest(kind=kind, child=child), + directory_scope(kind) as scope, + ): + guard = scope["guard"] + adapter = scope["adapter"] + account = POLICY.account_directory(scope["row"]) + target = account if child is None else account / child + regular_intent = (Path(scope["row"]["source"]), os.O_RDONLY) + guard.open_intents[:] = [regular_intent] + scope["module"]._sync_directory(target) + adapter.raw_open.assert_called_once_with( + target, os.O_RDONLY | os.O_DIRECTORY, 0o777 + ) + adapter.raw_fsync.assert_called_once_with(87) + adapter.raw_close.assert_called_once_with(87) + self.assertEqual({}, adapter.bindings) + self.assertEqual({}, adapter.failed_open_bindings) + self.assertEqual([], adapter.failures) + self.assertEqual([regular_intent], guard.open_intents) + self.assertNotIn(target, guard.approved) + self.assertFalse(guard.readable(target)) + self.assertEqual(1, scope["protocol"].parent.sequence) + request = scope["protocol"].sent[0] + self.assertEqual(str(target), request["path"]) + self.assertEqual( + os.O_RDONLY | os.O_DIRECTORY, + request["flags"] & ~getattr(os, "O_CLOEXEC", 0), + ) + + def test_directory_sync_refuses_other_cases_and_paths_before_open(self) -> None: + for kind in ("negative-a3", "invalid-state", "parser-a5", "flock-a4"): + with self.subTest(kind=kind), directory_scope(kind) as scope: + with self.assertRaises(BOOT.BootstrapError): + scope["module"]._sync_directory( + POLICY.account_directory(scope["row"]) + ) + scope["adapter"].raw_open.assert_not_called() + scope["adapter"].raw_fsync.assert_not_called() + with directory_scope() as scope: + account = POLICY.account_directory(scope["row"]) + targets = ( + account / "receipts", + account / "logs", + account.parent, + account.parent / ("b" * 64), + POLICY.RUN_ROOT / "completion" / "foreign" / "captures", + POLICY.RUN_ROOT / "logs", + POLICY.RUN_ROOT.parent / "outside", + ) + for target in targets: + with ( + self.subTest(target=target), + self.assertRaises(BOOT.BootstrapError), + ): + scope["module"]._sync_directory(target) + scope["adapter"].raw_open.assert_not_called() + scope["adapter"].raw_fsync.assert_not_called() + + def test_directory_sync_strict_flags_owner_type_and_ungranted_reads(self) -> None: + with directory_scope() as scope: + target = POLICY.account_directory(scope["row"]) + for extra in ( + os.O_CREAT, + os.O_WRONLY, + os.O_RDWR, + os.O_TRUNC, + os.O_NOFOLLOW, + os.O_NONBLOCK, + ): + with self.subTest(extra=extra), self.assertRaises(BOOT.BootstrapError): + scope["adapter"].target(target, os.O_DIRECTORY | extra) + for flags in (True, os.O_RDONLY): + with self.assertRaises(BOOT.BootstrapError): + scope["adapter"].target(target, flags) + for field, value in ( + ("st_uid", 43), + ("st_mode", stat.S_IFDIR | 0o755), + ("st_mode", stat.S_IFREG | 0o600), + ): + previous = getattr(scope["target_info"], field) + setattr(scope["target_info"], field, value) + with ( + self.subTest(field=field, value=value), + self.assertRaises(BOOT.BootstrapError), + ): + scope["module"]._sync_directory(target) + setattr(scope["target_info"], field, previous) + with self.assertRaises(BOOT.BootstrapError): + scope["guard"]("open", (str(target), None, os.O_DIRECTORY)) + with self.assertRaises(BOOT.BootstrapError): + scope["adapter"].open(target, os.O_DIRECTORY) + scope["adapter"].raw_open.assert_not_called() + + def test_directory_sync_postopen_identity_failure_closes_matching_return( + self, + ) -> None: + for mode in ( + "path_swap", + "returned_identity", + "returned_type", + "returned_uid", + "access", + ): + with self.subTest(mode=mode), directory_scope() as scope: + if mode == "path_swap": + original = scope["adapter"].raw_open.side_effect + + def swap_path(path: object, flags: int, mode: int = 0o777) -> int: + fd = original(path, flags, mode) + scope["target_info"].st_ino = 3 + return fd + + scope["adapter"].raw_open.side_effect = swap_path + elif mode == "returned_identity": + scope["fd_info"].st_ino = 3 + elif mode == "returned_type": + scope["fd_info"].st_mode = stat.S_IFREG | 0o600 + elif mode == "returned_uid": + scope["fd_info"].st_uid = 43 + else: + scope["access"].return_value = os.O_RDWR + with self.assertRaises(BOOT.BootstrapError): + scope["module"]._sync_directory( + POLICY.account_directory(scope["row"]) + ) + scope["adapter"].raw_fsync.assert_not_called() + scope["adapter"].raw_close.assert_called_once_with(87) + self.assertFalse(scope["adapter"].bindings) + self.assertIsNone(scope["adapter"].pending) + self.assertTrue(scope["adapter"].failures[0]["closed"]) + + def test_directory_sync_reused_fd_before_fsync_refuses_foreign_close(self) -> None: + with directory_scope() as scope: + original = scope["fd_info"] + reused = types.SimpleNamespace( + st_dev=1, st_ino=99, st_uid=42, st_mode=stat.S_IFDIR | 0o700 + ) + scope["fstat"].side_effect = [original, original, reused, reused] + with self.assertRaises(BOOT.BootstrapError): + scope["module"]._sync_directory(POLICY.account_directory(scope["row"])) + scope["adapter"].raw_fsync.assert_not_called() + scope["adapter"].raw_close.assert_not_called() + self.assertIn(87, scope["adapter"].bindings) + self.assertFalse(scope["adapter"].failures[0]["closed"]) + + def test_directory_sync_failure_uses_original_finally_and_retains_close_failure( + self, + ) -> None: + for mode in ("fsync", "close", "postopen_close"): + with self.subTest(mode=mode), directory_scope() as scope: + if mode == "fsync": + scope["adapter"].raw_fsync.side_effect = OSError( + "Synthetic fsync failure" + ) + else: + scope["adapter"].raw_close.side_effect = OSError( + "Synthetic close failure" + ) + if mode == "postopen_close": + scope["fd_info"].st_ino = 3 + with self.assertRaises((OSError, BOOT.BootstrapError)): + scope["module"]._sync_directory( + POLICY.account_directory(scope["row"]) + ) + scope["adapter"].raw_close.assert_called_once_with(87) + if mode == "fsync": + self.assertFalse(scope["adapter"].bindings) + elif mode == "close": + self.assertIn(87, scope["adapter"].bindings) + else: + self.assertIn(87, scope["adapter"].failed_open_bindings) + + def test_directory_sync_windows_preservation_and_regular_fdopen_refusal( + self, + ) -> None: + with directory_scope() as scope: + scope["module"].os.name = "nt" + scope["module"]._sync_directory(POLICY.account_directory(scope["row"])) + scope["adapter"].raw_open.assert_not_called() + self.assertEqual([], scope["protocol"].sent) + scope["guard"].open_intents[:] = [ + (Path(scope["row"]["source"]), os.O_RDONLY) + ] + with self.assertRaises(BOOT.BootstrapError): + scope["guard"].descriptor(87, "rb") + self.assertFalse(scope["guard"].known_fds) + + def test_directory_sync_parent_exact_existing_directory_proof(self) -> None: + row = case() + account = POLICY.account_directory(row) + for target in (account, account / "captures"): + policy = POLICY.MutationPolicy(row) + request = mutation( + 1, + "open", + target, + os.O_RDONLY | os.O_DIRECTORY | getattr(os, "O_CLOEXEC", 0), + ) + grant = policy.decide(request) + with mock.patch.object(POLICY, "owned_path") as owned: + POLICY.mutation_target(grant, request) + self.assertEqual( + [ + mock.call(target.parent, directory=True), + mock.call(target, directory=True), + ], + owned.call_args_list, + ) + for kind in ("invalid-state", "negative-a3", "parser-a5", "flock-a4"): + policy = POLICY.MutationPolicy(case(kind)) + with self.assertRaises(POLICY.SafetyError): + policy.decide( + mutation(1, "open", policy.account, os.O_RDONLY | os.O_DIRECTORY) + ) + + def test_fixed_limits_and_proof_scope(self) -> None: + self.assertEqual(POLICY.LIMITS, BOOT.LIMITS) + self.assertEqual(POLICY.FREE_MIN - POLICY.LIMITS["rss_abort_bytes"], 201326592) + self.assertEqual(POLICY.PROOF_SCOPE, BOOT.PROOF_SCOPE) + self.assertEqual( + set(POLICY.SOURCE_FILES), {path for _, path in BOOT.SOURCE_MODULES} + ) + self.assertNotIn("installed", POLICY.PROOF_SCOPE) + + def test_exact_integer_and_hash(self) -> None: + for value in (True, False, 1.0, None, -1, 1 << 80): + with self.subTest(value=value), self.assertRaises(POLICY.SafetyError): + POLICY.integer(value) + for value in (True, "A" * 64, "a" * 63, "a" * 65, None): + with self.subTest(value=value), self.assertRaises(POLICY.SafetyError): + POLICY.hash_text(value) + self.assertEqual(POLICY.integer(0), 0) + self.assertEqual(POLICY.hash_text("a" * 64), "a" * 64) + + def test_sensor_admission_exact_floor_and_abi(self) -> None: + samples = [ + POLICY.Sensor(1_000_000_000 + n * 1_000_000_000, 1, POLICY.FREE_MIN, 100) + for n in range(3) + ] + POLICY.admission(samples, 3_500_000_000) + changes = [ + ("pressure", 0), + ("pressure", 2), + ("pressure", 3), + ("pressure", 4), + ("pressure", True), + ("free_bytes", POLICY.FREE_MIN - 1), + ("free_bytes", True), + ("swap_used_bytes", 101), + ("monotonic_ns", 1_000_000_000), + ] + for field, value in changes: + altered = [POLICY.asdict(row) for row in samples] + altered[-1][field] = value + with ( + self.subTest(field=field, value=value), + self.assertRaises(POLICY.SafetyError), + ): + POLICY.admission( + [POLICY.Sensor(**row) for row in altered], 3_500_000_000 + ) + for now in (2_999_999_999, 3_500_000_001): + with self.subTest(now=now), self.assertRaises(POLICY.SafetyError): + POLICY.admission(samples, now) + with self.assertRaises(POLICY.SafetyError): + POLICY.admission(samples[:2], 3_500_000_000) + with self.assertRaises(POLICY.SafetyError): + POLICY.admission( + [POLICY.Sensor(1 + n, 1, POLICY.FREE_MIN, 0) for n in range(3)], 3 + ) + + def test_manifest_and_case_fences(self) -> None: + self.assertEqual(POLICY.validate_manifest(manifest()), manifest()) + for kind in POLICY.CASES: + POLICY.validate_case(case(kind)) + changes = [ + ("version", True), + ("stage", "B"), + ("synthetic", False), + ("root_execution_go", False), + ("run_root", "/tmp"), + ("source_root", "/Users/j/dev/TrueMemory"), + ("sources", {}), + ("cases", []), + ("cases", [case(), case()]), + ] + for field, value in changes: + altered = manifest() + altered[field] = value + with self.subTest(field=field), self.assertRaises(POLICY.SafetyError): + POLICY.validate_manifest(altered) + for field, value in ( + ("id", []), + ("session", "unknown"), + ("database", "/tmp/personal.db"), + ("source", "/Users/j/dev/TrueMemory/transcript.jsonl"), + ("source_bytes", True), + ("source_identity", [1, 2, 23, 3, 4]), + ): + altered = case() + altered[field] = value + with self.subTest(field=field), self.assertRaises(POLICY.SafetyError): + POLICY.validate_case(altered) + altered = manifest() + altered["limits"]["wall_seconds"] = 6 + with self.assertRaises(POLICY.SafetyError): + POLICY.validate_manifest(altered) + altered = manifest() + altered["limits"]["core_bytes"] = False + with self.assertRaises(POLICY.SafetyError): + POLICY.validate_manifest(altered) + + def test_paths_and_versioned_results(self) -> None: + for name in ("v" + str(number) for number in range(1, 100)): + self.assertEqual( + POLICY.evidence_paths(POLICY.RUN_ROOT / ("manifest-" + name + ".json")), + (POLICY.RUN_ROOT / ("results-" + name + ".json"), name), + ) + for text in ( + "relative", + str(POLICY.RUN_ROOT) + "/../outside", + str(POLICY.RUN_ROOT) + "//escape", + "/tmp/file", + ): + with self.subTest(text=text), self.assertRaises(POLICY.SafetyError): + POLICY.canonical_path(text, POLICY.RUN_ROOT) + for name in ("v0", "v01", "v100", "v3x", "v3\n", "v3", "v٣"): + with self.subTest(name=name), self.assertRaises(POLICY.SafetyError): + POLICY.evidence_paths(POLICY.RUN_ROOT / ("manifest-" + name + ".json")) + with self.assertRaises(POLICY.SafetyError): + POLICY.evidence_paths(POLICY.RUN_ROOT.parent / "manifest-v3.json") + + def test_identity_is_current_owned_incarnation(self) -> None: + row = POLICY.Identity(72887, 42, 12345, 72887) + self.assertTrue(POLICY.same_owned_identity(row, row, 42)) + for changed in ( + POLICY.Identity(72887, 42, 12346, 72887), + POLICY.Identity(72887, 43, 12345, 72887), + POLICY.Identity(72887, 42, 12345, 1), + POLICY.Identity(True, 42, 12345, 1), + ): + with self.subTest(changed=changed): + self.assertFalse(POLICY.same_owned_identity(row, changed, 42)) + + def test_ready_and_result_exact_protocol(self) -> None: + ready = { + "type": "ready", + "version": 1, + "pid": 100, + "audit_installed": True, + "limits": { + "cpu": [2, 3], + "core": [0, 0], + "nofile": [64, 64], + "fsize": [1048576, 1048576], + }, + } + POLICY.validate_ready(ready, 100) + for field, value in ( + ("pid", 101), + ("version", True), + ("audit_installed", 1), + ("limits", {}), + ): + altered = copy.deepcopy(ready) + altered[field] = value + with self.subTest(field=field), self.assertRaises(POLICY.SafetyError): + POLICY.validate_ready(altered, 100) + altered = copy.deepcopy(ready) + altered["limits"]["core"] = [False, False] + with self.assertRaises(POLICY.SafetyError): + POLICY.validate_ready(altered, 100) + done = { + "type": "result", + "version": 1, + "case": "capture-a1", + "passed": True, + "proof_scope": POLICY.PROOF_SCOPE, + "import_mode": "isolated_source_module", + "sources": manifest()["sources"], + "details": {}, + } + POLICY.validate_result(done, case(), manifest()["sources"]) + for field, value in ( + ("passed", 1), + ("version", True), + ("proof_scope", "installed_host"), + ("case", "capture-a2"), + ("sources", {}), + ("details", []), + ): + altered = dict(done) + altered[field] = value + with self.subTest(field=field), self.assertRaises(POLICY.SafetyError): + POLICY.validate_result(altered, case(), manifest()["sources"]) + + def test_mutation_positive_and_one_pending_slot(self) -> None: + policy = POLICY.MutationPolicy(case()) + policy.decide(mutation(1, "os.mkdir", policy.account, 0o700)) + lock = policy.account / "state.lock" + policy.decide(mutation(2, "open", lock, os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW)) + pending = policy.account / ("state-" + "a" * 32 + ".pending") + policy.decide( + mutation( + 3, "open", pending, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW + ) + ) + grant = policy.decide( + mutation(4, "os.rename", policy.account / "state.json", source=pending) + ) + self.assertEqual(grant["path"], str(policy.account / "state.json")) + with self.assertRaises(POLICY.SafetyError): + policy.decide( + mutation( + 5, + "open", + policy.account / ("state-" + "b" * 32 + ".pending"), + os.O_WRONLY | os.O_CREAT | os.O_EXCL, + ) + ) + + def test_mutation_negative_controls(self) -> None: + for event, filename, flags, source in ( + ("open", "state.json", os.O_WRONLY | os.O_CREAT, None), + ("open", "state.lock", os.O_RDWR | os.O_CREAT | os.O_TRUNC, None), + ("open", "unknown", os.O_WRONLY | os.O_CREAT | os.O_EXCL, None), + ("os.mkdir", "foreign", 0o700, None), + ("os.mkdir", "captures", 0o755, None), + ("os.rename", "state.json", 0, POLICY.RUN_ROOT / "arbitrary"), + ("unlink", "state.json", 0, None), + ): + policy = POLICY.MutationPolicy(case()) + with ( + self.subTest(event=event, filename=filename), + self.assertRaises(POLICY.SafetyError), + ): + policy.decide( + mutation(1, event, policy.account / filename, flags, source) + ) + for sequence in (0, 2, True, 33): + policy = POLICY.MutationPolicy(case()) + with self.subTest(sequence=sequence), self.assertRaises(POLICY.SafetyError): + policy.decide(mutation(sequence, "os.mkdir", policy.account, 0o700)) + policy = POLICY.MutationPolicy(case("negative-a3")) + with self.assertRaises(POLICY.SafetyError): + policy.decide(mutation(1, "os.mkdir", policy.account, 0o700)) + + def test_fd_wrappers_bind_latest_consumed_intent(self) -> None: + import fcntl + + row = case() + guard = BOOT.AuditGuard(FakeProtocol(), {"case": row}, POLICY) + path = Path(row["source"]) + info = types.SimpleNamespace( + st_dev=1, st_ino=2, st_uid=42, st_mode=stat.S_IFREG | 0o600 + ) + with ( + mock.patch.object(BOOT.os, "fstat", return_value=info), + mock.patch.object(BOOT.os, "getuid", return_value=42), + mock.patch.object(fcntl, "fcntl", return_value=os.O_RDONLY), + mock.patch.object(Path, "lstat", return_value=info), + mock.patch.object(Path, "is_symlink", return_value=False), + ): + guard.open_intents[:] = [(path, os.O_RDONLY)] + guard.descriptor(7, "r") + self.assertEqual(guard.known_fds, {7: (1, 2)}) + with self.assertRaises(BOOT.BootstrapError): + guard.descriptor(7, "rb") + info.st_ino = 3 + guard.open_intents[:] = [(path, os.O_RDONLY)] + guard.descriptor(7, "rb") + self.assertEqual(guard.known_fds, {7: (1, 3)}) + guard.open_intents[:] = [(path, os.O_WRONLY)] + with self.assertRaises(BOOT.BootstrapError): + guard.descriptor(7, "rb") + self.assertFalse(guard.open_intents) + for fd, mode in ((0, "r"), (7, "a"), (7, "r+"), (True, "r")): + with ( + self.subTest(fd=fd, mode=mode), + self.assertRaises(BOOT.BootstrapError), + ): + guard.descriptor(fd, mode) + + def test_audit_preopen_grant_and_denials(self) -> None: + protocol = FakeProtocol() + guard = BOOT.AuditGuard(protocol, {"case": case()}, POLICY) + path = POLICY.account_directory(case()) / "state.lock" + protocol.answers.append( + { + "type": "grant", + "version": 1, + "seq": 1, + "path": str(path), + "purpose": "Owned fixed lock", + } + ) + guard.path = lambda value: Path(value) + guard("open", (str(path), None, os.O_RDWR | os.O_CREAT)) + self.assertEqual(len(protocol.sent), 1) + self.assertEqual(guard.open_intents, [(path, os.O_RDWR)]) + guard("truememory.native.audit_probe", ("fixed-stage-a-v1",)) + self.assertTrue(guard.activated) + for event, args in ( + ("socket.connect", ()), + ("subprocess.Popen", ()), + ("os.remove", (str(path), -1)), + ("sqlite3.connect", ("memory",)), + ("resource.setrlimit", (0, (1, 1))), + ("import", ("truememory.engine", None)), + ("import", ("torch", None)), + ("os.mkdir", (str(path), 0o700, 1)), + ("os.rename", (str(path), str(path), 1, -1)), + ): + with self.subTest(event=event), self.assertRaises(BOOT.BootstrapError): + guard(event, args) + + def test_prelaunch_refusals_retain_nonexecuted_result(self) -> None: + fake = types.ModuleType("fake_psutil") + fake.Error = FakePsutilError + with ( + mock.patch.object( + POLICY.selectors, "DefaultSelector", return_value=FakeSelector() + ), + mock.patch.object( + POLICY, "sample", side_effect=POLICY.SafetyError("pressure_or_headroom") + ), + mock.patch.object(POLICY.subprocess, "Popen") as launch, + ): + result = POLICY.execute_case(manifest(), case(), fake, None) + self.assertFalse(result["executed"]) + self.assertFalse(result["passed"]) + self.assertTrue(result["reaped"]) + self.assertIn("pressure_or_headroom", result["blocked"]) + launch.assert_not_called() + samples = [ + POLICY.Sensor(1_000_000_000 + n * 1_000_000_000, 1, POLICY.FREE_MIN, 100) + for n in range(3) + ] + with ( + mock.patch.object( + POLICY.selectors, "DefaultSelector", return_value=FakeSelector() + ), + mock.patch.object(POLICY, "sample", side_effect=samples), + mock.patch.object(POLICY.time, "sleep"), + mock.patch.object(POLICY.time, "monotonic_ns", return_value=3_000_000_001), + mock.patch.object( + POLICY, "owned_path", side_effect=POLICY.SafetyError("fixture_changed") + ), + mock.patch.object(POLICY.subprocess, "Popen") as launch, + ): + result = POLICY.execute_case(manifest(), case(), fake, None) + self.assertFalse(result["executed"]) + self.assertFalse(result["passed"]) + self.assertIn("fixture_changed", result["blocked"]) + self.assertEqual(len(result["sensors"]), 3) + launch.assert_not_called() + + def test_pipe_deadline_and_partial_writes_are_pure(self) -> None: + with ( + mock.patch.object(POLICY.time, "monotonic", return_value=1), + mock.patch.object(POLICY.os, "write", side_effect=[2, 3]) as writer, + ): + POLICY.write_pipe(7, b"12345", 1.1) + self.assertEqual(writer.call_count, 2) + with ( + mock.patch.object(POLICY.time, "monotonic", return_value=1), + mock.patch.object(POLICY.os, "write", side_effect=[BlockingIOError(), 3]), + mock.patch.object( + POLICY.select, "select", return_value=([], [7], []) + ) as poll, + ): + POLICY.write_pipe(7, b"123", 1.1) + self.assertEqual(poll.call_count, 1) + with ( + mock.patch.object(POLICY.time, "monotonic", return_value=1), + mock.patch.object(POLICY.os, "write") as writer, + ): + with self.assertRaises(POLICY.SafetyError): + POLICY.write_pipe(7, b"123", 0.9) + writer.assert_not_called() + + def test_owned_popen_fallback_and_group_probe_are_pure(self) -> None: + fake = types.ModuleType("fake_psutil") + fake.NoSuchProcess = FakePsutilError + child = mock.Mock() + child.poll.return_value = None + child.wait.return_value = 0 + child.returncode = 0 + result = { + "reaped": False, + "descendants": [], + "descendant_containment": "not_observed", + } + with mock.patch.object(POLICY.time, "monotonic", return_value=1): + POLICY.terminate_owned(child, None, [], fake, result) + child.send_signal.assert_called_once_with(POLICY.signal.SIGTERM) + self.assertTrue(result["reaped"]) + self.assertEqual(result["termination_authority"], "unreaped_direct_popen_child") + identity = POLICY.Identity(100, 42, 123, 100) + changed = POLICY.Identity(100, 42, 124, 100) + child = mock.Mock() + child.poll.return_value = None + result = { + "reaped": False, + "descendants": [], + "descendant_containment": "not_observed", + } + with ( + mock.patch.object(POLICY.time, "monotonic", return_value=1), + mock.patch.object(POLICY, "observe_identity", return_value=changed), + mock.patch.object(POLICY.os, "getuid", return_value=42), + mock.patch.object(POLICY.os, "killpg") as signal_group, + ): + with self.assertRaises(POLICY.SafetyError): + POLICY.terminate_owned(child, identity, [], fake, result) + child.send_signal.assert_not_called() + signal_group.assert_not_called() + with mock.patch.object(POLICY.os, "killpg", side_effect=ProcessLookupError()): + self.assertTrue(POLICY.group_gone(100)) + with mock.patch.object(POLICY.os, "killpg", return_value=None): + self.assertFalse(POLICY.group_gone(100)) + with mock.patch.object(POLICY.os, "killpg", side_effect=PermissionError()): + self.assertFalse(POLICY.group_gone(100)) + member = POLICY.Identity(101, 42, 123, 100) + self.assertTrue(POLICY.member_identity_matches(member, member, 100, 42)) + self.assertFalse(POLICY.member_identity_matches(member, member, 101, 42)) + + def test_protocol_bounds_use_no_real_stdio(self) -> None: + protocol = BOOT.Protocol() + with mock.patch.object( + BOOT.os, "write", side_effect=lambda fd, data: len(data) + ) as writer: + protocol.emit({"type": "synthetic"}) + self.assertEqual(writer.call_count, 1) + with self.assertRaises(BOOT.BootstrapError): + protocol.emit({"payload": "x" * 2048}) + protocol.written_bytes = BOOT.LIMITS["output_bytes"] + with self.assertRaises(BOOT.BootstrapError): + protocol.emit({"type": "synthetic"}) + with mock.patch.object(BOOT.select, "select", return_value=([], [], [])): + with self.assertRaises(BOOT.BootstrapError): + BOOT.Protocol().receive(2048, 0.1) + protocol = BOOT.Protocol() + with ( + mock.patch.object(BOOT.select, "select", return_value=([0], [], [])), + mock.patch.object(BOOT.os, "read", return_value=b"x" * 2049), + ): + with self.assertRaises(BOOT.BootstrapError): + protocol.receive(2048, 0.1) + + def test_attempt_versions_are_canonical_before_io(self) -> None: + for number in range(1, 100): + self.assertEqual( + POLICY.validate_attempt_version("v" + str(number)), "v" + str(number) + ) + fake = types.ModuleType("fake_psutil") + fake.Error = FakePsutilError + invalid = ( + True, + False, + None, + 3, + b"v3", + [], + "v0", + "v00", + "v01", + "v100", + "V3", + "v3\n", + "v3", + "v٣", + "../v3", + "v3.log", + ) + for version in invalid: + with ( + self.subTest(version=version), + mock.patch.object(POLICY.selectors, "DefaultSelector") as selector, + mock.patch.object(POLICY, "sample") as sensor, + mock.patch.object(POLICY, "new_log") as writer, + mock.patch.object(POLICY.subprocess, "Popen") as launch, + ): + with self.assertRaises(POLICY.SafetyError): + POLICY.validate_attempt_version(version) + with self.assertRaises(POLICY.SafetyError): + POLICY.execute_case( + manifest(), case(), fake, None, attempt_version=version + ) + selector.assert_not_called() + sensor.assert_not_called() + writer.assert_not_called() + launch.assert_not_called() + + def test_actual_retry_logs_and_legacy_default(self) -> None: + fake = types.ModuleType("fake_psutil") + fake.Error = FakePsutilError + info = types.SimpleNamespace( + st_dev=1, st_ino=2, st_size=24, st_mtime_ns=3, st_ctime_ns=4 + ) + samples = [ + POLICY.Sensor(1_000_000_000 + n * 1_000_000_000, 1, POLICY.FREE_MIN, 100) + for n in range(3) + ] + fixture = case() + fixture_bytes = b"x" * 24 + fixture["source_sha256"] = POLICY.digest(fixture_bytes) + plan = manifest() + source_bytes = { + POLICY.SOURCE_ROOT / name: ("synthetic:" + name).encode("utf-8") + for name in POLICY.SOURCE_FILES + } + plan["sources"] = { + name: POLICY.digest(source_bytes[POLICY.SOURCE_ROOT / name]) + for name in POLICY.SOURCE_FILES + } + bootstrap_path = POLICY.SOURCE_ROOT / "tests/native/mac-native-bootstrap-v6.py" + bootstrap_bytes = b"fixed-bootstrap-v2" + plan["bootstrap_sha256"] = POLICY.digest(bootstrap_bytes) + + def read_fixture(path: Path, maximum: int, *, private: bool = True) -> bytes: + if path == Path(fixture["source"]): + self.assertEqual((maximum, private), (4096, True)) + return fixture_bytes + self.assertEqual((maximum, private), (262144, False)) + if path == bootstrap_path: + return bootstrap_bytes + if path in source_bytes: + return source_bytes[path] + raise AssertionError("Unplanned synthetic source read") + + for version in (None, "v3", "v99"): + expected = version if version is not None else "v1" + with self.subTest(version=version), ExitStack() as patches: + patches.enter_context( + mock.patch.object( + POLICY.selectors, "DefaultSelector", return_value=FakeSelector() + ) + ) + patches.enter_context( + mock.patch.object(POLICY, "sample", side_effect=samples) + ) + patches.enter_context(mock.patch.object(POLICY.time, "sleep")) + patches.enter_context( + mock.patch.object( + POLICY.time, "monotonic_ns", return_value=3_000_000_001 + ) + ) + patches.enter_context( + mock.patch.object(POLICY, "owned_path", return_value=info) + ) + reader = patches.enter_context( + mock.patch.object(POLICY, "read_bounded", side_effect=read_fixture) + ) + writer = patches.enter_context( + mock.patch.object( + POLICY, + "new_log", + side_effect=[7, 8, POLICY.SafetyError("stop_before_launch")], + ) + ) + patches.enter_context(mock.patch.object(POLICY.os, "fsync")) + patches.enter_context(mock.patch.object(POLICY.os, "close")) + launch = patches.enter_context( + mock.patch.object(POLICY.subprocess, "Popen") + ) + result = ( + POLICY.execute_case(plan, fixture, fake, None) + if version is None + else POLICY.execute_case( + plan, fixture, fake, None, attempt_version=version + ) + ) + self.assertEqual( + [call.args[0] for call in writer.call_args_list], + [ + POLICY.RUN_ROOT + / "logs" + / ("capture-a1-" + channel + "-" + expected + ".log") + for channel in ("stdout", "stderr", "events") + ], + ) + self.assertEqual( + reader.call_args_list[-1].args[0], + POLICY.SOURCE_ROOT / "tests/native/mac-native-bootstrap-v6.py", + ) + self.assertFalse(result["executed"]) + self.assertFalse(result["passed"]) + self.assertTrue(result["reaped"]) + self.assertIn("stop_before_launch", result["blocked"]) + launch.assert_not_called() + + def test_main_reserves_result_before_runtime_and_case(self) -> None: + with main_scope("v3") as calls: + + def runtime() -> types.ModuleType: + self.assertEqual(calls["new_log"].call_count, 1) + calls["execute_case"].assert_not_called() + fake = types.ModuleType("fake_psutil") + fake.Error = FakePsutilError + return fake + + calls["pinned_runtime"].side_effect = runtime + self.assertEqual(POLICY.main(), 1) + self.assertEqual( + calls["new_log"].call_args.args[0], POLICY.RUN_ROOT / "results-v3.json" + ) + calls["new_log"].assert_called_once() + self.assertEqual( + calls["execute_case"].call_args.kwargs, {"attempt_version": "v3"} + ) + payload = json.loads(calls["write_all"].call_args.args[1]) + self.assertEqual(payload["manifest_version"], "v3") + self.assertFalse(payload["complete"]) + self.assertFalse(payload["cases"][0]["executed"]) + calls["fsync"].assert_called_once_with(7) + calls["close"].assert_called_once_with(7) + + def test_main_existing_result_refuses_without_runtime(self) -> None: + with main_scope("v3") as calls: + calls["new_log"].side_effect = FileExistsError("retained_attempt") + with self.assertRaises(FileExistsError): + POLICY.main() + for name in ( + "pinned_runtime", + "PressureReader", + "execute_case", + "write_all", + "fsync", + "close", + ): + calls[name].assert_not_called() + + def test_main_runtime_refusal_retains_reserved_result(self) -> None: + with main_scope("v99") as calls: + calls["pinned_runtime"].side_effect = POLICY.SafetyError("runtime_refusal") + self.assertEqual(POLICY.main(), 1) + payload = json.loads(calls["write_all"].call_args.args[1]) + self.assertEqual(payload["manifest_version"], "v99") + self.assertEqual(payload["cases"], []) + self.assertIn("runtime_refusal", payload["setup_failure"]) + self.assertFalse(payload["complete"]) + calls["execute_case"].assert_not_called() + calls["fsync"].assert_called_once_with(7) + calls["close"].assert_called_once_with(7) + + def test_main_fatal_retains_incomplete_reserved_result(self) -> None: + with main_scope("v3") as calls: + calls["pinned_runtime"].side_effect = RuntimeError("fatal_inert_fixture") + with self.assertRaisesRegex(RuntimeError, "fatal_inert_fixture"): + POLICY.main() + calls["new_log"].assert_called_once() + calls["write_all"].assert_not_called() + calls["execute_case"].assert_not_called() + calls["fsync"].assert_called_once_with(7) + calls["close"].assert_called_once_with(7) + + def test_main_revision_only_reserved_for_v2(self) -> None: + revision = manifest() + revised = case("capture-revision") + revised["previous"]["key"] = "d" * 64 + revision["cases"] = [revised] + for version in ("v1", "v3", "v99"): + with self.subTest(version=version), main_scope(version, revision) as calls: + with self.assertRaisesRegex( + POLICY.SafetyError, "unexpected_manifest_case_stage" + ): + POLICY.main() + calls["new_log"].assert_not_called() + calls["pinned_runtime"].assert_not_called() + with main_scope("v2") as calls: + with self.assertRaisesRegex( + POLICY.SafetyError, "unexpected_manifest_case_stage" + ): + POLICY.main() + calls["new_log"].assert_not_called() + with main_scope("v2", revision) as calls: + calls["pinned_runtime"].side_effect = POLICY.SafetyError( + "controlled_stage_stop" + ) + self.assertEqual(POLICY.main(), 1) + self.assertEqual( + calls["new_log"].call_args.args[0], POLICY.RUN_ROOT / "results-v2.json" + ) + + def test_bootstrap_uses_actual_v2_policy(self) -> None: + guard = BOOT.AuditGuard(FakeProtocol(), {"case": case()}, POLICY) + self.assertIn( + POLICY.SOURCE_ROOT / "tests/native/mac-native-acceptance-v6.py", + guard.read_paths, + ) + self.assertNotIn( + POLICY.SOURCE_ROOT / "tests/native/mac-native-acceptance-v1.py", + guard.read_paths, + ) + tree = ast.parse(Path(BOOT.__file__).read_bytes()) + calls = [ + node + for node in ast.walk(tree) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "compile_module" + ] + policy = [ + node + for node in calls + if node.args + and isinstance(node.args[0], ast.Constant) + and node.args[0].value == "mac_native_policy_v6" + ] + self.assertEqual(len(policy), 1) + self.assertIsInstance(policy[0].args[1], ast.BinOp) + self.assertEqual( + policy[0].args[1].right.value, "tests/native/mac-native-acceptance-v6.py" + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-mac-native-acceptance-v7.py b/tests/test-mac-native-acceptance-v7.py new file mode 100644 index 00000000..2e4754c0 --- /dev/null +++ b/tests/test-mac-native-acceptance-v7.py @@ -0,0 +1,2246 @@ +"""Pure Stage A policy tests. Never launch a child, signal, sensor or fixture.""" + +from __future__ import annotations + +import ast +import copy +import json +import os +import stat +import sys +import types +import unittest +from contextlib import ExitStack, contextmanager +from pathlib import Path +from typing import Callable, Iterator +from unittest import mock + + +def load_source(name: str, filename: str) -> types.ModuleType: + path = Path(__file__).parent / "native" / filename + module = types.ModuleType(name) + module.__file__ = str(path) + sys.modules[name] = module + exec( + compile(path.read_bytes(), str(path), "exec", dont_inherit=True), + module.__dict__, + ) + return module + + +POLICY = load_source("native_acceptance_policy_test_v7", "mac-native-acceptance-v7.py") +BOOT = load_source("native_acceptance_bootstrap_test_v7", "mac-native-bootstrap-v7.py") + + +def case(kind: str = "capture-a1") -> dict: + name = {"capture-revision": "capture-a2", "invalid-state": "negative-a3"}.get( + kind, kind + ) + database = ( + "capture-a2" + if kind == "capture-revision" + else "negative-a3" + if kind == "invalid-state" + else kind + ) + row = { + "id": kind, + "source": str( + POLICY.RUN_ROOT + / "home/.claude/projects/native-acceptance-v1" + / (name + ".jsonl") + ), + "source_sha256": "a" * 64, + "source_bytes": 24, + "source_identity": [1, 2, 24, 3, 4], + "session": "synthetic-native-" + name, + "database": str(POLICY.RUN_ROOT / (database + "-v1.db")), + "previous": None, + } + if kind == "flock-a4": + for field in ("source", "source_sha256", "source_bytes", "source_identity"): + row[field] = None + if kind == "capture-revision": + row["previous"] = {"key": "b" * 64, "sha256": "c" * 64, "byte_count": 12} + return row + + +def manifest() -> dict: + return { + "version": 1, + "stage": "A", + "synthetic": True, + "root_execution_go": True, + "run_root": str(POLICY.RUN_ROOT), + "home": str(POLICY.RUN_ROOT / "home"), + "source_root": str(POLICY.SOURCE_ROOT), + "source_commit": "a" * 40, + "sources": {name: "a" * 64 for name in POLICY.SOURCE_FILES}, + "supervisor_sha256": "b" * 64, + "bootstrap_sha256": "c" * 64, + "limits": dict(POLICY.LIMITS), + "cases": [case()], + } + + +def mutation( + seq: int, event: str, path: Path, flags: int = 0, source: Path | None = None +) -> dict: + return { + "type": "mutation", + "version": 1, + "seq": seq, + "event": event, + "path": str(path), + "flags": flags, + "source": str(source) if source is not None else None, + } + + +class FakeProtocol: + def __init__(self) -> None: + self.sent: list[object] = [] + self.answers: list[dict] = [] + + def emit(self, value: object) -> None: + self.sent.append(value) + + def receive(self, maximum: int, seconds: float) -> dict: + if maximum != 2048 or seconds != 0.1: + raise AssertionError("Changed frame or ACK bound") + return self.answers.pop(0) + + +class FakeSelector: + def close(self) -> None: + return None + + +class DirectoryProtocol(FakeProtocol): + def __init__(self, row: dict) -> None: + super().__init__() + self.parent = POLICY.MutationPolicy(row) + + def emit(self, value: object) -> None: + self.sent.append(value) + grant = self.parent.decide(value) + POLICY.mutation_target(grant, value) + self.answers.append(grant) + + +def actual_sync_module() -> types.ModuleType: + path = Path(__file__).parent.parent / "truememory/ingest/completion.py" + text = path.read_text() + node = next( + node + for node in ast.parse(text).body + if isinstance(node, ast.FunctionDef) and node.name == "_sync_directory" + ) + source = ast.get_source_segment(text, node) + if source is None: + raise AssertionError("Actual directory helper missing") + module = types.ModuleType("truememory.ingest.completion") + module.__file__ = str(BOOT.SOURCE_ROOT / "truememory/ingest/completion.py") + module.Path = Path + module.os = types.SimpleNamespace( + name="posix", O_RDONLY=os.O_RDONLY, O_DIRECTORY=os.O_DIRECTORY + ) + exec(compile(source, module.__file__, "exec", dont_inherit=True), module.__dict__) + return module + + +@contextmanager +def directory_scope( + kind: str = "capture-a1", after_open: Callable[[], None] | None = None +) -> Iterator[dict[str, object]]: + import fcntl + + row = case(kind) + protocol = DirectoryProtocol(row) + guard = BOOT.AuditGuard(protocol, {"case": row}, POLICY) + module = actual_sync_module() + adapter = BOOT.DirectorySyncAdapter(guard, module) + guard.directory_sync = adapter + target_info = types.SimpleNamespace( + st_dev=1, st_ino=2, st_uid=42, st_mode=stat.S_IFDIR | 0o700 + ) + fd_info = types.SimpleNamespace( + st_dev=1, st_ino=2, st_uid=42, st_mode=stat.S_IFDIR | 0o700 + ) + adapter.raw_fsync = mock.Mock() + adapter.raw_close = mock.Mock() + + def raw_open(path: object, flags: int, mode: int = 0o777) -> int: + guard("open", (os.fspath(path), None, flags | getattr(os, "O_CLOEXEC", 0))) + if after_open is not None: + after_open() + return 87 + + adapter.raw_open = mock.Mock(side_effect=raw_open) + module.os.open, module.os.fsync, module.os.close = ( + adapter.open, + adapter.fsync, + adapter.close, + ) + with ExitStack() as patches: + patches.enter_context(mock.patch.object(Path, "resolve", lambda value: value)) + patches.enter_context(mock.patch.object(Path, "is_symlink", return_value=False)) + patches.enter_context(mock.patch.object(BOOT.os, "getuid", return_value=42)) + patches.enter_context( + mock.patch.object(POLICY, "owned_path", return_value=target_info) + ) + fstat = patches.enter_context( + mock.patch.object(BOOT.os, "fstat", return_value=fd_info) + ) + access = patches.enter_context( + mock.patch.object(fcntl, "fcntl", return_value=os.O_RDONLY) + ) + yield { + "row": row, + "protocol": protocol, + "guard": guard, + "module": module, + "adapter": adapter, + "target_info": target_info, + "fd_info": fd_info, + "fstat": fstat, + "access": access, + } + + +class FakePsutilError(Exception): + pass + + +class CaptureProtocol(FakeProtocol): + def __init__(self, row: dict) -> None: + super().__init__() + self.parent = POLICY.MutationPolicy(row) + + def emit(self, value: object) -> None: + self.sent.append(value) + self.answers.append(self.parent.decide(value)) + + +def actual_capture_module() -> types.ModuleType: + path = Path(__file__).parent.parent / "truememory/ingest/completion.py" + tree = ast.parse(path.read_text()) + names = { + "_open_private", + "_read_private", + "_capture_inventory", + "_immutable", + "verify_capture", + "_sync_directory", + } + selected = [ + node + for node in tree.body + if isinstance(node, ast.FunctionDef) and node.name in names + ] + if {node.name for node in selected} != names: + raise AssertionError("Actual capture helpers missing") + module = types.ModuleType("truememory.ingest.completion") + module.__file__ = str(BOOT.SOURCE_ROOT / "truememory/ingest/completion.py") + module.Path, module.Capture = Path, object + module.CompletionError = type("CompletionError", (OSError,), {}) + module.MAX_CAPTURES, module.MAX_STATE_BYTES, module.MAX_CAPTURE_BYTES = ( + 1024, + 8388608, + 16777216, + ) + module.hashlib, module.stat = BOOT.hashlib, stat + module.os = types.SimpleNamespace( + name="posix", + O_RDONLY=os.O_RDONLY, + O_CREAT=os.O_CREAT, + O_EXCL=os.O_EXCL, + O_WRONLY=os.O_WRONLY, + O_DIRECTORY=os.O_DIRECTORY, + O_NOFOLLOW=os.O_NOFOLLOW, + getuid=lambda: 42, + fstat=lambda fd: BOOT.os.fstat(fd), + fdopen=BOOT.os.fdopen, + ) + exec( + compile( + ast.Module(body=selected, type_ignores=[]), + module.__file__, + "exec", + dont_inherit=True, + ), + module.__dict__, + ) + return module + + +@contextmanager +def capture_scope() -> Iterator[dict[str, object]]: + import fcntl + + data = b"Retained synthetic capture\n" + row = case() + row["source_bytes"] = len(data) + row["source_identity"][2] = len(data) + row["source_sha256"] = BOOT.hashlib.sha256(data).hexdigest() + protocol = CaptureProtocol(row) + guard = BOOT.AuditGuard(protocol, {"case": row}, POLICY) + module = actual_capture_module() + directory = BOOT.DirectorySyncAdapter(guard, module) + directory.installed = True + guard.directory_sync = directory + adapter = BOOT.RetainedCaptureReadAdapter(guard, module) + guard.capture_reads = adapter + directory.raw_open, directory.raw_close = adapter.open, adapter.close + module.os.open, module.os.close = directory.open, directory.close + adapter.raw_close = mock.Mock() + target = next(iter(adapter.catalog)) + info = types.SimpleNamespace( + st_dev=1, + st_ino=2, + st_uid=42, + st_mode=stat.S_IFREG | 0o600, + st_nlink=1, + st_size=len(data), + st_mtime_ns=3, + st_ctime_ns=4, + ) + directory_info = types.SimpleNamespace( + st_dev=1, st_ino=99, st_uid=42, st_mode=stat.S_IFDIR | 0o700 + ) + + def raw_open(path: object, flags: int, mode: int = 0o777) -> int: + guard("open", (os.fspath(path), None, flags | getattr(os, "O_CLOEXEC", 0))) + return 87 + + def owned(path: Path, *, directory: bool = False) -> types.SimpleNamespace: + return directory_info if directory else info + + def pread(fd: int, count: int, offset: int) -> bytes: + if fd != 87 or not 0 < count <= 4097: + raise AssertionError("Changed bounded descriptor read") + return data[offset : offset + count] + + adapter.raw_open = mock.Mock(side_effect=raw_open) + with ExitStack() as patches: + patches.enter_context(mock.patch.object(Path, "resolve", lambda value: value)) + patches.enter_context(mock.patch.object(Path, "is_symlink", return_value=False)) + patches.enter_context(mock.patch.object(BOOT.os, "getuid", return_value=42)) + patches.enter_context( + mock.patch.object(POLICY, "owned_path", side_effect=owned) + ) + patches.enter_context(mock.patch.object(BOOT.os, "fstat", return_value=info)) + patches.enter_context( + mock.patch.object(fcntl, "fcntl", return_value=os.O_RDONLY) + ) + cursor = patches.enter_context( + mock.patch.object(BOOT.os, "lseek", return_value=0) + ) + reader = patches.enter_context( + mock.patch.object(BOOT.os, "pread", side_effect=pread) + ) + yield { + "row": row, + "protocol": protocol, + "guard": guard, + "module": module, + "adapter": adapter, + "directory": directory, + "target": target, + "info": info, + "data": data, + "cursor": cursor, + "reader": reader, + } + + +@contextmanager +def main_scope( + version: str = "v3", fixture: dict | None = None +) -> Iterator[dict[str, mock.Mock]]: + row = copy.deepcopy(fixture) if fixture is not None else manifest() + parser = mock.Mock() + parser.parse_args.return_value = types.SimpleNamespace( + manifest=str(POLICY.RUN_ROOT / ("manifest-" + version + ".json")), + execute_root_authorized=True, + ) + fake = types.ModuleType("fake_psutil") + fake.Error = FakePsutilError + with ExitStack() as patches: + patches.enter_context( + mock.patch.object(POLICY.argparse, "ArgumentParser", return_value=parser) + ) + patches.enter_context(mock.patch.object(POLICY.sys, "platform", "darwin")) + patches.enter_context( + mock.patch.object( + POLICY.sys, + "flags", + types.SimpleNamespace( + isolated=True, no_site=True, dont_write_bytecode=True + ), + ) + ) + patches.enter_context( + mock.patch.object(POLICY.sys, "dont_write_bytecode", True) + ) + patches.enter_context(mock.patch.object(POLICY.os, "umask", return_value=0o077)) + calls = { + "owned_path": patches.enter_context( + mock.patch.object(POLICY, "owned_path") + ), + "read_bounded": patches.enter_context( + mock.patch.object( + POLICY, "read_bounded", return_value=POLICY.encoded(row) + ) + ), + "digest": patches.enter_context( + mock.patch.object(POLICY, "digest", return_value="b" * 64) + ), + "new_log": patches.enter_context( + mock.patch.object(POLICY, "new_log", return_value=7) + ), + "pinned_runtime": patches.enter_context( + mock.patch.object(POLICY, "pinned_runtime", return_value=fake) + ), + "PressureReader": patches.enter_context( + mock.patch.object(POLICY, "PressureReader") + ), + "execute_case": patches.enter_context( + mock.patch.object( + POLICY, + "execute_case", + return_value={ + "passed": False, + "reaped": True, + "executed": False, + "blocked": "pressure_or_headroom", + }, + ) + ), + "write_all": patches.enter_context(mock.patch.object(POLICY, "write_all")), + "fsync": patches.enter_context(mock.patch.object(POLICY.os, "fsync")), + "close": patches.enter_context(mock.patch.object(POLICY.os, "close")), + } + yield calls + + +class ForeignDirectorySyncAdapter(BOOT.DirectorySyncAdapter): + def open( + self, path: object, flags: int, mode: int = 0o777, *, dir_fd: int | None = None + ) -> int: + if dir_fd is None: + return self.raw_open(path, flags, mode) + return self.raw_open(path, flags, mode, dir_fd=dir_fd) + + def close(self, fd: int) -> None: + self.raw_close(fd) + + +def directory_wrapper_frame( + directory: BOOT.DirectorySyncAdapter, method: str, outer: types.SimpleNamespace +) -> types.SimpleNamespace: + function = getattr(type(directory), method) + return types.SimpleNamespace( + f_code=function.__code__, + f_globals=function.__globals__, + f_locals={"self": directory}, + f_back=outer, + ) + + +class PurePolicyTests(unittest.TestCase): + def test_capture_constructor_requires_exact_installed_directory_adapter( + self, + ) -> None: + for mode in ( + "missing", + "foreign", + "subclass", + "not_installed", + "truthy_int", + "truthy_text", + ): + with self.subTest(mode=mode), capture_scope() as scope: + original = scope["directory"] + if mode == "missing": + directory = None + elif mode == "foreign": + directory = types.SimpleNamespace(installed=True) + elif mode == "subclass": + directory = ForeignDirectorySyncAdapter.__new__( + ForeignDirectorySyncAdapter + ) + directory.__dict__.update(original.__dict__) + else: + directory = original + directory.installed = { + "not_installed": False, + "truthy_int": 1, + "truthy_text": "installed", + }[mode] + scope["guard"].directory_sync = directory + with self.assertRaisesRegex( + BOOT.BootstrapError, "capture_read_directory_chain_missing" + ): + BOOT.RetainedCaptureReadAdapter(scope["guard"], scope["module"]) + scope["adapter"].raw_open.assert_not_called() + self.assertEqual(0, scope["guard"].sequence) + + def test_capture_fixed_known_directory_frame_and_delegate_positive(self) -> None: + with capture_scope() as scope: + adapter, directory = scope["adapter"], scope["directory"] + outer = types.SimpleNamespace() + for method in ("open", "close"): + with self.subTest(method=method): + frame = directory_wrapper_frame(directory, method, outer) + self.assertIs(adapter.producer_frame(frame, method), outer) + adapter.raw_open.assert_not_called() + adapter.raw_close.assert_not_called() + self.assertEqual(0, scope["guard"].sequence) + + def test_capture_postconstruction_subclass_inventory_refuses_before_grant( + self, + ) -> None: + with capture_scope() as scope: + adapter, guard, module = scope["adapter"], scope["guard"], scope["module"] + directory = ForeignDirectorySyncAdapter.__new__(ForeignDirectorySyncAdapter) + directory.__dict__.update(scope["directory"].__dict__) + adapter.directory, guard.directory_sync = directory, directory + module.os.open, module.os.close = directory.open, directory.close + entries = mock.MagicMock() + entries.__enter__.return_value = iter( + [types.SimpleNamespace(path=str(scope["target"]))] + ) + module.os.scandir = mock.Mock(return_value=entries) + with self.assertRaisesRegex( + BOOT.BootstrapError, "capture_read_directory_chain_changed" + ): + module._capture_inventory(scope["target"].parent) + adapter.raw_open.assert_not_called() + adapter.raw_close.assert_not_called() + self.assertEqual(0, guard.sequence) + self.assertEqual({}, adapter.bindings) + + def test_capture_postconstruction_foreign_or_changed_reference_refuses( + self, + ) -> None: + for mode in ("subclass", "foreign", "reference"): + with self.subTest(mode=mode), capture_scope() as scope: + adapter, directory, guard = ( + scope["adapter"], + scope["directory"], + scope["guard"], + ) + frame = directory_wrapper_frame( + directory, "open", types.SimpleNamespace() + ) + if mode == "reference": + guard.directory_sync = copy.copy(directory) + elif mode == "foreign": + adapter.directory = types.SimpleNamespace(**directory.__dict__) + guard.directory_sync = adapter.directory + else: + foreign = ForeignDirectorySyncAdapter.__new__( + ForeignDirectorySyncAdapter + ) + foreign.__dict__.update(directory.__dict__) + adapter.directory, guard.directory_sync = foreign, foreign + frame = directory_wrapper_frame( + foreign, "open", types.SimpleNamespace() + ) + with self.assertRaisesRegex( + BOOT.BootstrapError, "capture_read_directory_chain_changed" + ): + adapter.producer_frame(frame, "open") + self.assertEqual(0, guard.sequence) + adapter.raw_open.assert_not_called() + + def test_capture_chain_requires_known_code_globals_self_and_bound_delegate( + self, + ) -> None: + for method in ("open", "close"): + for mode in ( + "code", + "globals", + "self", + "outer_missing", + "delegate_foreign", + "delegate_other_self", + "delegate_unbound", + "delegate_capture_subclass", + ): + with self.subTest(method=method, mode=mode), capture_scope() as scope: + adapter, directory = scope["adapter"], scope["directory"] + outer = types.SimpleNamespace() + frame = directory_wrapper_frame(directory, method, outer) + if mode == "code": + frame.f_code = getattr( + ForeignDirectorySyncAdapter, method + ).__code__ + elif mode == "globals": + frame.f_globals = dict(frame.f_globals) + elif mode == "self": + frame.f_locals["self"] = copy.copy(directory) + elif mode == "outer_missing": + frame.f_back = None + elif mode == "delegate_foreign": + setattr(directory, "raw_" + method, mock.Mock()) + elif mode == "delegate_other_self": + other = copy.copy(adapter) + setattr(directory, "raw_" + method, getattr(other, method)) + elif mode == "delegate_unbound": + setattr( + directory, + "raw_" + method, + getattr(BOOT.RetainedCaptureReadAdapter, method), + ) + else: + foreign = type( + "ForeignRetainedCaptureReadAdapter", + (BOOT.RetainedCaptureReadAdapter,), + {method: getattr(ForeignDirectorySyncAdapter, method)}, + ) + adapter.__class__ = foreign + setattr(directory, "raw_" + method, getattr(adapter, method)) + self.assertIs(adapter.producer_frame(frame, method), frame) + self.assertEqual(0, scope["guard"].sequence) + adapter.raw_open.assert_not_called() + adapter.raw_close.assert_not_called() + + def test_capture_directory_live_close_requires_exact_current_known_chain( + self, + ) -> None: + for mode in ( + "valid", + "subclass", + "foreign_code", + "foreign_globals", + "foreign_self", + "changed_reference", + ): + with self.subTest(mode=mode), capture_scope() as scope: + adapter, directory, guard, module = ( + scope["adapter"], + scope["directory"], + scope["guard"], + scope["module"], + ) + directory.bindings[87] = (scope["target"].parent, (1, 99), 1) + if mode == "subclass": + foreign = ForeignDirectorySyncAdapter.__new__( + ForeignDirectorySyncAdapter + ) + foreign.__dict__.update(directory.__dict__) + adapter.directory, guard.directory_sync = foreign, foreign + directory = foreign + outer = types.SimpleNamespace( + f_code=module._sync_directory.__code__, f_globals=module.__dict__ + ) + frame = directory_wrapper_frame(directory, "close", outer) + if mode == "foreign_code": + frame.f_code = ForeignDirectorySyncAdapter.close.__code__ + elif mode == "foreign_globals": + frame.f_globals = dict(frame.f_globals) + elif mode == "foreign_self": + frame.f_locals["self"] = copy.copy(directory) + elif mode == "changed_reference": + guard.directory_sync = copy.copy(directory) + with mock.patch.object(BOOT.sys, "_getframe", return_value=frame): + if mode == "valid": + adapter.close(87) + adapter.raw_close.assert_called_once_with(87) + self.assertEqual([], adapter.failures) + else: + with self.assertRaisesRegex( + BOOT.BootstrapError, "capture_read_foreign_live_close" + ): + adapter.close(87) + adapter.raw_close.assert_not_called() + self.assertEqual( + "foreign_close_live_bound_descriptor", + adapter.failures[-1]["reason"], + ) + self.assertIn(87, directory.bindings) + self.assertEqual(0, guard.sequence) + + def test_capture_chain_source_has_no_dynamic_expected_wrapper_code(self) -> None: + tree = ast.parse( + (Path(__file__).parent / "native/mac-native-bootstrap-v7.py").read_text() + ) + adapter = next( + node + for node in tree.body + if isinstance(node, ast.ClassDef) + and node.name == "RetainedCaptureReadAdapter" + ) + methods = { + node.name: node + for node in adapter.body + if isinstance(node, ast.FunctionDef) + } + for name in ("producer_frame", "close"): + dynamic = [ + node + for node in ast.walk(methods[name]) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "type" + and len(node.args) == 1 + and isinstance(node.args[0], ast.Attribute) + and node.args[0].attr == "directory" + ] + self.assertTrue(dynamic) + for call in dynamic: + parent = next( + node + for node in ast.walk(methods[name]) + if isinstance(node, ast.Compare) and node.left is call + ) + self.assertEqual( + [ast.dump(ast.Name(id="DirectorySyncAdapter", ctx=ast.Load()))], + [ast.dump(node) for node in parent.comparators], + ) + expected = next( + node + for node in ast.walk(methods["producer_frame"]) + if isinstance(node, ast.Assign) + and any( + isinstance(target, ast.Name) and target.id == "expected_delegate" + for target in node.targets + ) + ) + self.assertEqual( + ast.dump( + ast.parse("getattr(RetainedCaptureReadAdapter,method)").body[0].value + ), + ast.dump(expected.value), + ) + + def test_capture_catalog_is_exact_positive_current_and_explicit_previous( + self, + ) -> None: + for kind in ("capture-a1", "capture-a2", "capture-revision"): + row = case(kind) + catalog = POLICY.capture_read_catalog(row) + current = ( + POLICY.account_directory(row) + / "captures" + / ( + POLICY.digest(POLICY.encoded(POLICY.capture_metadata(row))) + + ".jsonl" + ) + ) + self.assertEqual( + (row["source_bytes"], row["source_sha256"]), catalog[current] + ) + self.assertEqual(2 if kind == "capture-revision" else 1, len(catalog)) + if kind == "capture-revision": + old = row["previous"] + self.assertEqual( + (old["byte_count"], old["sha256"]), + catalog[ + POLICY.account_directory(row) + / "captures" + / (old["key"] + ".jsonl") + ], + ) + for kind in ("negative-a3", "invalid-state", "flock-a4", "parser-a5"): + self.assertEqual({}, POLICY.capture_read_catalog(case(kind))) + + def test_capture_parent_read_grants_preserve_exact_wire_and_flags(self) -> None: + row = case("capture-revision") + for target in POLICY.capture_read_catalog(row): + for flags in (os.O_RDONLY, os.O_RDONLY | os.O_NOFOLLOW): + parent = POLICY.MutationPolicy(row) + answer = parent.decide( + mutation(1, "open", target, flags | getattr(os, "O_CLOEXEC", 0)) + ) + self.assertEqual( + {"type", "version", "seq", "path", "purpose"}, set(answer) + ) + self.assertEqual(str(target), answer["path"]) + self.assertEqual(1, parent.sequence) + for target in ( + Path(row["source"]), + POLICY.account_directory(row) / "captures/unknown.jsonl", + POLICY.account_directory(row) / "receipts/unknown.json", + ): + with self.subTest(target=target), self.assertRaises(POLICY.SafetyError): + POLICY.MutationPolicy(row).decide( + mutation(1, "open", target, os.O_RDONLY | os.O_NOFOLLOW) + ) + target = next(iter(POLICY.capture_read_catalog(row))) + for flags in ( + os.O_APPEND, + os.O_NONBLOCK | os.O_NOFOLLOW, + os.O_TRUNC, + os.O_RDWR, + ): + with self.subTest(flags=flags), self.assertRaises(POLICY.SafetyError): + POLICY.MutationPolicy(row).decide(mutation(1, "open", target, flags)) + + def test_actual_inventory_capture_gets_fresh_grant_hash_and_owned_close( + self, + ) -> None: + with capture_scope() as scope: + module, adapter, guard = scope["module"], scope["adapter"], scope["guard"] + entries = mock.MagicMock() + entries.__enter__.return_value = iter( + [types.SimpleNamespace(path=str(scope["target"]))] + ) + module.os.scandir = mock.Mock(return_value=entries) + self.assertEqual( + (1, len(scope["data"])), + module._capture_inventory(scope["target"].parent), + ) + adapter.raw_open.assert_called_once_with( + scope["target"], os.O_RDONLY | os.O_NOFOLLOW, 0o600 + ) + adapter.raw_close.assert_called_once_with(87) + self.assertEqual(1, guard.sequence) + self.assertFalse(guard.approved) + self.assertFalse(guard.readable(scope["target"])) + self.assertFalse(adapter.bindings) + self.assertEqual([], adapter.failures) + adapter.terminal() + self.assertEqual( + [0, len(scope["data"])], + [call.args[2] for call in scope["reader"].call_args_list], + ) + self.assertTrue( + all( + call.args == (87, 0, os.SEEK_CUR) + for call in scope["cursor"].call_args_list + ) + ) + + def test_capture_parent_target_hash_and_close_use_only_fresh_owned_descriptor( + self, + ) -> None: + with capture_scope() as scope: + with ( + mock.patch.object(POLICY.os, "open", return_value=87) as opened, + mock.patch.object(POLICY.os, "close") as closed, + ): + POLICY.verify_capture_read_target(scope["row"], scope["target"]) + opened.assert_called_once_with( + scope["target"], os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK + ) + closed.assert_called_once_with(87) + closed.reset_mock() + wrong = b"x" * len(scope["data"]) + with ( + mock.patch.object( + POLICY.os, + "pread", + side_effect=lambda fd, count, offset: wrong[ + offset : offset + count + ], + ), + self.assertRaises(POLICY.SafetyError), + ): + POLICY.verify_capture_read_target(scope["row"], scope["target"]) + closed.assert_called_once_with(87) + closed.reset_mock() + scope["info"].st_uid = 99 + with self.assertRaises(POLICY.SafetyError): + POLICY.verify_capture_read_target(scope["row"], scope["target"]) + closed.assert_not_called() + + def test_capture_verify_consumer_is_checked_before_fdopen_and_failure_stays_nonterminal( + self, + ) -> None: + with capture_scope() as scope: + module, adapter = scope["module"], scope["adapter"] + module.os.fdopen = mock.Mock( + side_effect=BOOT.BootstrapError( + "Synthetic stop after verified raw open" + ) + ) + capture = types.SimpleNamespace( + path=scope["target"], + metadata={ + "byte_count": len(scope["data"]), + "sha256": scope["row"]["source_sha256"], + }, + ) + with self.assertRaisesRegex(BOOT.BootstrapError, "Synthetic stop"): + module.verify_capture(capture) + self.assertEqual(1, scope["guard"].sequence) + self.assertIn(87, adapter.bindings) + module.os.fdopen.assert_called_once_with(87, "rb") + adapter.raw_close.assert_not_called() + with self.assertRaises(BOOT.BootstrapError): + adapter.terminal() + + def test_capture_foreign_code_raw_wrapper_and_generic_write_authority_refuse( + self, + ) -> None: + with capture_scope() as scope: + adapter, guard, target = scope["adapter"], scope["guard"], scope["target"] + guard.approved[target] = os.O_WRONLY + self.assertFalse(guard.readable(target)) + with self.assertRaises(BOOT.BootstrapError): + guard("open", (str(target), None, os.O_RDONLY | os.O_NOFOLLOW)) + with self.assertRaises(BOOT.BootstrapError): + adapter.open(target, os.O_RDONLY | os.O_NOFOLLOW, 0o600) + with self.assertRaises(BOOT.BootstrapError): + adapter.io_open(target, "rb") + with self.assertRaises(BOOT.BootstrapError): + adapter.worker_context(sys._getframe(), target) + clone = types.FunctionType( + scope["module"]._open_private.__code__, dict(scope["module"].__dict__) + ) + with self.assertRaises(BOOT.BootstrapError): + clone(target, os.O_RDONLY) + with self.assertRaises(BOOT.BootstrapError): + adapter.descriptor(87, "rb") + adapter.raw_open.assert_not_called() + + def test_capture_bounded_eof_hash_identity_and_access_failures(self) -> None: + import fcntl + + with capture_scope() as scope: + adapter, target, info = scope["adapter"], scope["target"], scope["info"] + identity = tuple(POLICY.file_identity(info)) + self.assertIs(info, adapter.verify(87, target, identity)) + for data in ( + scope["data"][:-1], + scope["data"] + b"x", + b"x" * len(scope["data"]), + ): + with ( + self.subTest(data=data), + mock.patch.object( + BOOT.os, + "pread", + side_effect=lambda fd, count, offset: data[ + offset : offset + count + ], + ), + self.assertRaises(BOOT.BootstrapError), + ): + adapter.verify(87, target, identity) + with ( + mock.patch.object(fcntl, "fcntl", return_value=os.O_RDWR), + self.assertRaises(BOOT.BootstrapError), + ): + adapter.verify(87, target, identity) + original = info.st_ino + info.st_ino = 999 + with self.assertRaises(BOOT.BootstrapError): + adapter.verify(87, target, identity) + info.st_ino = original + with ( + mock.patch.object(BOOT.os, "lseek", side_effect=[0, 1]), + self.assertRaises(BOOT.BootstrapError), + ): + adapter.verify(87, target, identity) + + def test_capture_live_bound_or_foreign_failed_return_is_never_closed_or_rebound( + self, + ) -> None: + with capture_scope() as scope: + adapter, target, info = scope["adapter"], scope["target"], scope["info"] + identity = tuple(POLICY.file_identity(info)) + original = (target, identity, 1) + adapter.bindings[87] = original + adapter.close_failed_open( + 87, target, identity, BOOT.BootstrapError("Synthetic live return") + ) + self.assertIs(original, adapter.bindings[87]) + self.assertFalse(adapter.failures[0]["closed"]) + adapter.raw_close.assert_not_called() + info.st_ino = 999 + with self.assertRaises(BOOT.BootstrapError): + adapter.close_failed_open( + 88, + target, + identity, + BOOT.BootstrapError("Synthetic foreign return"), + ) + adapter.raw_close.assert_not_called() + self.assertIs(original, adapter.bindings[87]) + with self.assertRaises(BOOT.BootstrapError): + adapter.terminal() + + def test_capture_failed_exclusive_grant_cannot_supply_following_read_authority( + self, + ) -> None: + with capture_scope() as scope: + module, adapter, guard = scope["module"], scope["adapter"], scope["guard"] + + def raw_open(path: object, flags: int, mode: int = 0o777) -> int: + guard( + "open", (os.fspath(path), None, flags | getattr(os, "O_CLOEXEC", 0)) + ) + if flags & os.O_EXCL: + raise FileExistsError("Retained immutable evidence") + return 87 + + adapter.raw_open.side_effect = raw_open + module.os.fdopen = mock.Mock( + side_effect=BOOT.BootstrapError( + "Synthetic stop after fresh readonly grant" + ) + ) + with self.assertRaisesRegex(BOOT.BootstrapError, "Synthetic stop"): + module._immutable(scope["target"], scope["data"]) + self.assertEqual(2, guard.sequence) + self.assertEqual(2, len(scope["protocol"].sent)) + self.assertTrue(scope["protocol"].sent[0]["flags"] & os.O_EXCL) + self.assertEqual( + os.O_RDONLY | os.O_NOFOLLOW, + scope["protocol"].sent[1]["flags"] & ~getattr(os, "O_CLOEXEC", 0), + ) + self.assertNotIn(scope["target"], guard.approved) + self.assertIn(87, adapter.bindings) + adapter.raw_close.assert_not_called() + + def test_directory_sync_live_bound_return_never_closes_or_replaces_prior_binding( + self, + ) -> None: + for original_identity in ((1, 2), (1, 99)): + with ( + self.subTest(original_identity=original_identity), + directory_scope() as scope, + ): + adapter = scope["adapter"] + account = POLICY.account_directory(scope["row"]) + previous = (account / "captures", original_identity, 1) + adapter.bindings[87] = previous + retained = {88: (1, 55, stat.S_IFDIR, 42)} + adapter.failed_open_bindings.update(retained) + with self.assertRaises(BOOT.BootstrapError): + scope["module"]._sync_directory(account) + adapter.raw_fsync.assert_not_called() + adapter.raw_close.assert_not_called() + self.assertEqual({87: previous}, adapter.bindings) + self.assertIs(adapter.bindings[87], previous) + self.assertEqual(retained, adapter.failed_open_bindings) + self.assertIsNone(adapter.pending) + self.assertEqual(1, len(adapter.failures)) + self.assertEqual(87, adapter.failures[0]["fd"]) + self.assertFalse(adapter.failures[0]["closed"]) + self.assertIn("bound_conflict", adapter.failures[0]["reason"]) + + def test_directory_sync_bound_conflict_retains_bounded_failure_even_without_observed_identity( + self, + ) -> None: + with directory_scope() as scope: + adapter = scope["adapter"] + previous = (POLICY.account_directory(scope["row"]), (1, 2), 1) + adapter.bindings[87] = previous + for observed in (None, scope["fd_info"]): + adapter.close_failed_open( + 87, observed, BOOT.BootstrapError("Synthetic conflict") + ) + adapter.raw_close.assert_not_called() + self.assertEqual({87: previous}, adapter.bindings) + self.assertFalse(adapter.failed_open_bindings) + self.assertEqual(2, len(adapter.failures)) + self.assertTrue( + all( + not row["closed"] and "bound_conflict" in row["reason"] + for row in adapter.failures + ) + ) + adapter.failures[:] = [ + { + "fd": 87, + "reason": "Retained synthetic bound conflict", + "closed": False, + } + for _ in range(BOOT.LIMITS["events"]) + ] + original = copy.deepcopy(adapter.failures) + with self.assertRaises(BOOT.BootstrapError): + adapter.close_failed_open( + 87, None, BOOT.BootstrapError("Over-limit conflict") + ) + self.assertEqual(original, adapter.failures) + self.assertEqual({87: previous}, adapter.bindings) + adapter.raw_close.assert_not_called() + + def test_directory_sync_exact_account_and_captures_parent_grants(self) -> None: + for kind in ("capture-a1", "capture-a2", "capture-revision"): + for child in (None, "captures"): + with ( + self.subTest(kind=kind, child=child), + directory_scope(kind) as scope, + ): + guard = scope["guard"] + adapter = scope["adapter"] + account = POLICY.account_directory(scope["row"]) + target = account if child is None else account / child + regular_intent = (Path(scope["row"]["source"]), os.O_RDONLY) + guard.open_intents[:] = [regular_intent] + scope["module"]._sync_directory(target) + adapter.raw_open.assert_called_once_with( + target, os.O_RDONLY | os.O_DIRECTORY, 0o777 + ) + adapter.raw_fsync.assert_called_once_with(87) + adapter.raw_close.assert_called_once_with(87) + self.assertEqual({}, adapter.bindings) + self.assertEqual({}, adapter.failed_open_bindings) + self.assertEqual([], adapter.failures) + self.assertEqual([regular_intent], guard.open_intents) + self.assertNotIn(target, guard.approved) + self.assertFalse(guard.readable(target)) + self.assertEqual(1, scope["protocol"].parent.sequence) + request = scope["protocol"].sent[0] + self.assertEqual(str(target), request["path"]) + self.assertEqual( + os.O_RDONLY | os.O_DIRECTORY, + request["flags"] & ~getattr(os, "O_CLOEXEC", 0), + ) + + def test_directory_sync_refuses_other_cases_and_paths_before_open(self) -> None: + for kind in ("negative-a3", "invalid-state", "parser-a5", "flock-a4"): + with self.subTest(kind=kind), directory_scope(kind) as scope: + with self.assertRaises(BOOT.BootstrapError): + scope["module"]._sync_directory( + POLICY.account_directory(scope["row"]) + ) + scope["adapter"].raw_open.assert_not_called() + scope["adapter"].raw_fsync.assert_not_called() + with directory_scope() as scope: + account = POLICY.account_directory(scope["row"]) + targets = ( + account / "receipts", + account / "logs", + account.parent, + account.parent / ("b" * 64), + POLICY.RUN_ROOT / "completion" / "foreign" / "captures", + POLICY.RUN_ROOT / "logs", + POLICY.RUN_ROOT.parent / "outside", + ) + for target in targets: + with ( + self.subTest(target=target), + self.assertRaises(BOOT.BootstrapError), + ): + scope["module"]._sync_directory(target) + scope["adapter"].raw_open.assert_not_called() + scope["adapter"].raw_fsync.assert_not_called() + + def test_directory_sync_strict_flags_owner_type_and_ungranted_reads(self) -> None: + with directory_scope() as scope: + target = POLICY.account_directory(scope["row"]) + for extra in ( + os.O_CREAT, + os.O_WRONLY, + os.O_RDWR, + os.O_TRUNC, + os.O_NOFOLLOW, + os.O_NONBLOCK, + ): + with self.subTest(extra=extra), self.assertRaises(BOOT.BootstrapError): + scope["adapter"].target(target, os.O_DIRECTORY | extra) + for flags in (True, os.O_RDONLY): + with self.assertRaises(BOOT.BootstrapError): + scope["adapter"].target(target, flags) + for field, value in ( + ("st_uid", 43), + ("st_mode", stat.S_IFDIR | 0o755), + ("st_mode", stat.S_IFREG | 0o600), + ): + previous = getattr(scope["target_info"], field) + setattr(scope["target_info"], field, value) + with ( + self.subTest(field=field, value=value), + self.assertRaises(BOOT.BootstrapError), + ): + scope["module"]._sync_directory(target) + setattr(scope["target_info"], field, previous) + with self.assertRaises(BOOT.BootstrapError): + scope["guard"]("open", (str(target), None, os.O_DIRECTORY)) + with self.assertRaises(BOOT.BootstrapError): + scope["adapter"].open(target, os.O_DIRECTORY) + scope["adapter"].raw_open.assert_not_called() + + def test_directory_sync_postopen_identity_failure_closes_matching_return( + self, + ) -> None: + for mode in ( + "path_swap", + "returned_identity", + "returned_type", + "returned_uid", + "access", + ): + with self.subTest(mode=mode), directory_scope() as scope: + if mode == "path_swap": + original = scope["adapter"].raw_open.side_effect + + def swap_path(path: object, flags: int, mode: int = 0o777) -> int: + fd = original(path, flags, mode) + scope["target_info"].st_ino = 3 + return fd + + scope["adapter"].raw_open.side_effect = swap_path + elif mode == "returned_identity": + scope["fd_info"].st_ino = 3 + elif mode == "returned_type": + scope["fd_info"].st_mode = stat.S_IFREG | 0o600 + elif mode == "returned_uid": + scope["fd_info"].st_uid = 43 + else: + scope["access"].return_value = os.O_RDWR + with self.assertRaises(BOOT.BootstrapError): + scope["module"]._sync_directory( + POLICY.account_directory(scope["row"]) + ) + scope["adapter"].raw_fsync.assert_not_called() + scope["adapter"].raw_close.assert_called_once_with(87) + self.assertFalse(scope["adapter"].bindings) + self.assertIsNone(scope["adapter"].pending) + self.assertTrue(scope["adapter"].failures[0]["closed"]) + + def test_directory_sync_reused_fd_before_fsync_refuses_foreign_close(self) -> None: + with directory_scope() as scope: + original = scope["fd_info"] + reused = types.SimpleNamespace( + st_dev=1, st_ino=99, st_uid=42, st_mode=stat.S_IFDIR | 0o700 + ) + scope["fstat"].side_effect = [original, original, reused, reused] + with self.assertRaises(BOOT.BootstrapError): + scope["module"]._sync_directory(POLICY.account_directory(scope["row"])) + scope["adapter"].raw_fsync.assert_not_called() + scope["adapter"].raw_close.assert_not_called() + self.assertIn(87, scope["adapter"].bindings) + self.assertFalse(scope["adapter"].failures[0]["closed"]) + + def test_directory_sync_failure_uses_original_finally_and_retains_close_failure( + self, + ) -> None: + for mode in ("fsync", "close", "postopen_close"): + with self.subTest(mode=mode), directory_scope() as scope: + if mode == "fsync": + scope["adapter"].raw_fsync.side_effect = OSError( + "Synthetic fsync failure" + ) + else: + scope["adapter"].raw_close.side_effect = OSError( + "Synthetic close failure" + ) + if mode == "postopen_close": + scope["fd_info"].st_ino = 3 + with self.assertRaises((OSError, BOOT.BootstrapError)): + scope["module"]._sync_directory( + POLICY.account_directory(scope["row"]) + ) + scope["adapter"].raw_close.assert_called_once_with(87) + if mode == "fsync": + self.assertFalse(scope["adapter"].bindings) + elif mode == "close": + self.assertIn(87, scope["adapter"].bindings) + else: + self.assertIn(87, scope["adapter"].failed_open_bindings) + + def test_directory_sync_windows_preservation_and_regular_fdopen_refusal( + self, + ) -> None: + with directory_scope() as scope: + scope["module"].os.name = "nt" + scope["module"]._sync_directory(POLICY.account_directory(scope["row"])) + scope["adapter"].raw_open.assert_not_called() + self.assertEqual([], scope["protocol"].sent) + scope["guard"].open_intents[:] = [ + (Path(scope["row"]["source"]), os.O_RDONLY) + ] + with self.assertRaises(BOOT.BootstrapError): + scope["guard"].descriptor(87, "rb") + self.assertFalse(scope["guard"].known_fds) + + def test_directory_sync_parent_exact_existing_directory_proof(self) -> None: + row = case() + account = POLICY.account_directory(row) + for target in (account, account / "captures"): + policy = POLICY.MutationPolicy(row) + request = mutation( + 1, + "open", + target, + os.O_RDONLY | os.O_DIRECTORY | getattr(os, "O_CLOEXEC", 0), + ) + grant = policy.decide(request) + with mock.patch.object(POLICY, "owned_path") as owned: + POLICY.mutation_target(grant, request) + self.assertEqual( + [ + mock.call(target.parent, directory=True), + mock.call(target, directory=True), + ], + owned.call_args_list, + ) + for kind in ("invalid-state", "negative-a3", "parser-a5", "flock-a4"): + policy = POLICY.MutationPolicy(case(kind)) + with self.assertRaises(POLICY.SafetyError): + policy.decide( + mutation(1, "open", policy.account, os.O_RDONLY | os.O_DIRECTORY) + ) + + def test_fixed_limits_and_proof_scope(self) -> None: + self.assertEqual(POLICY.LIMITS, BOOT.LIMITS) + self.assertEqual(POLICY.FREE_MIN - POLICY.LIMITS["rss_abort_bytes"], 201326592) + self.assertEqual(POLICY.PROOF_SCOPE, BOOT.PROOF_SCOPE) + self.assertEqual( + set(POLICY.SOURCE_FILES), {path for _, path in BOOT.SOURCE_MODULES} + ) + self.assertNotIn("installed", POLICY.PROOF_SCOPE) + + def test_exact_integer_and_hash(self) -> None: + for value in (True, False, 1.0, None, -1, 1 << 80): + with self.subTest(value=value), self.assertRaises(POLICY.SafetyError): + POLICY.integer(value) + for value in (True, "A" * 64, "a" * 63, "a" * 65, None): + with self.subTest(value=value), self.assertRaises(POLICY.SafetyError): + POLICY.hash_text(value) + self.assertEqual(POLICY.integer(0), 0) + self.assertEqual(POLICY.hash_text("a" * 64), "a" * 64) + + def test_sensor_admission_exact_floor_and_abi(self) -> None: + samples = [ + POLICY.Sensor(1_000_000_000 + n * 1_000_000_000, 1, POLICY.FREE_MIN, 100) + for n in range(3) + ] + POLICY.admission(samples, 3_500_000_000) + changes = [ + ("pressure", 0), + ("pressure", 2), + ("pressure", 3), + ("pressure", 4), + ("pressure", True), + ("free_bytes", POLICY.FREE_MIN - 1), + ("free_bytes", True), + ("swap_used_bytes", 101), + ("monotonic_ns", 1_000_000_000), + ] + for field, value in changes: + altered = [POLICY.asdict(row) for row in samples] + altered[-1][field] = value + with ( + self.subTest(field=field, value=value), + self.assertRaises(POLICY.SafetyError), + ): + POLICY.admission( + [POLICY.Sensor(**row) for row in altered], 3_500_000_000 + ) + for now in (2_999_999_999, 3_500_000_001): + with self.subTest(now=now), self.assertRaises(POLICY.SafetyError): + POLICY.admission(samples, now) + with self.assertRaises(POLICY.SafetyError): + POLICY.admission(samples[:2], 3_500_000_000) + with self.assertRaises(POLICY.SafetyError): + POLICY.admission( + [POLICY.Sensor(1 + n, 1, POLICY.FREE_MIN, 0) for n in range(3)], 3 + ) + + def test_manifest_and_case_fences(self) -> None: + self.assertEqual(POLICY.validate_manifest(manifest()), manifest()) + for kind in POLICY.CASES: + POLICY.validate_case(case(kind)) + changes = [ + ("version", True), + ("stage", "B"), + ("synthetic", False), + ("root_execution_go", False), + ("run_root", "/tmp"), + ("source_root", "/Users/j/dev/TrueMemory"), + ("sources", {}), + ("cases", []), + ("cases", [case(), case()]), + ] + for field, value in changes: + altered = manifest() + altered[field] = value + with self.subTest(field=field), self.assertRaises(POLICY.SafetyError): + POLICY.validate_manifest(altered) + for field, value in ( + ("id", []), + ("session", "unknown"), + ("database", "/tmp/personal.db"), + ("source", "/Users/j/dev/TrueMemory/transcript.jsonl"), + ("source_bytes", True), + ("source_identity", [1, 2, 23, 3, 4]), + ): + altered = case() + altered[field] = value + with self.subTest(field=field), self.assertRaises(POLICY.SafetyError): + POLICY.validate_case(altered) + altered = manifest() + altered["limits"]["wall_seconds"] = 6 + with self.assertRaises(POLICY.SafetyError): + POLICY.validate_manifest(altered) + altered = manifest() + altered["limits"]["core_bytes"] = False + with self.assertRaises(POLICY.SafetyError): + POLICY.validate_manifest(altered) + + def test_paths_and_versioned_results(self) -> None: + for name in ("v" + str(number) for number in range(1, 100)): + self.assertEqual( + POLICY.evidence_paths(POLICY.RUN_ROOT / ("manifest-" + name + ".json")), + (POLICY.RUN_ROOT / ("results-" + name + ".json"), name), + ) + for text in ( + "relative", + str(POLICY.RUN_ROOT) + "/../outside", + str(POLICY.RUN_ROOT) + "//escape", + "/tmp/file", + ): + with self.subTest(text=text), self.assertRaises(POLICY.SafetyError): + POLICY.canonical_path(text, POLICY.RUN_ROOT) + for name in ("v0", "v01", "v100", "v3x", "v3\n", "v3", "v٣"): + with self.subTest(name=name), self.assertRaises(POLICY.SafetyError): + POLICY.evidence_paths(POLICY.RUN_ROOT / ("manifest-" + name + ".json")) + with self.assertRaises(POLICY.SafetyError): + POLICY.evidence_paths(POLICY.RUN_ROOT.parent / "manifest-v3.json") + + def test_identity_is_current_owned_incarnation(self) -> None: + row = POLICY.Identity(72887, 42, 12345, 72887) + self.assertTrue(POLICY.same_owned_identity(row, row, 42)) + for changed in ( + POLICY.Identity(72887, 42, 12346, 72887), + POLICY.Identity(72887, 43, 12345, 72887), + POLICY.Identity(72887, 42, 12345, 1), + POLICY.Identity(True, 42, 12345, 1), + ): + with self.subTest(changed=changed): + self.assertFalse(POLICY.same_owned_identity(row, changed, 42)) + + def test_ready_and_result_exact_protocol(self) -> None: + ready = { + "type": "ready", + "version": 1, + "pid": 100, + "audit_installed": True, + "limits": { + "cpu": [2, 3], + "core": [0, 0], + "nofile": [64, 64], + "fsize": [1048576, 1048576], + }, + } + POLICY.validate_ready(ready, 100) + for field, value in ( + ("pid", 101), + ("version", True), + ("audit_installed", 1), + ("limits", {}), + ): + altered = copy.deepcopy(ready) + altered[field] = value + with self.subTest(field=field), self.assertRaises(POLICY.SafetyError): + POLICY.validate_ready(altered, 100) + altered = copy.deepcopy(ready) + altered["limits"]["core"] = [False, False] + with self.assertRaises(POLICY.SafetyError): + POLICY.validate_ready(altered, 100) + done = { + "type": "result", + "version": 1, + "case": "capture-a1", + "passed": True, + "proof_scope": POLICY.PROOF_SCOPE, + "import_mode": "isolated_source_module", + "sources": manifest()["sources"], + "details": {}, + } + POLICY.validate_result(done, case(), manifest()["sources"]) + for field, value in ( + ("passed", 1), + ("version", True), + ("proof_scope", "installed_host"), + ("case", "capture-a2"), + ("sources", {}), + ("details", []), + ): + altered = dict(done) + altered[field] = value + with self.subTest(field=field), self.assertRaises(POLICY.SafetyError): + POLICY.validate_result(altered, case(), manifest()["sources"]) + + def test_mutation_positive_and_one_pending_slot(self) -> None: + policy = POLICY.MutationPolicy(case()) + policy.decide(mutation(1, "os.mkdir", policy.account, 0o700)) + lock = policy.account / "state.lock" + policy.decide(mutation(2, "open", lock, os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW)) + pending = policy.account / ("state-" + "a" * 32 + ".pending") + policy.decide( + mutation( + 3, "open", pending, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW + ) + ) + grant = policy.decide( + mutation(4, "os.rename", policy.account / "state.json", source=pending) + ) + self.assertEqual(grant["path"], str(policy.account / "state.json")) + with self.assertRaises(POLICY.SafetyError): + policy.decide( + mutation( + 5, + "open", + policy.account / ("state-" + "b" * 32 + ".pending"), + os.O_WRONLY | os.O_CREAT | os.O_EXCL, + ) + ) + + def test_mutation_negative_controls(self) -> None: + for event, filename, flags, source in ( + ("open", "state.json", os.O_WRONLY | os.O_CREAT, None), + ("open", "state.lock", os.O_RDWR | os.O_CREAT | os.O_TRUNC, None), + ("open", "unknown", os.O_WRONLY | os.O_CREAT | os.O_EXCL, None), + ("os.mkdir", "foreign", 0o700, None), + ("os.mkdir", "captures", 0o755, None), + ("os.rename", "state.json", 0, POLICY.RUN_ROOT / "arbitrary"), + ("unlink", "state.json", 0, None), + ): + policy = POLICY.MutationPolicy(case()) + with ( + self.subTest(event=event, filename=filename), + self.assertRaises(POLICY.SafetyError), + ): + policy.decide( + mutation(1, event, policy.account / filename, flags, source) + ) + for sequence in (0, 2, True, 33): + policy = POLICY.MutationPolicy(case()) + with self.subTest(sequence=sequence), self.assertRaises(POLICY.SafetyError): + policy.decide(mutation(sequence, "os.mkdir", policy.account, 0o700)) + policy = POLICY.MutationPolicy(case("negative-a3")) + with self.assertRaises(POLICY.SafetyError): + policy.decide(mutation(1, "os.mkdir", policy.account, 0o700)) + + def test_fd_wrappers_bind_latest_consumed_intent(self) -> None: + import fcntl + + row = case() + guard = BOOT.AuditGuard(FakeProtocol(), {"case": row}, POLICY) + path = Path(row["source"]) + info = types.SimpleNamespace( + st_dev=1, st_ino=2, st_uid=42, st_mode=stat.S_IFREG | 0o600 + ) + with ( + mock.patch.object(BOOT.os, "fstat", return_value=info), + mock.patch.object(BOOT.os, "getuid", return_value=42), + mock.patch.object(fcntl, "fcntl", return_value=os.O_RDONLY), + mock.patch.object(Path, "lstat", return_value=info), + mock.patch.object(Path, "is_symlink", return_value=False), + ): + guard.open_intents[:] = [(path, os.O_RDONLY)] + guard.descriptor(7, "r") + self.assertEqual(guard.known_fds, {7: (1, 2)}) + with self.assertRaises(BOOT.BootstrapError): + guard.descriptor(7, "rb") + info.st_ino = 3 + guard.open_intents[:] = [(path, os.O_RDONLY)] + guard.descriptor(7, "rb") + self.assertEqual(guard.known_fds, {7: (1, 3)}) + guard.open_intents[:] = [(path, os.O_WRONLY)] + with self.assertRaises(BOOT.BootstrapError): + guard.descriptor(7, "rb") + self.assertFalse(guard.open_intents) + for fd, mode in ((0, "r"), (7, "a"), (7, "r+"), (True, "r")): + with ( + self.subTest(fd=fd, mode=mode), + self.assertRaises(BOOT.BootstrapError), + ): + guard.descriptor(fd, mode) + + def test_audit_preopen_grant_and_denials(self) -> None: + protocol = FakeProtocol() + guard = BOOT.AuditGuard(protocol, {"case": case()}, POLICY) + path = POLICY.account_directory(case()) / "state.lock" + protocol.answers.append( + { + "type": "grant", + "version": 1, + "seq": 1, + "path": str(path), + "purpose": "Owned fixed lock", + } + ) + guard.path = lambda value: Path(value) + guard("open", (str(path), None, os.O_RDWR | os.O_CREAT)) + self.assertEqual(len(protocol.sent), 1) + self.assertEqual(guard.open_intents, [(path, os.O_RDWR)]) + guard("truememory.native.audit_probe", ("fixed-stage-a-v1",)) + self.assertTrue(guard.activated) + for event, args in ( + ("socket.connect", ()), + ("subprocess.Popen", ()), + ("os.remove", (str(path), -1)), + ("sqlite3.connect", ("memory",)), + ("resource.setrlimit", (0, (1, 1))), + ("import", ("truememory.engine", None)), + ("import", ("torch", None)), + ("os.mkdir", (str(path), 0o700, 1)), + ("os.rename", (str(path), str(path), 1, -1)), + ): + with self.subTest(event=event), self.assertRaises(BOOT.BootstrapError): + guard(event, args) + + def test_prelaunch_refusals_retain_nonexecuted_result(self) -> None: + fake = types.ModuleType("fake_psutil") + fake.Error = FakePsutilError + with ( + mock.patch.object( + POLICY.selectors, "DefaultSelector", return_value=FakeSelector() + ), + mock.patch.object( + POLICY, "sample", side_effect=POLICY.SafetyError("pressure_or_headroom") + ), + mock.patch.object(POLICY.subprocess, "Popen") as launch, + ): + result = POLICY.execute_case(manifest(), case(), fake, None) + self.assertFalse(result["executed"]) + self.assertFalse(result["passed"]) + self.assertTrue(result["reaped"]) + self.assertIn("pressure_or_headroom", result["blocked"]) + launch.assert_not_called() + samples = [ + POLICY.Sensor(1_000_000_000 + n * 1_000_000_000, 1, POLICY.FREE_MIN, 100) + for n in range(3) + ] + with ( + mock.patch.object( + POLICY.selectors, "DefaultSelector", return_value=FakeSelector() + ), + mock.patch.object(POLICY, "sample", side_effect=samples), + mock.patch.object(POLICY.time, "sleep"), + mock.patch.object(POLICY.time, "monotonic_ns", return_value=3_000_000_001), + mock.patch.object( + POLICY, "owned_path", side_effect=POLICY.SafetyError("fixture_changed") + ), + mock.patch.object(POLICY.subprocess, "Popen") as launch, + ): + result = POLICY.execute_case(manifest(), case(), fake, None) + self.assertFalse(result["executed"]) + self.assertFalse(result["passed"]) + self.assertIn("fixture_changed", result["blocked"]) + self.assertEqual(len(result["sensors"]), 3) + launch.assert_not_called() + + def test_pipe_deadline_and_partial_writes_are_pure(self) -> None: + with ( + mock.patch.object(POLICY.time, "monotonic", return_value=1), + mock.patch.object(POLICY.os, "write", side_effect=[2, 3]) as writer, + ): + POLICY.write_pipe(7, b"12345", 1.1) + self.assertEqual(writer.call_count, 2) + with ( + mock.patch.object(POLICY.time, "monotonic", return_value=1), + mock.patch.object(POLICY.os, "write", side_effect=[BlockingIOError(), 3]), + mock.patch.object( + POLICY.select, "select", return_value=([], [7], []) + ) as poll, + ): + POLICY.write_pipe(7, b"123", 1.1) + self.assertEqual(poll.call_count, 1) + with ( + mock.patch.object(POLICY.time, "monotonic", return_value=1), + mock.patch.object(POLICY.os, "write") as writer, + ): + with self.assertRaises(POLICY.SafetyError): + POLICY.write_pipe(7, b"123", 0.9) + writer.assert_not_called() + + def test_owned_popen_fallback_and_group_probe_are_pure(self) -> None: + fake = types.ModuleType("fake_psutil") + fake.NoSuchProcess = FakePsutilError + child = mock.Mock() + child.poll.return_value = None + child.wait.return_value = 0 + child.returncode = 0 + result = { + "reaped": False, + "descendants": [], + "descendant_containment": "not_observed", + } + with mock.patch.object(POLICY.time, "monotonic", return_value=1): + POLICY.terminate_owned(child, None, [], fake, result) + child.send_signal.assert_called_once_with(POLICY.signal.SIGTERM) + self.assertTrue(result["reaped"]) + self.assertEqual(result["termination_authority"], "unreaped_direct_popen_child") + identity = POLICY.Identity(100, 42, 123, 100) + changed = POLICY.Identity(100, 42, 124, 100) + child = mock.Mock() + child.poll.return_value = None + result = { + "reaped": False, + "descendants": [], + "descendant_containment": "not_observed", + } + with ( + mock.patch.object(POLICY.time, "monotonic", return_value=1), + mock.patch.object(POLICY, "observe_identity", return_value=changed), + mock.patch.object(POLICY.os, "getuid", return_value=42), + mock.patch.object(POLICY.os, "killpg") as signal_group, + ): + with self.assertRaises(POLICY.SafetyError): + POLICY.terminate_owned(child, identity, [], fake, result) + child.send_signal.assert_not_called() + signal_group.assert_not_called() + with mock.patch.object(POLICY.os, "killpg", side_effect=ProcessLookupError()): + self.assertTrue(POLICY.group_gone(100)) + with mock.patch.object(POLICY.os, "killpg", return_value=None): + self.assertFalse(POLICY.group_gone(100)) + with mock.patch.object(POLICY.os, "killpg", side_effect=PermissionError()): + self.assertFalse(POLICY.group_gone(100)) + member = POLICY.Identity(101, 42, 123, 100) + self.assertTrue(POLICY.member_identity_matches(member, member, 100, 42)) + self.assertFalse(POLICY.member_identity_matches(member, member, 101, 42)) + + def test_protocol_bounds_use_no_real_stdio(self) -> None: + protocol = BOOT.Protocol() + with mock.patch.object( + BOOT.os, "write", side_effect=lambda fd, data: len(data) + ) as writer: + protocol.emit({"type": "synthetic"}) + self.assertEqual(writer.call_count, 1) + with self.assertRaises(BOOT.BootstrapError): + protocol.emit({"payload": "x" * 2048}) + protocol.written_bytes = BOOT.LIMITS["output_bytes"] + with self.assertRaises(BOOT.BootstrapError): + protocol.emit({"type": "synthetic"}) + with mock.patch.object(BOOT.select, "select", return_value=([], [], [])): + with self.assertRaises(BOOT.BootstrapError): + BOOT.Protocol().receive(2048, 0.1) + protocol = BOOT.Protocol() + with ( + mock.patch.object(BOOT.select, "select", return_value=([0], [], [])), + mock.patch.object(BOOT.os, "read", return_value=b"x" * 2049), + ): + with self.assertRaises(BOOT.BootstrapError): + protocol.receive(2048, 0.1) + + def test_attempt_versions_are_canonical_before_io(self) -> None: + for number in range(1, 100): + self.assertEqual( + POLICY.validate_attempt_version("v" + str(number)), "v" + str(number) + ) + fake = types.ModuleType("fake_psutil") + fake.Error = FakePsutilError + invalid = ( + True, + False, + None, + 3, + b"v3", + [], + "v0", + "v00", + "v01", + "v100", + "V3", + "v3\n", + "v3", + "v٣", + "../v3", + "v3.log", + ) + for version in invalid: + with ( + self.subTest(version=version), + mock.patch.object(POLICY.selectors, "DefaultSelector") as selector, + mock.patch.object(POLICY, "sample") as sensor, + mock.patch.object(POLICY, "new_log") as writer, + mock.patch.object(POLICY.subprocess, "Popen") as launch, + ): + with self.assertRaises(POLICY.SafetyError): + POLICY.validate_attempt_version(version) + with self.assertRaises(POLICY.SafetyError): + POLICY.execute_case( + manifest(), case(), fake, None, attempt_version=version + ) + selector.assert_not_called() + sensor.assert_not_called() + writer.assert_not_called() + launch.assert_not_called() + + def test_actual_retry_logs_and_legacy_default(self) -> None: + fake = types.ModuleType("fake_psutil") + fake.Error = FakePsutilError + info = types.SimpleNamespace( + st_dev=1, st_ino=2, st_size=24, st_mtime_ns=3, st_ctime_ns=4 + ) + samples = [ + POLICY.Sensor(1_000_000_000 + n * 1_000_000_000, 1, POLICY.FREE_MIN, 100) + for n in range(3) + ] + fixture = case() + fixture_bytes = b"x" * 24 + fixture["source_sha256"] = POLICY.digest(fixture_bytes) + plan = manifest() + source_bytes = { + POLICY.SOURCE_ROOT / name: ("synthetic:" + name).encode("utf-8") + for name in POLICY.SOURCE_FILES + } + plan["sources"] = { + name: POLICY.digest(source_bytes[POLICY.SOURCE_ROOT / name]) + for name in POLICY.SOURCE_FILES + } + bootstrap_path = POLICY.SOURCE_ROOT / "tests/native/mac-native-bootstrap-v7.py" + bootstrap_bytes = b"fixed-bootstrap-v2" + plan["bootstrap_sha256"] = POLICY.digest(bootstrap_bytes) + + def read_fixture(path: Path, maximum: int, *, private: bool = True) -> bytes: + if path == Path(fixture["source"]): + self.assertEqual((maximum, private), (4096, True)) + return fixture_bytes + self.assertEqual((maximum, private), (262144, False)) + if path == bootstrap_path: + return bootstrap_bytes + if path in source_bytes: + return source_bytes[path] + raise AssertionError("Unplanned synthetic source read") + + for version in (None, "v3", "v99"): + expected = version if version is not None else "v1" + with self.subTest(version=version), ExitStack() as patches: + patches.enter_context( + mock.patch.object( + POLICY.selectors, "DefaultSelector", return_value=FakeSelector() + ) + ) + patches.enter_context( + mock.patch.object(POLICY, "sample", side_effect=samples) + ) + patches.enter_context(mock.patch.object(POLICY.time, "sleep")) + patches.enter_context( + mock.patch.object( + POLICY.time, "monotonic_ns", return_value=3_000_000_001 + ) + ) + patches.enter_context( + mock.patch.object(POLICY, "owned_path", return_value=info) + ) + reader = patches.enter_context( + mock.patch.object(POLICY, "read_bounded", side_effect=read_fixture) + ) + writer = patches.enter_context( + mock.patch.object( + POLICY, + "new_log", + side_effect=[7, 8, POLICY.SafetyError("stop_before_launch")], + ) + ) + patches.enter_context(mock.patch.object(POLICY.os, "fsync")) + patches.enter_context(mock.patch.object(POLICY.os, "close")) + launch = patches.enter_context( + mock.patch.object(POLICY.subprocess, "Popen") + ) + result = ( + POLICY.execute_case(plan, fixture, fake, None) + if version is None + else POLICY.execute_case( + plan, fixture, fake, None, attempt_version=version + ) + ) + self.assertEqual( + [call.args[0] for call in writer.call_args_list], + [ + POLICY.RUN_ROOT + / "logs" + / ("capture-a1-" + channel + "-" + expected + ".log") + for channel in ("stdout", "stderr", "events") + ], + ) + self.assertEqual( + reader.call_args_list[-1].args[0], + POLICY.SOURCE_ROOT / "tests/native/mac-native-bootstrap-v7.py", + ) + self.assertFalse(result["executed"]) + self.assertFalse(result["passed"]) + self.assertTrue(result["reaped"]) + self.assertIn("stop_before_launch", result["blocked"]) + launch.assert_not_called() + + def test_main_reserves_result_before_runtime_and_case(self) -> None: + with main_scope("v3") as calls: + + def runtime() -> types.ModuleType: + self.assertEqual(calls["new_log"].call_count, 1) + calls["execute_case"].assert_not_called() + fake = types.ModuleType("fake_psutil") + fake.Error = FakePsutilError + return fake + + calls["pinned_runtime"].side_effect = runtime + self.assertEqual(POLICY.main(), 1) + self.assertEqual( + calls["new_log"].call_args.args[0], POLICY.RUN_ROOT / "results-v3.json" + ) + calls["new_log"].assert_called_once() + self.assertEqual( + calls["execute_case"].call_args.kwargs, {"attempt_version": "v3"} + ) + payload = json.loads(calls["write_all"].call_args.args[1]) + self.assertEqual(payload["manifest_version"], "v3") + self.assertFalse(payload["complete"]) + self.assertFalse(payload["cases"][0]["executed"]) + calls["fsync"].assert_called_once_with(7) + calls["close"].assert_called_once_with(7) + + def test_main_existing_result_refuses_without_runtime(self) -> None: + with main_scope("v3") as calls: + calls["new_log"].side_effect = FileExistsError("retained_attempt") + with self.assertRaises(FileExistsError): + POLICY.main() + for name in ( + "pinned_runtime", + "PressureReader", + "execute_case", + "write_all", + "fsync", + "close", + ): + calls[name].assert_not_called() + + def test_main_runtime_refusal_retains_reserved_result(self) -> None: + with main_scope("v99") as calls: + calls["pinned_runtime"].side_effect = POLICY.SafetyError("runtime_refusal") + self.assertEqual(POLICY.main(), 1) + payload = json.loads(calls["write_all"].call_args.args[1]) + self.assertEqual(payload["manifest_version"], "v99") + self.assertEqual(payload["cases"], []) + self.assertIn("runtime_refusal", payload["setup_failure"]) + self.assertFalse(payload["complete"]) + calls["execute_case"].assert_not_called() + calls["fsync"].assert_called_once_with(7) + calls["close"].assert_called_once_with(7) + + def test_main_fatal_retains_incomplete_reserved_result(self) -> None: + with main_scope("v3") as calls: + calls["pinned_runtime"].side_effect = RuntimeError("fatal_inert_fixture") + with self.assertRaisesRegex(RuntimeError, "fatal_inert_fixture"): + POLICY.main() + calls["new_log"].assert_called_once() + calls["write_all"].assert_not_called() + calls["execute_case"].assert_not_called() + calls["fsync"].assert_called_once_with(7) + calls["close"].assert_called_once_with(7) + + def test_main_revision_only_reserved_for_v2(self) -> None: + revision = manifest() + revised = case("capture-revision") + revised["previous"]["key"] = "d" * 64 + revision["cases"] = [revised] + for version in ("v1", "v3", "v99"): + with self.subTest(version=version), main_scope(version, revision) as calls: + with self.assertRaisesRegex( + POLICY.SafetyError, "unexpected_manifest_case_stage" + ): + POLICY.main() + calls["new_log"].assert_not_called() + calls["pinned_runtime"].assert_not_called() + with main_scope("v2") as calls: + with self.assertRaisesRegex( + POLICY.SafetyError, "unexpected_manifest_case_stage" + ): + POLICY.main() + calls["new_log"].assert_not_called() + with main_scope("v2", revision) as calls: + calls["pinned_runtime"].side_effect = POLICY.SafetyError( + "controlled_stage_stop" + ) + self.assertEqual(POLICY.main(), 1) + self.assertEqual( + calls["new_log"].call_args.args[0], POLICY.RUN_ROOT / "results-v2.json" + ) + + def test_bootstrap_uses_actual_v2_policy(self) -> None: + guard = BOOT.AuditGuard(FakeProtocol(), {"case": case()}, POLICY) + self.assertIn( + POLICY.SOURCE_ROOT / "tests/native/mac-native-acceptance-v7.py", + guard.read_paths, + ) + self.assertNotIn( + POLICY.SOURCE_ROOT / "tests/native/mac-native-acceptance-v1.py", + guard.read_paths, + ) + tree = ast.parse(Path(BOOT.__file__).read_bytes()) + calls = [ + node + for node in ast.walk(tree) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Name) + and node.func.id == "compile_module" + ] + policy = [ + node + for node in calls + if node.args + and isinstance(node.args[0], ast.Constant) + and node.args[0].value == "mac_native_policy_v7" + ] + self.assertEqual(len(policy), 1) + self.assertIsInstance(policy[0].args[1], ast.BinOp) + self.assertEqual( + policy[0].args[1].right.value, "tests/native/mac-native-acceptance-v7.py" + ) + + +class DispatchPath: + def __init__(self, info: types.SimpleNamespace) -> None: + self.info = info + self.symlink = False + self.lookups = 0 + + def lstat(self) -> types.SimpleNamespace: + self.lookups += 1 + return self.info + + def is_symlink(self) -> bool: + return self.symlink + + +@contextmanager +def descriptor_dispatch_scope( + *, catalog: bool = True, intent: int = os.O_RDONLY, access: int | None = None +) -> Iterator[dict[str, object]]: + import fcntl + + info = types.SimpleNamespace( + st_dev=1, st_ino=2, st_uid=42, st_mode=stat.S_IFREG | 0o600 + ) + path_info = types.SimpleNamespace( + st_dev=1, st_ino=2, st_uid=42, st_mode=stat.S_IFREG | 0o600 + ) + path = DispatchPath(path_info) + guard = BOOT.AuditGuard(FakeProtocol(), {"case": case()}, POLICY) + reader = types.SimpleNamespace( + capture_path=mock.Mock(return_value={} if catalog else None), + descriptor=mock.Mock(), + ) + guard.capture_reads = reader + guard.open_intents[:] = [(path, intent)] + with ExitStack() as patches: + fstat = patches.enter_context( + mock.patch.object(BOOT.os, "fstat", return_value=info) + ) + patches.enter_context(mock.patch.object(BOOT.os, "getuid", return_value=42)) + flags = patches.enter_context( + mock.patch.object( + fcntl, "fcntl", return_value=intent if access is None else access + ) + ) + yield { + "guard": guard, + "reader": reader, + "path": path, + "info": info, + "path_info": path_info, + "fstat": fstat, + "flags": flags, + } + + +class DescriptorDispatchTests(unittest.TestCase): + def test_catalog_writers_use_complete_generic_validation(self) -> None: + for mode in ("w", "wb"): + with ( + self.subTest(mode=mode), + descriptor_dispatch_scope(intent=os.O_WRONLY) as state, + ): + guard, reader = state["guard"], state["reader"] + guard.descriptor(87, mode) + reader.descriptor.assert_not_called() + state["fstat"].assert_called_once_with(87) + self.assertEqual(state["flags"].call_count, 1) + self.assertEqual(state["path"].lookups, 1) + self.assertEqual(guard.known_fds, {87: (1, 2)}) + self.assertEqual(guard.open_intents, []) + + def test_catalog_readers_delegate_then_complete_generic_validation(self) -> None: + for mode in ("r", "rb"): + with self.subTest(mode=mode), descriptor_dispatch_scope() as state: + guard, reader = state["guard"], state["reader"] + guard.descriptor(87, mode) + reader.descriptor.assert_called_once_with(87, mode) + state["fstat"].assert_called_once_with(87) + self.assertEqual(state["flags"].call_count, 1) + self.assertEqual(state["path"].lookups, 1) + self.assertEqual(guard.known_fds, {87: (1, 2)}) + self.assertEqual(guard.open_intents, []) + + def test_plain_and_optional_reader_routes_are_preserved(self) -> None: + for intent, mode in ( + (os.O_RDONLY, "r"), + (os.O_RDONLY, "rb"), + (os.O_WRONLY, "w"), + (os.O_WRONLY, "wb"), + ): + for optional_none in (False, True): + with ( + self.subTest(intent=intent, mode=mode, optional_none=optional_none), + descriptor_dispatch_scope(catalog=False, intent=intent) as state, + ): + guard, reader = state["guard"], state["reader"] + if optional_none: + guard.capture_reads = None + guard.descriptor(87, mode) + reader.descriptor.assert_not_called() + self.assertEqual(guard.known_fds, {87: (1, 2)}) + self.assertEqual(guard.open_intents, []) + + def test_catalog_writer_identity_and_access_failures_never_register(self) -> None: + for failure in ( + "fd_uid", + "fd_directory", + "path_uid", + "path_directory", + "inode", + "device", + "symlink", + "access", + ): + with ( + self.subTest(failure=failure), + descriptor_dispatch_scope(intent=os.O_WRONLY) as state, + ): + guard, reader = state["guard"], state["reader"] + if failure == "fd_uid": + state["info"].st_uid = 43 + elif failure == "fd_directory": + state["info"].st_mode = stat.S_IFDIR | 0o700 + elif failure == "path_uid": + state["path_info"].st_uid = 43 + elif failure == "path_directory": + state["path_info"].st_mode = stat.S_IFDIR | 0o700 + elif failure == "inode": + state["path_info"].st_ino = 3 + elif failure == "device": + state["path_info"].st_dev = 3 + elif failure == "symlink": + state["path"].symlink = True + else: + state["flags"].return_value = os.O_RDONLY + with self.assertRaises(BOOT.BootstrapError): + guard.descriptor(87, "wb") + reader.descriptor.assert_not_called() + self.assertEqual(guard.known_fds, {}) + + def test_catalog_writer_invalid_wrappers_and_fstat_errors_refuse(self) -> None: + class ForeignMode(str): + pass + + for fd, mode in ( + (0, "w"), + (1, "wb"), + (2, "w"), + (True, "w"), + (87.0, "w"), + (87, None), + (87, b"wb"), + (87, ForeignMode("wb")), + (87, "a"), + (87, "r+"), + (87, ""), + ): + with ( + self.subTest(fd=fd, mode=mode), + descriptor_dispatch_scope(intent=os.O_WRONLY) as state, + ): + with self.assertRaisesRegex( + BOOT.BootstrapError, "unsupported_descriptor_wrapper" + ): + state["guard"].descriptor(fd, mode) + state["reader"].descriptor.assert_not_called() + state["fstat"].assert_not_called() + self.assertEqual(state["guard"].known_fds, {}) + with descriptor_dispatch_scope(intent=os.O_WRONLY) as state: + state["fstat"].side_effect = OSError("fixed_descriptor_unavailable") + with self.assertRaisesRegex(OSError, "fixed_descriptor_unavailable"): + state["guard"].descriptor(87, "w") + state["reader"].descriptor.assert_not_called() + self.assertEqual(state["guard"].known_fds, {}) + + def test_catalog_read_refusal_precedes_generic_registration(self) -> None: + for reason in ("capture_read_wrapper_not_owned", "fixed_read_binding_refusal"): + with self.subTest(reason=reason), descriptor_dispatch_scope() as state: + state["reader"].descriptor.side_effect = BOOT.BootstrapError(reason) + with self.assertRaisesRegex(BOOT.BootstrapError, reason): + state["guard"].descriptor(87, "rb") + state["reader"].descriptor.assert_called_once_with(87, "rb") + state["fstat"].assert_not_called() + self.assertEqual(state["guard"].known_fds, {}) + self.assertEqual( + state["guard"].open_intents, [(state["path"], os.O_RDONLY)] + ) + + def test_catalog_dispatch_does_not_waive_generic_access_checks(self) -> None: + rows = ( + (os.O_RDONLY, os.O_WRONLY, "rb", True), + (os.O_RDONLY, os.O_RDONLY, "wb", True), + (os.O_WRONLY, os.O_RDONLY, "wb", False), + (os.O_WRONLY, os.O_WRONLY, "rb", False), + (os.O_RDWR, os.O_RDWR, "wb", False), + ) + for intent, access, mode, read_delegated in rows: + with ( + self.subTest(intent=intent, access=access, mode=mode), + descriptor_dispatch_scope(intent=intent, access=access) as state, + ): + with self.assertRaisesRegex( + BOOT.BootstrapError, "descriptor_access_mismatch" + ): + state["guard"].descriptor(87, mode) + self.assertEqual( + state["reader"].descriptor.call_count, int(read_delegated) + ) + self.assertEqual(state["guard"].known_fds, {}) + self.assertEqual(state["guard"].open_intents, []) + + def test_catalog_inventory_32_passes_and_33_refuses(self) -> None: + for intent, mode in ((os.O_RDONLY, "rb"), (os.O_WRONLY, "wb")): + for existing in (31, 32): + with ( + self.subTest(intent=intent, existing=existing), + descriptor_dispatch_scope(intent=intent) as state, + ): + guard = state["guard"] + guard.known_fds = { + 300 + number: (1, 100 + number) for number in range(existing) + } + before = dict(guard.known_fds) + if existing == 31: + guard.descriptor(87, mode) + self.assertEqual(len(guard.known_fds), 32) + self.assertEqual(guard.known_fds[87], (1, 2)) + else: + with self.assertRaisesRegex( + BOOT.BootstrapError, "descriptor_inventory_limit" + ): + guard.descriptor(87, mode) + self.assertEqual(guard.known_fds, before) + self.assertEqual( + state["reader"].descriptor.call_count, + int(intent == os.O_RDONLY), + ) + self.assertEqual(guard.open_intents, []) + + def test_catalog_missing_intent_has_no_read_authority(self) -> None: + with descriptor_dispatch_scope() as state: + state["guard"].open_intents.clear() + with self.assertRaisesRegex( + BOOT.BootstrapError, "descriptor_not_preapproved" + ): + state["guard"].descriptor(87, "rb") + state["reader"].descriptor.assert_not_called() + self.assertEqual(state["guard"].known_fds, {}) + + def test_catalog_dispatch_uses_only_latest_intent_access(self) -> None: + with descriptor_dispatch_scope(intent=os.O_WRONLY) as state: + guard = state["guard"] + guard.open_intents.insert(0, (state["path"], os.O_RDONLY)) + guard.descriptor(87, "wb") + state["reader"].descriptor.assert_not_called() + self.assertEqual(guard.open_intents, [(state["path"], os.O_RDONLY)]) + self.assertEqual(guard.known_fds, {87: (1, 2)}) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-mac-native-package-acceptance-v3.py b/tests/test-mac-native-package-acceptance-v3.py new file mode 100644 index 00000000..e4a6e6d0 --- /dev/null +++ b/tests/test-mac-native-package-acceptance-v3.py @@ -0,0 +1,859 @@ +"""Inert Stage B contracts; no child, model, socket or native limit execution.""" + +from __future__ import annotations + +import ast +import copy +import hashlib +import importlib.util +import os +import stat +import sys +import unittest +from pathlib import Path +from types import ModuleType, SimpleNamespace +from unittest.mock import Mock, patch + +PARENT = Path(__file__).parent / "native/mac-native-package-acceptance-v3.py" +BOOTSTRAP = Path(__file__).parent / "native/mac-native-package-bootstrap-v3.py" +PACKAGE_RAW = b'{\n "A_runtime_packet_path": ".work/reports/mac-native-runtime-pins-v1.json",\n "A_runtime_packet_sha256": "8db57e3f700aebb3adeb383c719a4bc5e2525917f4c017c853ee4694be1ee1e4",\n "OS": {\n "kernel_release": "25.6.0",\n "kernel_version": "Darwin Kernel Version 25.6.0: Tue Aug 18 17:49:54 PDT 2026; root:xnu-12377.161.15.700.19~2/RELEASE_ARM64_T8142",\n "machine": "arm64"\n },\n "all_publication_held": true,\n "created_utc": "2026-10-12T09:07:42.262934+00:00",\n "current_wheel_artifact_packet_path": ".work/reports/memory-installed-wheel-artifact-v2.json",\n "current_wheel_artifact_packet_sha256": "4b82143e4adaa5e26d81bbdfb32fc725d171bf0e466fe7ee0bfe14c1bc0c062f",\n "data": {\n "truememory/data/l3_weights.json": "5d91643f843b4639375a4324aa87cbd09815a4242b11f3fe4138bae641b2d5b8"\n },\n "data_map_sha256": "19d20af55c695fe0952cfc26e4fd339037ac3bbfe05fab4b14bb1ae814ca4ea2",\n "interpreter_linkage_packet_path": ".work/reports/mac-native-interpreter-linkage-v1.json",\n "interpreter_linkage_packet_sha256": "762d535cdb9f021729fe101c654966e52a9d8da285d5fe77bc0b1db815d43843",\n "interpreter_runtime_packet_path": ".work/reports/mac-native-interpreter-runtime-v1.json",\n "interpreter_runtime_packet_sha256": "73a77a325ba667ff84d395c8ecddd9d21d63be27914c4586c6d5e65e2ca55596",\n "live_service_queried": false,\n "metadata_identity_mode": "Actual current Linux py3-none-any wheel metadata staged privately; not installed Mac package",\n "model_loads": 0,\n "modules_imported": false,\n "native_GO": false,\n "numpy_MachO_dependency_names": {\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_multiarray_tests.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_multiarray_umath.cpython-312-darwin.so": [\n "/System/Library/Frameworks/Accelerate.framework/Versions/A/Accelerate",\n "/usr/lib/libc++.1.dylib",\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_operand_flag_tests.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_rational_tests.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_simd.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_struct_ufunc_tests.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_umath_tests.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/fft/_pocketfft_umath.cpython-312-darwin.so": [\n "/usr/lib/libc++.1.dylib",\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/linalg/_umath_linalg.cpython-312-darwin.so": [\n "/System/Library/Frameworks/Accelerate.framework/Versions/A/Accelerate",\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/linalg/lapack_lite.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib",\n "/System/Library/Frameworks/Accelerate.framework/Versions/A/Accelerate"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_bounded_integers.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_common.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_generator.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_mt19937.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_pcg64.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_philox.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_sfc64.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/bit_generator.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/mtrand.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ]\n },\n "numpy_dependency_root": "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy",\n "numpy_files": {\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/__config__.py": {\n "bytes": 4867,\n "sha256": "062c14b924ddd2c1297088a9a9623607a90ab73c8bc56dd6087ca4bde928e739"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/__init__.py": {\n "bytes": 26277,\n "sha256": "92a46f791e453926d3292af2582b89995a289475f0eaaea71a949823200b838a"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_array_api_info.py": {\n "bytes": 10385,\n "sha256": "ce6ec789062aca14aa165dc9586ece2e1130f891843eaefeefea968575aa559f"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_configtool.py": {\n "bytes": 1007,\n "sha256": "105449de96b34f162113da29fa8716c8a4cb66bae91617e6992fed5ababc0b1a"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/__init__.py": {\n "bytes": 6662,\n "sha256": "837ce8aec8693095d2e1c7c306d385d1785a50be97a009935c64cae04e3555d2"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_add_newdocs.py": {\n "bytes": 216086,\n "sha256": "bdb68c6da2f3a1048cb2d6123afb6ede6f2bee88c88acb4d330c5485dc1324ce"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_add_newdocs_scalars.py": {\n "bytes": 12933,\n "sha256": "db6f2b889f9dcfd7d5df64ef3a430f532b3fdbfc279c3b94f8451b1757a16efb"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_asarray.py": {\n "bytes": 3894,\n "sha256": "f5aa779032cd51c8ca49039fae454fcebd2d64647513576d0884ff61e68525d2"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_dtype.py": {\n "bytes": 10547,\n "sha256": "70ce899e3a072d445608780df15990ca37a28838dcc210792ff7cf30e7b5bae3"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_dtype_ctypes.py": {\n "bytes": 3726,\n "sha256": "28f3e56a40ec3e4b938523abe6a1705b48c9f559e36ef5b811684e6c21d81881"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_exceptions.py": {\n "bytes": 5159,\n "sha256": "5fc120d61ab5b94f0bf7088ec05a368d1aba4b4be78c27606051e3de1016f42a"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_internal.py": {\n "bytes": 29437,\n "sha256": "c15b426aca730fccd5261139ee1a825abaf298d21d2522aa7f37338cd44598ee"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_methods.py": {\n "bytes": 9393,\n "sha256": "724facf7e63c5b8fcc2fac14bf31a02cb048d9e28f516d86502ba5077a425476"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_multiarray_tests.cpython-312-darwin.so": {\n "bytes": 120952,\n "sha256": "7eebe35f8c33291691312a8418d770da61837330588f966fbae8ad88099631eb"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_multiarray_umath.cpython-312-darwin.so": {\n "bytes": 3678408,\n "sha256": "b96123b811488473c23efa6d45f196641463c928372ff896785813b821aed652"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_operand_flag_tests.cpython-312-darwin.so": {\n "bytes": 51240,\n "sha256": "349c39c8494d2439e6732a0f8caf105a4737f21cbf1b962e1b07b36d853b58f6"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_rational_tests.cpython-312-darwin.so": {\n "bytes": 72808,\n "sha256": "0bfbea6b715e9c44c438b868b76e252178a478ecd094a1521d086165b55fdf22"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_simd.cpython-312-darwin.so": {\n "bytes": 339592,\n "sha256": "3178cd1f5ab8625fa8371bf48dbe54b4ed88896f9f70519c34ccaea970374197"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_string_helpers.py": {\n "bytes": 2845,\n "sha256": "e929a0a22ea80f60ae9e3c014abf41676d079029ef5ba9d33db953394de95a78"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_struct_ufunc_tests.cpython-312-darwin.so": {\n "bytes": 51480,\n "sha256": "9337744b95313d29845b741049a8cdaa8abe57520524e7f16c396b2ae64f1d32"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_type_aliases.py": {\n "bytes": 3755,\n "sha256": "a4d4a14f10ba6af2c1d912fbf8f38eb69150469e773bee5480e4f1962cf58296"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_ufunc_config.py": {\n "bytes": 15611,\n "sha256": "0e938bb63600619bfe9028e8f285f9ddcd925b62d0220690ec4a650a48ac45a5"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_umath_tests.cpython-312-darwin.so": {\n "bytes": 71712,\n "sha256": "ba6ac034d6e710660862a3260c4e8d62171de92a9b3ea32029bc17774ea7f84f"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/arrayprint.py": {\n "bytes": 65271,\n "sha256": "f7a7c9a96b764436f675560f2dda3990d0d042ae8aa898d2baa40e7f5918209b"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/cversions.py": {\n "bytes": 347,\n "sha256": "1ff88d229c7dfa1635710371aa34f677fe525d98496cca3f71aab8feae8b07b2"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/defchararray.py": {\n "bytes": 37817,\n "sha256": "30b125c1f98ae7bea0cf9f80fbb49558867dece970d72e675ff7f0c6c84d22f7"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/einsumfunc.py": {\n "bytes": 57930,\n "sha256": "b91287b000d50c68c3be44597f9d968d4e18a967c729143aa5efe012bf4f847d"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/fromnumeric.py": {\n "bytes": 142658,\n "sha256": "6055d8e195cf7aaae417d23bda203cd35014e3db3d7c28a32e7eb71e858e945f"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/function_base.py": {\n "bytes": 19677,\n "sha256": "b8b8f508410c567b0ea4b44db18e28eb09c62e9ec33680fcefdea7de82eeb9c1"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/getlimits.py": {\n "bytes": 15003,\n "sha256": "46d3015808f29584e67eab482f52542a02fb6f0c95c2ca1bd386dc42b9a9ed72"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/memmap.py": {\n "bytes": 12651,\n "sha256": "c88b10ea7f64a59ba58204492459136e35709c1e257a8ca2cef529736894e27f"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/multiarray.py": {\n "bytes": 56786,\n "sha256": "ae9b35e60d0150e2ade05d45c1a31dbfeee627dbe668b2b7a1bfc2e3fe53f77f"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/numeric.py": {\n "bytes": 83038,\n "sha256": "d75e4d3072e59b477cad20f088d964f6b41c840e7304d3f76fe9f6832d760a9f"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/numerictypes.py": {\n "bytes": 15967,\n "sha256": "d7924b057d26fcc4206a21ffc81048e6096fd2f5ee794d1aae74b9e915df5319"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/overrides.py": {\n "bytes": 7480,\n "sha256": "2bdc18ac0e766e23be366f0605d0f382ea9272d2500e8baf03821e91395fc555"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/printoptions.py": {\n "bytes": 1056,\n "sha256": "345a6fcb96e78dbbea9ca7aa42dd2f784131a400180153688865c7de98255807"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/records.py": {\n "bytes": 36754,\n "sha256": "fd40158fe835806fa832a449440e04f414f72331b733825fc58dd48e6255330d"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/shape_base.py": {\n "bytes": 32712,\n "sha256": "1d6e897dfdb7edf2d71e8ed75254a6e7106b1a947592f88d91af96d1ad2c469e"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/strings.py": {\n "bytes": 50578,\n "sha256": "a9c5e0f420035ed0f3a5f51bd95307c0a78f3389d764f84da46a554e35097065"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/umath.py": {\n "bytes": 2140,\n "sha256": "450a42560a838a2260e85f23c8398b7d09e7a8d8dd4170376e0d920463060c0b"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_distributor_init.py": {\n "bytes": 421,\n "sha256": "14148976054795071ae41ad011560fa059ba2924c98481675ad59b1241214d2a"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_expired_attrs_2_0.py": {\n "bytes": 3771,\n "sha256": "735b4637a636441b0324e9498dcff127231ea3a1d321d939c75048c6dfab2684"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_globals.py": {\n "bytes": 4155,\n "sha256": "d37b1eb1df7d7ae55fdc2bd9d757631344be0bacfe56d8456062b60b23e3722b"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_pyinstaller/__init__.py": {\n "bytes": 0,\n "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_pyinstaller/hook-numpy.py": {\n "bytes": 1362,\n "sha256": "314db6a50e0091460f416bb90bca510e99d85c494b27c4741463582544298951"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_pytesttester.py": {\n "bytes": 6328,\n "sha256": "0e39582fcb8834dd9759ade79e55fa80f1aea0b8dcc750627bf3d0cdba767290"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_typing/__init__.py": {\n "bytes": 5466,\n "sha256": "33034ac192a99a0eecf9babd14db781811274926d04a6923f92a71b66329a229"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_typing/_add_docstring.py": {\n "bytes": 3999,\n "sha256": "ff783b0fee87010dcc4f85fa0c4d3bc8bb9af4ba96161b24355c754d2ed7f4ee"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_typing/_array_like.py": {\n "bytes": 4188,\n "sha256": "10f6547c94a36a6bec589e91b39670c00ff916106961da227dc555b557163e7d"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_typing/_char_codes.py": {\n "bytes": 8764,\n "sha256": "8f4ee7a64f3635bec8adadb3ed94e553751c38fc2dda033ba992ab3cb7634f8f"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_typing/_dtype_like.py": {\n "bytes": 3907,\n "sha256": "1a299be0d4de9405780e7c09bceebd8aad17f7fbdd438071433dd0232632f4c1"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_typing/_extended_precision.py": {\n "bytes": 434,\n "sha256": "a649d4aa06a4d0504d33eb0444fa96fa91461fbd7f2be89e84549e7b9a108aa1"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_typing/_nbit.py": {\n "bytes": 632,\n "sha256": "2926cac0e2adb686fee72b53e6f095907ac3327d181efca9b534f28ffe8061cc"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_typing/_nbit_base.py": {\n "bytes": 3058,\n "sha256": "9cf669b1096db91e41d22680605f6a0f685efe45e79acb2ffd185a50d16c5beb"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_typing/_nested_sequence.py": {\n "bytes": 2639,\n "sha256": "c2678328b566bc49c1f5e61d77f982355584a1caa6068f0686929661018541ec"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_typing/_scalars.py": {\n "bytes": 944,\n "sha256": "2e15d8d814c799e60acde2197e98efb8c9fee5e38b8d4da7f73ef3d65e5b29ff"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_typing/_shape.py": {\n "bytes": 275,\n "sha256": "e9c16ff8b6d2c86f6695f4813861b2ba5f50fc652b95c1d00bd2596be9b6d1c0"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_typing/_ufunc.py": {\n "bytes": 156,\n "sha256": "1ce91a13eeb05747dddeb9876460b7f580072087fcb72be5cde903e32e064310"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_utils/__init__.py": {\n "bytes": 3477,\n "sha256": "8559d9ec2d0c09235b330f99caaf8c282612b5a197e91cea14c9e787b79de1d1"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_utils/_convertions.py": {\n "bytes": 329,\n "sha256": "d3133175e2cece20e61ec44cffc96e121e12fa441d328320e86c4d0c36acebd9"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_utils/_inspect.py": {\n "bytes": 7436,\n "sha256": "cc5b890011f4f03d4a82afde79c6240f53a0834397a75b78a2a8d9c4cd2474b9"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_utils/_pep440.py": {\n "bytes": 13988,\n "sha256": "8adf4fe3fa075d6c37071768573ec93ccba3e64c45e4ceff049f19d66f67bb4c"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/char/__init__.py": {\n "bytes": 93,\n "sha256": "c6cea9a6b31d98d79755fb934645379c5f6a761bad59d3eee6869ea760235a67"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/conftest.py": {\n "bytes": 8652,\n "sha256": "3193ef75f63f7ccd1bf9c303be34026ab6a3a3c8c4bab62f3647eb485abf1c1d"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/__init__.py": {\n "bytes": 1290,\n "sha256": "c0935a445d5414e9d92aa881aecb2158b8d31a2119f6bbd695c8add318fb634c"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/_dtype.py": {\n "bytes": 323,\n "sha256": "1870617d5b6c56b3fbbf5dc8ba3133f5a18810d91821d6dfb91bbe35ec355275"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/_dtype_ctypes.py": {\n "bytes": 351,\n "sha256": "c17e26dfb6f4cd08319734f93a313fba3d84e42a625fd13b1cb1693ba87f9436"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/_internal.py": {\n "bytes": 949,\n "sha256": "ab1a472442d735471826426dd4b92d42e666e01c18bb86d79ccb8110ea7a3ce5"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/_multiarray_umath.py": {\n "bytes": 2098,\n "sha256": "4fcf07660143e550ae5d109278b6cfa23f7d9ca512760cb0f315b27fa7aa8714"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/_utils.py": {\n "bytes": 923,\n "sha256": "e5f935f09378dd183a607bda9fa42376b39e3ae2ed4652d598fe8c69d0442550"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/arrayprint.py": {\n "bytes": 339,\n "sha256": "2db7b8b26597605cddf6c3bd2cb2793d94b80b76d2bcbb7a1d1b70484e7ac4df"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/defchararray.py": {\n "bytes": 347,\n "sha256": "6bd96ebef9e2f2046b19574a3bb53fc70b0516f9339719d58312fbe632d19822"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/einsumfunc.py": {\n "bytes": 339,\n "sha256": "08db9c20d81422ba622d09f8c4f23f9a88308df2a5140de1ee0c00bd15706f93"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/fromnumeric.py": {\n "bytes": 343,\n "sha256": "e536a89c956e0b5d74aafddfddca93b668dac935f4066a89020a00267e47dd92"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/function_base.py": {\n "bytes": 351,\n "sha256": "be18e1cec1330ddd7544783aa6295f3093b75fa93de1a9f944026a8fe9e5ce6b"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/getlimits.py": {\n "bytes": 335,\n "sha256": "ea70a4e13c342e35bb8e85aca6b2392e23338ded60b0e8ceda5490fcec0107c2"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/multiarray.py": {\n "bytes": 793,\n "sha256": "6e374f2dbbc9ba3eb533a4d991b0793573823661f841b52aea0dde3e428fe930"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/numeric.py": {\n "bytes": 360,\n "sha256": "0ad93f42293207698cd31234941781f184d47d3c104977d576920c46dba76cca"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/numerictypes.py": {\n "bytes": 347,\n "sha256": "6d7c13c3351a8736c7ac51a14b946424ebdbe93604b109d00b9c30f6637ed55c"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/overrides.py": {\n "bytes": 335,\n "sha256": "d456726f453a249bb2a23b7116f43b1d2276ae97e159e73417e5f499aa428dcf"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/records.py": {\n "bytes": 327,\n "sha256": "f727c50f1c8e73af255ea7db6a8b2044d970d5d6d63fc0911f320f12d12d4a07"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/shape_base.py": {\n "bytes": 339,\n "sha256": "dacadd42d17577c2e951b0e318c5d3f93ab3d8ad8d6933be6440b8be2098b306"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/umath.py": {\n "bytes": 319,\n "sha256": "84c56636b20276a5d18a2446ed4315d06afef7162a2469a438d1909f6d222bdf"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/ctypeslib/__init__.py": {\n "bytes": 193,\n "sha256": "585c0c8695762c93fe2103aca5a227855f1ce973ca670aa9a44f9cbed229aaf5"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/ctypeslib/_ctypeslib.py": {\n "bytes": 19079,\n "sha256": "36d114a62b108437c44c12c092a61fec08ead3188d859bab6f94a61861f9e290"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/doc/ufuncs.py": {\n "bytes": 5414,\n "sha256": "f71b7c1f7e0686b5eb16af5c585506bc915e3daf58b87f53ab50f30279b61361"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/dtypes.py": {\n "bytes": 1312,\n "sha256": "cee3f0802d228c5da80d100e27a23d24a85a26e857140ca00722d06a856d4f9e"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/exceptions.py": {\n "bytes": 7709,\n "sha256": "df16e967d97b1779a1c2dca6be5d2fa84e346a237c6e471e6a8f1a5df66c9795"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/__init__.py": {\n "bytes": 2448,\n "sha256": "7008141d680941966c7eff1ca3c28136bfe6f01e9fa7374169436f25e05ffcda"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/__main__.py": {\n "bytes": 130,\n "sha256": "ea2da3547d9f3eb895d5aa1c4d8fdd505bd62b5f2a6bece3a6721203e3a9177c"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/__version__.py": {\n "bytes": 48,\n "sha256": "f7d4ba9927afba1c0698ef64bbefbb55425e921374a2de3e2747192a21dcaa9c"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/_backends/__init__.py": {\n "bytes": 299,\n "sha256": "eff6c0edcff10e92dce3ff2f3df1f7dbe2f19fd7dc513823436e6caddbf0bc03"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/_backends/_backend.py": {\n "bytes": 1151,\n "sha256": "a055d9f3e57071049b96d97cfe98162cfa82399f18fe9c7ba1e4e4fc19712537"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/_backends/_distutils.py": {\n "bytes": 2385,\n "sha256": "8444f4581e2acbe0f80739de906016864f78e64e707aada51940d1ea1ae25cca"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/_backends/_meson.py": {\n "bytes": 8626,\n "sha256": "398d3089f27dddd148e357c1aba0ddb2f9d0fdeac0e6aba2c6fa7ad1eb5a888e"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/_isocbind.py": {\n "bytes": 2360,\n "sha256": "cda060a5f3cd466c551b776850895b6488b207df7432c5e2c0369bad28fd1e74"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/_src_pyf.py": {\n "bytes": 7695,\n "sha256": "3c7a68f43dbc2aadeaff7f8a157f03dec143f5e5fc0357372ee2cdcd7cc1c8ec"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/auxfuncs.py": {\n "bytes": 26920,\n "sha256": "767694c2b740bf8f8b6c488735b4b5beb8d03423b4941bb25a48f746dfd48b31"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/capi_maps.py": {\n "bytes": 30083,\n "sha256": "fc3a1b487a90a24faa4ea2299923493c0ef37f4006316426a1b090d72ec735c7"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/cb_rules.py": {\n "bytes": 25051,\n "sha256": "e8a6cfbbdc9f27eee901adb8223f47df82e5d7941cf025d3a8216250923a47c6"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/cfuncs.py": {\n "bytes": 52660,\n "sha256": "e09e0fd76a06a725991dbd4054a025ed827741482783018c0a7075221ac99fb5"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/common_rules.py": {\n "bytes": 5030,\n "sha256": "ffdcb32289493068297770fde0b741b0e0e77d4b2431182ddafd37ac40881c94"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/crackfortran.py": {\n "bytes": 146861,\n "sha256": "642ff279b193b0600ce150718cc18577d6ff9ac5e4f167e694c9ce516034c41d"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/diagnose.py": {\n "bytes": 5075,\n "sha256": "616363d6f33af1ee3b2dbdbbd30959939cab714fb24e5cc669834f059ee74c05"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/f2py2e.py": {\n "bytes": 28827,\n "sha256": "eb0439f72fc8ab8530f5461d129265c9ba52dbf6ffc3da04b60c253c270272c3"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/f90mod_rules.py": {\n "bytes": 9810,\n "sha256": "ed9e6fa2b538c215f8d39c4c2faea1af88b589c09473d82be9a9f847e00c87b3"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/func2subr.py": {\n "bytes": 10045,\n "sha256": "0d752c605d9327127de7beb3fcc927e3bf584bda6cb56b0a52dbbdfc9e858c1f"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/rules.py": {\n "bytes": 63094,\n "sha256": "f574d1eec4a0c245129e07119ea77ea4ddd86aad6cc230753ebacd8fb602a872"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/symbolic.py": {\n "bytes": 53310,\n "sha256": "a83d5d2d5d592ecb881814f794736c91abdbdfd866c2ee0311a934916928e681"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/use_rules.py": {\n "bytes": 3376,\n "sha256": "7a666148b3db343c811e7b1f28a5c39c6cf83ff830ae02f475f099703de7f43e"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/fft/__init__.py": {\n "bytes": 8156,\n "sha256": "251bd35d9a814b98e076ab2e0da2a5a7eabbfb11427a7e873ae127d5a26137c0"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/fft/_helper.py": {\n "bytes": 6787,\n "sha256": "8489f66721181b87cb077306bc23efa52acb5c57e1fb13b8cc62a58e4fd34236"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/fft/_pocketfft.py": {\n "bytes": 62599,\n "sha256": "8c4a13d3b123e678aaccacaf67c541935a7b80dd55283f2aca81bf315d02a3fa"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/fft/_pocketfft_umath.cpython-312-darwin.so": {\n "bytes": 329912,\n "sha256": "d902fbb947127a8e78f56d67057fad07f73563a91a60a761423e3ba09ffb8731"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/__init__.py": {\n "bytes": 3004,\n "sha256": "cd81aea847cfaaaecb0db89da7162cf068023409a8278c50805bc5dd7289e518"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_array_utils_impl.py": {\n "bytes": 1697,\n "sha256": "1985a2c8da8b43b0c6512057cf46db47a000a994ad0c8c147bbb6c6d9fffd793"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_arraypad_impl.py": {\n "bytes": 33519,\n "sha256": "69f17907431860505f879f7ac0f69be927464119e4068583b5f84f11fee63131"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_arraysetops_impl.py": {\n "bytes": 37341,\n "sha256": "9f896cc0ddc2f4f94ed7cf0b28fa5dba777ec77df3c44788afcd6448d55416b4"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_arrayterator_impl.py": {\n "bytes": 7218,\n "sha256": "1ed3800c81ee1bd0036db3138268783ff9ae91ed55fbc13e14d10eddb54e18f0"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_datasource.py": {\n "bytes": 22731,\n "sha256": "ce4f956e7e099431c455adc37ba0373608e79e2cd2262f8717466fbc0e98228e"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_format_impl.py": {\n "bytes": 36884,\n "sha256": "2a90cc1f9fc635b13815c166b03eda58e1fb11f8ac511e4d9b6d67d147e83691"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_function_base_impl.py": {\n "bytes": 193738,\n "sha256": "b2310fd26d09b566bc50c94be4d3032866503c16a75a5ff67ae994a4a7aea7c9"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_histograms_impl.py": {\n "bytes": 38432,\n "sha256": "52dbbb68041ced9a6c1e7ff4e288149d9ab565c7477e2a5cabd791abcd7ba155"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_index_tricks_impl.py": {\n "bytes": 31519,\n "sha256": "0cde783785ad86729ac3dc079a9f77ae8e1abb7d11d2cde9e35447ec91463e2b"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_iotools.py": {\n "bytes": 30876,\n "sha256": "d23b69be99792feff5383236d45fb58b5f6dd5edcbfbac09c517750922dec0bd"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_nanfunctions_impl.py": {\n "bytes": 71399,\n "sha256": "62d1c87a064ab166c6795a192ea7dd4004797f7b63d7a9a33c86cec6ca05e4b4"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_npyio_impl.py": {\n "bytes": 98636,\n "sha256": "557dbdca775fdf05ab660f6694c5d25194634426ddb2a22a7e1706828dc83b82"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_polynomial_impl.py": {\n "bytes": 44125,\n "sha256": "048794e3f17a3923deb56fdfa30544f29d03775bafb13bb57c9737b928130521"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_scimath_impl.py": {\n "bytes": 15684,\n "sha256": "6f9ffec91cd62483bc75afd9e01f2a41eebc8fc9693bc82db4696f18da65cadc"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_shape_base_impl.py": {\n "bytes": 39013,\n "sha256": "c9858af506f79e40dae1a4fee13900a06216aa78b95813d0a952156c7c37b311"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_stride_tricks_impl.py": {\n "bytes": 19115,\n "sha256": "d4ff05ee3167787eff06d5fbe0ebdc23a48d9f6890a733e348aa266e1355366c"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_twodim_base_impl.py": {\n "bytes": 33923,\n "sha256": "0fe52393e90c6565cc23ac1b8ba44fb347a02e084827309ece0468ac88b085c8"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_type_check_impl.py": {\n "bytes": 19914,\n "sha256": "d2b5b3bcb84c16ab9ddbeca1e7c45515063c0f61a98146039a77e99782b7b563"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_ufunclike_impl.py": {\n "bytes": 6014,\n "sha256": "2987a20d544c11fcfdf243a4c3e5d1cb47be8eb4f38c238399d946252afb8cd3"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_user_array_impl.py": {\n "bytes": 8026,\n "sha256": "65c91b14c34cdbc1136e97b5d9a6262780b58917a2a1233e4ded8dde217f5195"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_utils_impl.py": {\n "bytes": 23499,\n "sha256": "96db16f4c329de4d6fc3b22789d8584f97e8f60ae0c33eb6e1fbd17a21c3c967"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_version.py": {\n "bytes": 4783,\n "sha256": "0affabe9bf73540e9b0febf2158f2d09746ef5de4eb069e3a976a57d8d9194e0"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/array_utils.py": {\n "bytes": 144,\n "sha256": "5db732849f52d0894d9cff4dcbacb280b30400259650f34e53cbdebe3d531292"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/format.py": {\n "bytes": 477,\n "sha256": "9e9274789853eee28d2b96b49423067e226ff91e23c8d86220f7996c970d5c1b"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/introspect.py": {\n "bytes": 2749,\n "sha256": "e97a1b86c7a928352ff339a83e47d53391206421afeb53f63ca97f6916c49074"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/mixins.py": {\n "bytes": 7195,\n "sha256": "cd047e8888c2492796c5ea4ae62fbbba34967f0163ad467fab18deac88a4c3a6"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/npyio.py": {\n "bytes": 68,\n "sha256": "79a3ef7c7192cd413bd132471cb3823c85fd1b98a132e0447b1cbafcf36127d4"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/recfunctions.py": {\n "bytes": 59539,\n "sha256": "4f869ae715dabfd9ed7f0e589b33e2abf614921d76c06938d17c812d03c21335"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/scimath.py": {\n "bytes": 169,\n "sha256": "aa315a41eab4cc4225ee02aa3a16a3fef9820bf29a15dc9398774b514912a792"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/stride_tricks.py": {\n "bytes": 88,\n "sha256": "c74fc17f097270102547706fa714e775e3fde9d1c14ff7c049b9134d3cc16202"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/user_array.py": {\n "bytes": 63,\n "sha256": "ceceaee93017a00c9218065cd6a13a7ca0f800dfadeaead909a89968a9878aca"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/linalg/__init__.py": {\n "bytes": 2076,\n "sha256": "b50025b9aa09f4e77b1a67d7c03b8915a86cc1549179084da21a1a1a5d613d02"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/linalg/_linalg.py": {\n "bytes": 114861,\n "sha256": "e47d0e2aed21361291cd6651999a59e6479ba5866abb8eabb1dbea5b0d1618ac"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/linalg/_umath_linalg.cpython-312-darwin.so": {\n "bytes": 154848,\n "sha256": "86f2c3a5e1a516211e144ba13d90c18b9db903477fa66154ee5cdc4cc59c3067"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/linalg/lapack_lite.cpython-312-darwin.so": {\n "bytes": 53008,\n "sha256": "3f67c0ee45da7ae027d4e066495953574d9a901fe1304d9ddfbba79397a5d054"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/ma/__init__.py": {\n "bytes": 1406,\n "sha256": "5e90d6617c1ab83738f7e20db24e39e43d37530e29eb115db1d58e9f6aedf3b5"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/ma/core.py": {\n "bytes": 289255,\n "sha256": "9970fa92caa4f0c10ab51362b6347fe75d807461f98f931ef7d6daa9f30b003b"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/ma/extras.py": {\n "bytes": 67790,\n "sha256": "0089da9a60816d1bfae48d67d4bd416c54a6bdd42e6470e7ad9d68a2b1a84c81"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/ma/mrecords.py": {\n "bytes": 26486,\n "sha256": "01e5c14904591fd9946ad35ace8f23c661d3b8bf0e35ea3500c9476dab5645c4"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/ma/testutils.py": {\n "bytes": 10213,\n "sha256": "1d69f3dacc7244e58aedfadcd5a4768183cc36c4d4004e5e4e0b1c8d3268361c"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/matlib.py": {\n "bytes": 10638,\n "sha256": "fd2f4df12d8d36c1c6414725a31af1001b490de8c7894c8c74c24eed26b23e40"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/matrixlib/__init__.py": {\n "bytes": 243,\n "sha256": "52de88a9f8ee03e930c28e8704e6008058d75c2fe1ed857fdc7c83b0a33bd9d9"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/matrixlib/defmatrix.py": {\n "bytes": 30875,\n "sha256": "c29c3a95953d5faaa9f30009a240d7b76441acbd5e5ea96606dfa88c8c18ce55"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/polynomial/__init__.py": {\n "bytes": 6726,\n "sha256": "8064b02cda4f0a95df3e08894ac815a15b09d004b573efcc5a518e7a21b9e6c2"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/polynomial/_polybase.py": {\n "bytes": 39358,\n "sha256": "6f49028938149bc0f9402fcb5929bac8dbf00bbf67a4301e92c2b4bd03dc8824"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/polynomial/chebyshev.py": {\n "bytes": 62286,\n "sha256": "1621159a3acd55b85c6301b951012af66a910584cf86f1ab902ff2e80b314688"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/polynomial/hermite.py": {\n "bytes": 54571,\n "sha256": "8bc58c5e0038edc2792e214a8077556299a14326abbaceda26bb04debe096e6a"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/polynomial/hermite_e.py": {\n "bytes": 52273,\n "sha256": "244c341316423611ca5a98fb0b948a3f7dd04ebe88e307a04d4c8fbbd50ac8a3"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/polynomial/laguerre.py": {\n "bytes": 52447,\n "sha256": "2d8568238e6285355703b4af0dff07fcaac0bff2fac055dd0fbba3f830f6d7eb"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/polynomial/legendre.py": {\n "bytes": 51102,\n "sha256": "65667892aeceb7e239982303ed6cb910a06d23c8dcf3caac74f1b2d0c65a160f"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/polynomial/polynomial.py": {\n "bytes": 52669,\n "sha256": "9da743f753387a41e92d56c6c5763307e83b8932bdc8ce039408e0f7926c8cff"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/polynomial/polyutils.py": {\n "bytes": 22635,\n "sha256": "68064957a6f465962e2b520a0cf1e188981299757f7948a48d7714a132c7c3b6"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/__init__.py": {\n "bytes": 7480,\n "sha256": "585ce7b73b5454d6a25c2a50967f2dc322fc1d214d4bb5c0589949b105e06ea9"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_bounded_integers.cpython-312-darwin.so": {\n "bytes": 279912,\n "sha256": "0e08cf005eb9fb8378086928485ce30cfb12cd2bba411b6dd7202124734c2b75"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_common.cpython-312-darwin.so": {\n "bytes": 232000,\n "sha256": "ecd16f24eea4c44bf28cb718300f4156cc99c1725dbeda82bb447066976cacdd"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_examples/cffi/extending.py": {\n "bytes": 884,\n "sha256": "8e920bd678cc85fd277a19a53079206642319ecd890bd1840d88000a15fcec6f"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_examples/cffi/parse.py": {\n "bytes": 1750,\n "sha256": "3caf6f754c709af76716f1f7acea609e7a484b09e277bae2e573a606b316a49c"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_examples/numba/extending.py": {\n "bytes": 1959,\n "sha256": "67b67f5e9ec73c4e0ae4167b030a59dbdaafb9fb45024be132d357e77b6530cc"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_examples/numba/extending_distributions.py": {\n "bytes": 2036,\n "sha256": "7dd78f5de523e3ac972b430ad5cb33c541d088e4e236e36cadb66a3f0e00746b"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_generator.cpython-312-darwin.so": {\n "bytes": 651216,\n "sha256": "391cf5d6d41959c14abe10d1108a6bfe664dd12677ae9f88abf57d35447524a8"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_mt19937.cpython-312-darwin.so": {\n "bytes": 132176,\n "sha256": "94083b8c1e0d99556bc732b8a59ffc273cbcb981836da95459919f3158faf475"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_pcg64.cpython-312-darwin.so": {\n "bytes": 133456,\n "sha256": "0230f385549bca10636d49991e15c021ced23c705bedde086fc72a6276c611b4"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_philox.cpython-312-darwin.so": {\n "bytes": 115648,\n "sha256": "9cebd1d7e17a5d9296bd1b6b1769466f71e773a31bf5505958cc2723af601730"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_pickle.py": {\n "bytes": 2742,\n "sha256": "2ede3b99afef9e72477674257398d9fc3a811ec74a8b442250d68890c7fde6a0"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_sfc64.cpython-312-darwin.so": {\n "bytes": 97328,\n "sha256": "359fbd1de581fc5d26d87c9596675f9ceb35e394cc63d47812d4e6f73c5866ca"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/bit_generator.cpython-312-darwin.so": {\n "bytes": 189920,\n "sha256": "8a1bba22fafd2ed834c3cd7e95cc2a5a4abc09fe27004949d18e04ab2dd4d277"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/mtrand.cpython-312-darwin.so": {\n "bytes": 527616,\n "sha256": "8152e184801e01d2c92e547bece71b1eba501d6bfce6fbd3308575852e50055c"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/rec/__init__.py": {\n "bytes": 83,\n "sha256": "90d0186284800348b3a545516fed7cb09c3e88ab45ab6465d94e52387de91d13"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/strings/__init__.py": {\n "bytes": 83,\n "sha256": "a36ef01d6f2319a51f6c3a294b2126603e918dea37dc0ce4181053816ae5187e"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/testing/__init__.py": {\n "bytes": 581,\n "sha256": "12a7be3b1fb7252aa4e904679cf3c52c25bd224aaff4eb890e19e6dae18cdf37"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/testing/_private/__init__.py": {\n "bytes": 0,\n "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/testing/_private/extbuild.py": {\n "bytes": 7716,\n "sha256": "2e0d6ca80f7843be0a8b9bbeb31d0f123eeeaebdd83f8ef4f810a2caf2701314"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/testing/_private/utils.py": {\n "bytes": 98720,\n "sha256": "0514e16f2db0215fed604fb3015bae396ce9ff20476c27ec395d1e7bd0786491"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/testing/overrides.py": {\n "bytes": 2134,\n "sha256": "07c63c3e5a6f2bbd48712ba8b9b5d68f82f935598b5529fb58c8352fbc593bc9"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/testing/print_coercion_tables.py": {\n "bytes": 6286,\n "sha256": "49ba0d9822dce45c95f9447c80a8c97363c8a233755d04ef1f45b30eaef93363"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/typing/__init__.py": {\n "bytes": 6984,\n "sha256": "658ba47953a80a0da7db6408a1a78dfda3cd538228a58dcbbf8540f66e284007"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/typing/mypy_plugin.py": {\n "bytes": 6846,\n "sha256": "1d7ad11a80a461471cb56e06213a01c7fa973df6721b617723a4ef9da2c54a58"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/version.py": {\n "bytes": 293,\n "sha256": "833eb1051581de6bf4d23cd6f090e0fa4cbab6c548463dc718b5be62920fa2d7"\n }\n },\n "numpy_metadata": {\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy-2.4.6.dist-info/METADATA": {\n "bytes": 6608,\n "sha256": "b082c52ccbe3b880bae177c35171b52ed6c947f3ad71e3762f2723f60a411de0"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy-2.4.6.dist-info/RECORD": {\n "bytes": 110004,\n "sha256": "badf350b4082ff0798860f09e89c00ecbb809948d453b1c54b5ddb951d7eee45"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy-2.4.6.dist-info/WHEEL": {\n "bytes": 122,\n "sha256": "26726246f91715a154e07973ca8a82029674ee3cba86914a77f2e47c1a566739"\n }\n },\n "numpy_total_bytes": 10961565,\n "numpy_version": "2.4.6",\n "original_truememory_source_config_data_read": false,\n "original_venv_written": false,\n "psutil_metadata": {\n "bytes": 22187,\n "path": "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil-7.2.2.dist-info/METADATA",\n "sha256": "525f8ba78523cf7bc3759d62a105e7c57fd679ea702402b9449fb0746aa0217f"\n },\n "psutil_pins": {\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/__init__.py": {\n "bytes": 89878,\n "sha256": "d138a5786b163b56ba86ea0b2d5589dfca37e1bcdf8de1057fe1e933d6ab808a"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_common.py": {\n "bytes": 25254,\n "sha256": "1bbce9fd97e6f5439a0d385f28401f527c5a9d978d9d062593d7ef6fe215fc75"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_ntuples.py": {\n "bytes": 10970,\n "sha256": "9b8e8b938abae2786a515f686f4d8132218be897eb6b459b9324ed17d88a31c7"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_psaix.py": {\n "bytes": 17616,\n "sha256": "afe4038d5e1454559f17dac9e6c2c56ad8cf873d43880f6503e8549cc06a18ec"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_psbsd.py": {\n "bytes": 29263,\n "sha256": "c1a8314a9640649c99197ff261dbeec1f0080175e4a3f6b5cb370a39332deb94"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_pslinux.py": {\n "bytes": 84787,\n "sha256": "a8a09bab87848075b691c46112b00692745785a8c9d5a2c87037ae124b469749"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_psosx.py": {\n "bytes": 15913,\n "sha256": "89f0ea0e9c1fe38f224b612792bf31d07c8880d79281806e5379c00514f6b3f2"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_psposix.py": {\n "bytes": 11575,\n "sha256": "288887b58f5939e3b00e021f6e7b490c2f7f4852804a359918ab7d9c6fb39839"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_pssunos.py": {\n "bytes": 23934,\n "sha256": "cc348442008a54cb6054299b2ecae0d9a123c07bb4ba1f8e14b319487f83cee5"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_psutil_osx.abi3.so": {\n "bytes": 105920,\n "sha256": "ff4568b9187ca7b37c5134aaf614ffb061e7fedccebd84158343afb05991b8e2"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_pswindows.py": {\n "bytes": 35370,\n "sha256": "580ad391de734c113682200ee9f4245e34737f5e26af0a73a4e8c9b4458de220"\n }\n },\n "python_realpath": "/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/bin/python3.12",\n "python_sha256": "ae711ab41ea1e6d9c2daa0ef816a0e041f9388ffd7c5bccb7d167d5b8afacfaa",\n "python_version": "3.12.12",\n "runtime_closure_complete": false,\n "runtime_trust_policy": "Exact pinned package/NumPy/psutil/interpreter/framework/native dependencies; exact canonical trusted-unhashed stdlib Python and observed OS loader inputs. Qualified source proof, no native security sandbox",\n "schema": 2,\n "source_bytes": {\n "truememory/__init__.py": 6932,\n "truememory/_platform.py": 5947,\n "truememory/client.py": 13639,\n "truememory/clustering.py": 22412,\n "truememory/consolidation.py": 62951,\n "truememory/embedding_target.py": 5225,\n "truememory/engine.py": 174355,\n "truememory/fts_search.py": 10285,\n "truememory/hybrid.py": 12551,\n "truememory/hyde.py": 8143,\n "truememory/maintenance.py": 137455,\n "truememory/memory_budget.py": 41094,\n "truememory/model_client.py": 34164,\n "truememory/model_server.py": 100943,\n "truememory/personality.py": 68823,\n "truememory/personality_style_vec.py": 21995,\n "truememory/predictive.py": 18593,\n "truememory/query_classifier.py": 5045,\n "truememory/rebuild_source.py": 19160,\n "truememory/reranker.py": 25401,\n "truememory/salience.py": 20238,\n "truememory/storage.py": 68710,\n "truememory/temporal.py": 49851,\n "truememory/tier_config.py": 6988,\n "truememory/tier_switch/__init__.py": 319,\n "truememory/tier_switch/activation.py": 33404,\n "truememory/tier_switch/cache.py": 7779,\n "truememory/tier_switch/job.py": 25028,\n "truememory/tier_switch/manager.py": 47952,\n "truememory/tier_switch/runtime.py": 38531,\n "truememory/tier_switch/sensors.py": 4336,\n "truememory/tier_switch/serving.py": 18642,\n "truememory/tier_switch/source.py": 28269,\n "truememory/tier_switch/state_machine.py": 5509,\n "truememory/tier_switch/throttler.py": 14364,\n "truememory/tier_switch/worker.py": 27782,\n "truememory/vector_search.py": 96703\n },\n "source_commit": "aeecf25f78279629789d98a29bbd8bf817233391",\n "source_map_sha256": "e1cbacd6cdfa558110647572b614d0d3983413af3dc988530a30b56d6a06cbd6",\n "source_root": "/Users/j/dev/truememory-maintenance-v1/.work/worktrees/whole-process-memory-297-v1",\n "sources": {\n "truememory/__init__.py": "00d3ab14540092b63b449d14533b17292018ae4a69cd3f3ef1345e1a02b54d98",\n "truememory/_platform.py": "0688763a6fc26ef8db2b87866aabd5a2373a18f4ee735859cc6d595147e310c0",\n "truememory/client.py": "f020ac930617cd5d4848775ddd7390a61665e0fbdc62d51ccaf19bccdeeb3a33",\n "truememory/clustering.py": "ea9c2dbb7d083217983bb962f696e7bfcf08359ba5969f916fa63241baa535a5",\n "truememory/consolidation.py": "dcff2bf4c8f80315453b5078cdae575e4414adf879b69ef8f7983e72478fd474",\n "truememory/embedding_target.py": "da61626141f1ca5587eaa2fa3c27381f80834e79bc32351b5d7a3c20e86c7ddc",\n "truememory/engine.py": "898f2b8dd79e8ca18154f8e4b3ef0cbfe785c27b543719f975f4a1f2cf0a010c",\n "truememory/fts_search.py": "9c954bd2513ea8e3c82b33e0fbc311cd5b7346d12ee70da011df3f4244dc4389",\n "truememory/hybrid.py": "17b281113b2927c6e63088c0d48dc34c2681a9943e1d4f4aa4d0a70d1c36dd17",\n "truememory/hyde.py": "127e4d1c01767e13c0d963d6878f01a7329bb749b4510e831ef51c9c879276e4",\n "truememory/maintenance.py": "413ffbc08f92ba3ed331cab5e0aebd67ba2cadf2e5400f8b5c09c47f80521819",\n "truememory/memory_budget.py": "2d2578554d1c029c5205ac1fd27511f30e3d372a1178bf88cb7b81b0501409a4",\n "truememory/model_client.py": "a8a9332ab7cf532c98594f4f7455642f00d6235d9ca8a4552a75c68a49b7d0f3",\n "truememory/model_server.py": "424986a108d0ecd9e984ed77e352fba36305bf9aa3d79517de8fd1cfa4f8701b",\n "truememory/personality.py": "4a55a594360435bd56152738af061988b73e3374c3e8757e934c0b88cf4cd2af",\n "truememory/personality_style_vec.py": "f294e67b46ce4473b2cf15a49c80fc348de27f5236834dfbec832fc092c5362e",\n "truememory/predictive.py": "88ae1a2f8436b305855a5884e3ac5ebe435106808533dc0bc563d2054d374c41",\n "truememory/query_classifier.py": "52f1f3bfe5a75f45eb5aef389b490953422bf1d8fb03c5181bff816440bf2476",\n "truememory/rebuild_source.py": "e46639a50d35b711bc0c0b0de9e7a90652b1386b25356c14d095551673c7bd76",\n "truememory/reranker.py": "c02740b54246ca9f52fd6df90d424f18d3417c36734346ede128c91d3c66e90d",\n "truememory/salience.py": "a005256ecc20c44ad7d4825a11fafa2c2b5951a762450dfce9fd80ec7e12940e",\n "truememory/storage.py": "adc89850a897a2f9b478c8a23ad62fa438d7a214405ea21df886e029cdf4481a",\n "truememory/temporal.py": "52e98b3924573db6a209ef27d0318e254e5c976a7a65d75762f901d712505a65",\n "truememory/tier_config.py": "a8e690000c74f7629690c67fb99e34d4bd87f02b78795fb68ec2009f18348489",\n "truememory/tier_switch/__init__.py": "2333416f4985986b9fcf6558478f5c3cf9f75c277961c14902162f8f11ea1705",\n "truememory/tier_switch/activation.py": "24b820b0ff129fb42b9c13544e42b7c4b2ec8397b427d23b61c60c3e8ff19d47",\n "truememory/tier_switch/cache.py": "77bc24c176763629f83d199cc588eca9a9e7432e36e29584a2bd3d475ac314b4",\n "truememory/tier_switch/job.py": "c1def76a9510c3cd600ec11737ee068d71cedae2bc980048fba6c70bdee7a1f0",\n "truememory/tier_switch/manager.py": "160a61ff2b2a4b2a7fdeb3c7fa2442ce161f5b1a426735ea3cac53829b6b8c4b",\n "truememory/tier_switch/runtime.py": "861893308506c0189e50eea2d21a9da43b2c7be0eb90c56f8fd62e6bb40dc5be",\n "truememory/tier_switch/sensors.py": "6471176f75ab5ddce3aa523340a6567ec24f9243c3551f4203baa8ea2f15b55b",\n "truememory/tier_switch/serving.py": "25c1524a02affc127e170c16ceee0f61c2b3b221e28c5a19e9fb347aec57bef7",\n "truememory/tier_switch/source.py": "f8ffe9cd5890c5e6520ca52cfe20bad49cb789d2222e29fffc4300bdbc64f227",\n "truememory/tier_switch/state_machine.py": "39ccf8d3e944728f7bf18e6efe10d0e8f9db625f9d812220554dd980a932d1b9",\n "truememory/tier_switch/throttler.py": "3416ad7681a22b2478e9e4375041f7d2d364b474a81255103e11abe79d5b5f32",\n "truememory/tier_switch/worker.py": "8bae8daf54ab06c0693df46578e340cbde874dda0dccfdb5bcfc2a30d3081ef7",\n "truememory/vector_search.py": "2acd58a0dd858cc6a3a8c69117c1666f5e74d166ed9aeb57b81da4da45804739"\n },\n "staged_truememory_distribution_metadata": {\n ".work/artifacts/memory-installed-wheel-v2/metadata/METADATA": {\n "bytes": 20005,\n "sha256": "6f9399e0fa68584949e48662a5a9c53989eacecc78e39bb7e52cddccdd34662e"\n },\n ".work/artifacts/memory-installed-wheel-v2/metadata/RECORD": {\n "bytes": 9361,\n "sha256": "692cc8dac4cc7fb0e7a480ca1c836a5c6340fd8c1e256b789ad124ba1a02c844"\n },\n ".work/artifacts/memory-installed-wheel-v2/metadata/WHEEL": {\n "bytes": 87,\n "sha256": "5b77e4a649bbfb07fdbc123967eeecd1e5a478cfaca6eefc23c35e6fdf037848"\n },\n ".work/artifacts/memory-installed-wheel-v2/metadata/entry_points.txt": {\n "bytes": 164,\n "sha256": "0a30f6f99c5f7a3e9c87327c34fa7a56ec295a3f3237c7e084d0ca76ee668c9c"\n },\n ".work/artifacts/memory-installed-wheel-v2/metadata/licenses/LICENSE": {\n "bytes": 34523,\n "sha256": "0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0"\n }\n },\n "staged_wheel_sha256": "f723b880e455fea1545c31a9b7fe01def1902af3a36609cfa084fc9548296c51",\n "stdlib_complete_dependency_bytes_hashed": false,\n "supersedes_packet_sha256": "f6418ee79b7f63783f97e9bb3cde468655b952b21279a522cc8ae5f6bfaaffe4",\n "trusted_OS_sharedcache_bytes_hashed": false,\n "unresolved": [\n "Complete interpreter/stdlib/extension/dylib path admission closure requires separate review",\n "System dylibs may be in macOS sharedcache; linkednames/OSbuild are not byte-hash proof",\n "Actual NumPy/fullpackage64MiB/CPU2/5s fit is unproven"\n ]\n}\n' +INTERPRETER_RAW = b'{"all_publication_held":true,"created_utc":"2026-10-12T09:02:33.473994+00:00","exact_content_files":{"/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_psutil_osx.abi3.so":{"bytes":105920,"device":16777232,"inode":13393584,"kind":"runtime native dependency","sha256":"ff4568b9187ca7b37c5134aaf614ffb061e7fedccebd84158343afb05991b8e2"},"/opt/homebrew/Cellar/mpdecimal/4.0.1/lib/libmpdec.4.0.1.dylib":{"bytes":189376,"device":16777232,"inode":5790118,"kind":"runtime native dependency","sha256":"1013bf3eafc5969ab5388fbf283b50c132b208635b59fd5b87b5c71b706aa784"},"/opt/homebrew/Cellar/openssl@3/3.6.2/lib/libcrypto.3.dylib":{"bytes":4845712,"device":16777232,"inode":5810980,"kind":"runtime native dependency","sha256":"ef2239cec921003b54b61968f00489271e2a2805d7d1b4e69d632423f38efe1e"},"/opt/homebrew/Cellar/openssl@3/3.6.2/lib/libssl.3.dylib":{"bytes":884560,"device":16777232,"inode":5810979,"kind":"runtime native dependency","sha256":"26ecfd43c742914a142c8b414b1f308cefe0531fcc6e38b6d45021649850e758"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/Python":{"bytes":6036784,"device":16777232,"inode":5801272,"kind":"CPython framework","sha256":"c365f0090d2bcd97df69c194dbe0293224ef09d88cc8c1e59260c18250e11ac6"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/bin/python3.12":{"bytes":52640,"device":16777232,"inode":5797918,"kind":"runtime native dependency","sha256":"ae711ab41ea1e6d9c2daa0ef816a0e041f9388ffd7c5bccb7d167d5b8afacfaa"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_asyncio.cpython-312-darwin.so":{"bytes":118304,"device":16777232,"inode":5798190,"kind":"runtime native dependency","sha256":"8efe7fe97e42bfa00cf4702a1a37e5e665ff9f6fa2ae45093c68a4885cffd8de"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_bisect.cpython-312-darwin.so":{"bytes":71024,"device":16777232,"inode":5798189,"kind":"runtime native dependency","sha256":"4d8df53fb16339db1e9c832d5e7559bc91fc4c7912d586051490c0cbf9506fc4"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_blake2.cpython-312-darwin.so":{"bytes":89824,"device":16777232,"inode":5798167,"kind":"runtime native dependency","sha256":"d0f8d9738e2a76eab4d1118bcc8d75af2279f841b4e21d3cf2285f85d08cee6b"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_bz2.cpython-312-darwin.so":{"bytes":73200,"device":16777232,"inode":5798204,"kind":"runtime native dependency","sha256":"1012bb5a539948ddd2732162694616155d964487d73e7e8206b25fbe0965d162"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_cn.cpython-312-darwin.so":{"bytes":186208,"device":16777232,"inode":5798191,"kind":"runtime native dependency","sha256":"097bd9bef01f21790d15dca5abf8dfa7621fffce584a53a2ab515d379b41d6ac"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_hk.cpython-312-darwin.so":{"bytes":202816,"device":16777232,"inode":5798236,"kind":"runtime native dependency","sha256":"c9a7ed2f260c8a4a3c7b3d2c2677f33bb6a5e6ed01e8ebb6b67462bf2c4bde90"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_iso2022.cpython-312-darwin.so":{"bytes":72320,"device":16777232,"inode":5798217,"kind":"runtime native dependency","sha256":"ccb4d633b05ab2ccbbde4f064a569477cbb93462eb50d07cc7642d9112097786"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_jp.cpython-312-darwin.so":{"bytes":302720,"device":16777232,"inode":5798203,"kind":"runtime native dependency","sha256":"22e89fa609becb4bfb21563299c2b5b7218a8ec6c609db86983b1d432ca529b7"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_kr.cpython-312-darwin.so":{"bytes":169776,"device":16777232,"inode":5798221,"kind":"runtime native dependency","sha256":"b75f197cc6ff439429309e3d01e7521afd88965ad3a59780c73829d54f65262d"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_tw.cpython-312-darwin.so":{"bytes":169280,"device":16777232,"inode":5798219,"kind":"runtime native dependency","sha256":"4fe57867459470fc8778fdad724992dd39a9766aa69f363cdb04021489b6383d"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_contextvars.cpython-312-darwin.so":{"bytes":68672,"device":16777232,"inode":5798232,"kind":"runtime native dependency","sha256":"3f4033c0bd82bd65ff9a0dec226069c2c15f1f61d8dcd00ee4f8200c9a2e1037"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_crypt.cpython-312-darwin.so":{"bytes":68848,"device":16777232,"inode":5798227,"kind":"runtime native dependency","sha256":"4178beda9a6f47ff4835ea7aa2c295213909ccc036785312fd9034f5180c0414"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_csv.cpython-312-darwin.so":{"bytes":91632,"device":16777232,"inode":5798179,"kind":"runtime native dependency","sha256":"369b952f7a8815f26fe0838696030a5193f5a3ce0296e7c4056ba513173776dd"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_ctypes.cpython-312-darwin.so":{"bytes":158880,"device":16777232,"inode":5798205,"kind":"runtime native dependency","sha256":"4e7d8ebb1e94961ac689c37538a7e7d94661271eaea872de1956e78e003d10a5"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_ctypes_test.cpython-312-darwin.so":{"bytes":74416,"device":16777232,"inode":5798187,"kind":"runtime native dependency","sha256":"279b9c93123eefa0d0b3da516123ad0d56121fb7236bf0c2cdb34f5f92bbd4c0"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_curses.cpython-312-darwin.so":{"bytes":159808,"device":16777232,"inode":5798215,"kind":"runtime native dependency","sha256":"dd35ae0d20ee960d3485ab74bc7b1475ab6bc758776528ecf3d7a11b2eb4626a"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_curses_panel.cpython-312-darwin.so":{"bytes":73312,"device":16777232,"inode":5798173,"kind":"runtime native dependency","sha256":"8948ea0a8f26f23668f0d50aa27e8a9fd08bf9a69fd9084b8d1fc5b95bdafd2f"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_datetime.cpython-312-darwin.so":{"bytes":134080,"device":16777232,"inode":5798208,"kind":"runtime native dependency","sha256":"b0cfd7a9447561997dc02726cfa4c0967c58e3e28591e019421d552fcdd9c26a"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_dbm.cpython-312-darwin.so":{"bytes":72128,"device":16777232,"inode":5798216,"kind":"runtime native dependency","sha256":"13734b3815ea839aa7da5d0df61645138aef13598f74f791385bb37d3dced490"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_decimal.cpython-312-darwin.so":{"bytes":194160,"device":16777232,"inode":5798177,"kind":"runtime native dependency","sha256":"f12c874f31c111067580a340da7aa7f528d354ab282782ea876483d4d524f263"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_elementtree.cpython-312-darwin.so":{"bytes":118352,"device":16777232,"inode":5798228,"kind":"runtime native dependency","sha256":"bc0206adb8a4cab24ed39b244fa02b8f6f6e8700ac5bf95803d42c58780367f8"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_hashlib.cpython-312-darwin.so":{"bytes":98640,"device":16777232,"inode":5798171,"kind":"runtime native dependency","sha256":"bd270aacae15d88ca2e9c63ef8011b5fd96805db6e7bb3fe281abeb3fb5650ba"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_heapq.cpython-312-darwin.so":{"bytes":69920,"device":16777232,"inode":5798222,"kind":"runtime native dependency","sha256":"2b561bbb0b50dcea90638e035574ec811531a028f7d340fed8b82c1197288eb6"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_json.cpython-312-darwin.so":{"bytes":90560,"device":16777232,"inode":5798238,"kind":"runtime native dependency","sha256":"332c8737a959c43c649d694d0cb2d5241155e01dadbb3c1cc809dbc5700d11e0"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_lsprof.cpython-312-darwin.so":{"bytes":72912,"device":16777232,"inode":5798235,"kind":"runtime native dependency","sha256":"5a6cbf0cff49c8bf7e787a99a2a4f206c47703efaf95e6143a6a3133b4a371e4"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_lzma.cpython-312-darwin.so":{"bytes":92880,"device":16777232,"inode":5798199,"kind":"runtime native dependency","sha256":"31d89874e46ad2c229c9d9a575f731fcde92f2e6cd5b0c3f2985a247f67377d4"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_md5.cpython-312-darwin.so":{"bytes":71824,"device":16777232,"inode":5798224,"kind":"runtime native dependency","sha256":"4bbf4933c4274e6de96e861662b378a7b07d3dd4ae6a078270a7d0e4b0028fa0"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_multibytecodec.cpython-312-darwin.so":{"bytes":96672,"device":16777232,"inode":5798192,"kind":"runtime native dependency","sha256":"1806b8076b5b671cad0dbbb4b7a61f0ad37e7342960b6ef71ae7c35f730cb5c7"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_multiprocessing.cpython-312-darwin.so":{"bytes":73952,"device":16777232,"inode":5798225,"kind":"runtime native dependency","sha256":"30ed202b88240c2b001166bbc7bdf009afc00faaa019d4f555d5e94d2c02d5bc"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_opcode.cpython-312-darwin.so":{"bytes":69344,"device":16777232,"inode":5798220,"kind":"runtime native dependency","sha256":"82561570fe57a1f968ef9ed3b8e54ae062037db6ca910bb5d76562c0de57d42c"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_pickle.cpython-312-darwin.so":{"bytes":152736,"device":16777232,"inode":5798202,"kind":"runtime native dependency","sha256":"9c0fef7faef33c2b0a8a9d8edb05d0f4b58bd4b54f869abed97cd84d65baf186"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_posixshmem.cpython-312-darwin.so":{"bytes":69920,"device":16777232,"inode":5798169,"kind":"runtime native dependency","sha256":"ee89ff1dbdc18da313a65d56f3d2505bd669d5491d4ef1d14c9b68c78f8915c6"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_posixsubprocess.cpython-312-darwin.so":{"bytes":72496,"device":16777232,"inode":5798195,"kind":"runtime native dependency","sha256":"5f17a49a4d6bec05a5b2127b2cf173762577175ec40236ae8fe7b242b2e222e4"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_queue.cpython-312-darwin.so":{"bytes":72576,"device":16777232,"inode":5798166,"kind":"runtime native dependency","sha256":"35aec636c3d05307a9a17f320e1bf6a38314b276d431a9ea8528578292d8d8d0"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_random.cpython-312-darwin.so":{"bytes":71888,"device":16777232,"inode":5798230,"kind":"runtime native dependency","sha256":"82a7ed4d7519482d36a3ae06953cbf117d59838d686e818b741f17421256e349"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_scproxy.cpython-312-darwin.so":{"bytes":71296,"device":16777232,"inode":5798223,"kind":"runtime native dependency","sha256":"594511ebdf53542b712a079ae41a5e9ad35d987ba43d8e6cefd4cd8ba033c3dd"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sha1.cpython-312-darwin.so":{"bytes":71872,"device":16777232,"inode":5798168,"kind":"runtime native dependency","sha256":"4b6c1e2c28fc4ef9a9e16e53dcec2e5c65acb73489e32209d98f51a0e4ea35ed"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sha2.cpython-312-darwin.so":{"bytes":90112,"device":16777232,"inode":5798213,"kind":"runtime native dependency","sha256":"f44daa3f719025ede5455e5dcec063d61bf3d952051c3c7ce47cd6dc05319c7a"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sha3.cpython-312-darwin.so":{"bytes":73632,"device":16777232,"inode":5798181,"kind":"runtime native dependency","sha256":"0c31f8df357e9ea942c05b671e4f5449127a6c74a1f6a2f2034b678949666938"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_socket.cpython-312-darwin.so":{"bytes":132720,"device":16777232,"inode":5798176,"kind":"runtime native dependency","sha256":"39072a435f5c496bb7e0493e666b1c4fb7fd4fd3af6e737cc37973ecfb3b438b"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sqlite3.cpython-312-darwin.so":{"bytes":165664,"device":16777232,"inode":5798226,"kind":"runtime native dependency","sha256":"ed57e5b3f9a36b4fc626b9511fa85641d1bd20bddf91d51b4399d4fed12f463a"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_ssl.cpython-312-darwin.so":{"bytes":220192,"device":16777232,"inode":5798183,"kind":"runtime native dependency","sha256":"b8b10518a2e538fe52f2f9c697c5c04c8f78adfbd97dd2e3e1c23698c63600fe"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_statistics.cpython-312-darwin.so":{"bytes":68864,"device":16777232,"inode":5798180,"kind":"runtime native dependency","sha256":"acc8b5429de2de1c493bfb5bf63c18a783c8aff6bc5d438dc69f3d992981fe52"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_struct.cpython-312-darwin.so":{"bytes":95008,"device":16777232,"inode":5798163,"kind":"runtime native dependency","sha256":"faad693f3025bad2bad7a846c45cbaa2e6f59311e6033ae5a014541be5ad350c"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testbuffer.cpython-312-darwin.so":{"bytes":92128,"device":16777232,"inode":5798165,"kind":"runtime native dependency","sha256":"6e5c46995d00fa2589cf3aea5321d6c82e4a5e5033c43d73a1a26adddbfd68bc"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testcapi.cpython-312-darwin.so":{"bytes":368896,"device":16777232,"inode":5798194,"kind":"runtime native dependency","sha256":"742971e338168594364e73acbddc8baed68ce803313d7e7f555ef25a374bcb42"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testclinic.cpython-312-darwin.so":{"bytes":113040,"device":16777232,"inode":5798209,"kind":"runtime native dependency","sha256":"5508c03dc49992eb4f355da7892c4d02522f6420945d1e760e6ba43447ca857b"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testimportmultiple.cpython-312-darwin.so":{"bytes":68320,"device":16777232,"inode":5798175,"kind":"runtime native dependency","sha256":"e2bc9041f0af2c72dbf97328b0cbfeb7bda6f5972bf9dbdbdfd9f66c0c9af186"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testinternalcapi.cpython-312-darwin.so":{"bytes":94576,"device":16777232,"inode":5798231,"kind":"runtime native dependency","sha256":"e94180a61ef7448e05efd93f3baff65751829ca60962dde09656b06e4adca7c0"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testmultiphase.cpython-312-darwin.so":{"bytes":77152,"device":16777232,"inode":5798211,"kind":"runtime native dependency","sha256":"7d4ae01dbd58d96ba3095b09d9714db63da59bc9327e0cc02e162d02137f52e7"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testsinglephase.cpython-312-darwin.so":{"bytes":70256,"device":16777232,"inode":5798214,"kind":"runtime native dependency","sha256":"807d2c24503b6d3ab95771ace413a2be1b2ae67bf2571b530f015ceac57abe3d"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_uuid.cpython-312-darwin.so":{"bytes":68624,"device":16777232,"inode":5798218,"kind":"runtime native dependency","sha256":"653ea323d372448beab3de0f315a364fa5b9cb324ccfe9e9427c2b525751eb7d"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_xxinterpchannels.cpython-312-darwin.so":{"bytes":74928,"device":16777232,"inode":5798174,"kind":"runtime native dependency","sha256":"793e428e1eac546753e8476e5a2f2d201fbbfdc58ac5688ed38d79e6fb2cf14f"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_xxsubinterpreters.cpython-312-darwin.so":{"bytes":73936,"device":16777232,"inode":5798188,"kind":"runtime native dependency","sha256":"0c78f075c14580405c60a57396385f0d7dc22def54729a712e58040c646090ad"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_xxtestfuzz.cpython-312-darwin.so":{"bytes":71328,"device":16777232,"inode":5798210,"kind":"runtime native dependency","sha256":"1bbfa1d6d2758737f7858066148406054b3303f9e72551b5873c67c99a56e168"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_zoneinfo.cpython-312-darwin.so":{"bytes":91968,"device":16777232,"inode":5798170,"kind":"runtime native dependency","sha256":"013df37ed8270df3a08c37b092440b308e14236e36f850a85cee4f00543c84b5"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/array.cpython-312-darwin.so":{"bytes":97488,"device":16777232,"inode":5798164,"kind":"runtime native dependency","sha256":"1c1eae5ebe6616222a551aab4b98e04869096570083b639a938d4108d649b916"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/audioop.cpython-312-darwin.so":{"bytes":89696,"device":16777232,"inode":5798233,"kind":"runtime native dependency","sha256":"8fb121e801de3b95ddff68203c6424094e231e0cdeb6449a0bac73b617cecfa2"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/binascii.cpython-312-darwin.so":{"bytes":89056,"device":16777232,"inode":5798237,"kind":"runtime native dependency","sha256":"c3973d4e15c7fff739fedcaabdcdcd4306ddba019dd3ed64501172bf4d0438e6"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/cmath.cpython-312-darwin.so":{"bytes":88800,"device":16777232,"inode":5798229,"kind":"runtime native dependency","sha256":"bae2872aba117e1411e7893e771a76a0b4f23c75b1db312ffb7aef23a9bf77b6"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/fcntl.cpython-312-darwin.so":{"bytes":70576,"device":16777232,"inode":5798182,"kind":"runtime native dependency","sha256":"e55c4d1ab8fde9d06febb8dd14929c80ccc536433fa426b20693a0c7c54bde5e"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/grp.cpython-312-darwin.so":{"bytes":70832,"device":16777232,"inode":5798172,"kind":"runtime native dependency","sha256":"2001fe992d994e07207f4a110e453441ec61948ddc4dff865bda6ed90a63a7e9"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/math.cpython-312-darwin.so":{"bytes":111968,"device":16777232,"inode":5798212,"kind":"runtime native dependency","sha256":"deeeae703eca73e6bf0ae53404fa4835834c4bd9bd32c889cc9c238a017a24af"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/mmap.cpython-312-darwin.so":{"bytes":73568,"device":16777232,"inode":5798207,"kind":"runtime native dependency","sha256":"f209d7eee73b2ae94faed176d23682173bf4d42a622a9eda7f57d02b1e23d262"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/nis.cpython-312-darwin.so":{"bytes":71024,"device":16777232,"inode":5798206,"kind":"runtime native dependency","sha256":"6d399051d7bbba339df38e22f2f64f789e91a917e0f69034784b7311b37fd22a"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/pyexpat.cpython-312-darwin.so":{"bytes":99600,"device":16777232,"inode":5798200,"kind":"runtime native dependency","sha256":"be320ffc44952341adfe75b8d246c24837fb5a518b4db692bedea350db3b45a0"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/readline.cpython-312-darwin.so":{"bytes":95280,"device":16777232,"inode":5798201,"kind":"runtime native dependency","sha256":"321b279033e2c37316511de74fd25a3525fbc5705793d8f50d72e36a226bd5e2"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/resource.cpython-312-darwin.so":{"bytes":70560,"device":16777232,"inode":5798234,"kind":"runtime native dependency","sha256":"5ead8d0bd513326906c217ebef38b33aa57936c19f81a8e386914d5b5d99a6a4"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/select.cpython-312-darwin.so":{"bytes":92944,"device":16777232,"inode":5798198,"kind":"runtime native dependency","sha256":"ec19dcc94817f0261ec4af51d85b509a6bcd9a65dd2676b466e3d51c55b6426e"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/syslog.cpython-312-darwin.so":{"bytes":70624,"device":16777232,"inode":5798197,"kind":"runtime native dependency","sha256":"7ccafd2aa3bd053ede95f0c632fd23f31a2904ad258290bc5330a53931fc9b8d"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/termios.cpython-312-darwin.so":{"bytes":71808,"device":16777232,"inode":5798184,"kind":"runtime native dependency","sha256":"dd7f18c041f00c51d03c9e99f31e0d73b3f49bcc04633b365bb2294c619a8f29"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/unicodedata.cpython-312-darwin.so":{"bytes":1181200,"device":16777232,"inode":5798186,"kind":"runtime native dependency","sha256":"f481050f6ae43920240605bd7af4267dc8f6f44f33f6b2328e894dd854dce399"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/xxlimited.cpython-312-darwin.so":{"bytes":70784,"device":16777232,"inode":5798178,"kind":"runtime native dependency","sha256":"7bad5cfecfb05b33666bdc35fdce9b23f3dff4f8658fd0f35272a5cdf3d4480c"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/xxlimited_35.cpython-312-darwin.so":{"bytes":70192,"device":16777232,"inode":5798193,"kind":"runtime native dependency","sha256":"4e15fa86847061606a6ab78b6e4b8aae0345f9c6d6a344cf060f673bc94869f6"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/xxsubtype.cpython-312-darwin.so":{"bytes":69472,"device":16777232,"inode":5798196,"kind":"runtime native dependency","sha256":"4061f5e5aa25947893b8b2281ee922e34da6826b9de409477ac472733b7ed6f8"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/zlib.cpython-312-darwin.so":{"bytes":94112,"device":16777232,"inode":5798185,"kind":"runtime native dependency","sha256":"94382a4afd7c5f92c30b7561cf02606ee7906427aa5739e8bda71a6fadab04de"},"/opt/homebrew/Cellar/sqlite/3.53.3/lib/libsqlite3.3.53.3.dylib":{"bytes":1286928,"device":16777232,"inode":18678560,"kind":"runtime native dependency","sha256":"b64abb706584a8952e67a3efd872e3fe6e6852c6a2a0d09adec2e94c4ab08ca5"},"/opt/homebrew/Cellar/xz/5.8.3/lib/liblzma.5.dylib":{"bytes":184512,"device":16777232,"inode":18678574,"kind":"runtime native dependency","sha256":"3d5bfa2f097c31463642b1daab5e662b44368bb4da368f85e412e7f9adcbaa10"}},"interpreter":{"framework_realpath":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/Python","realpath":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/bin/python3.12","requested":"/Users/j/dev/TrueMemory/.venv/bin/python","version":"3.12.12"},"linkage_packet_path":".work/reports/mac-native-interpreter-linkage-v1.json","linkage_packet_sha256":"762d535cdb9f021729fe101c654966e52a9d8da285d5fe77bc0b1db815d43843","live_service_queried":false,"modules_imported":false,"native_GO":false,"native_or_models_executed":false,"original_source_config_data_read":false,"runtime_closure_complete":false,"schema":1,"stdlib_complete_dependency_bytes_hashed":false,"total_pinned_native_bytes":22436608,"trust_policy":"Qualified isolated source/dependency behavior; Python audit hooks do not form a native security sandbox","trusted_OS_sharedcache_bytes_hashed":false,"trusted_stdlib":{"builtins_and_frozen":"Interpreter-provided runtime tied to executable/framework pins","canonical_root":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12","excluded_subtrees":["site-packages","test","tests","idlelib","turtledemo","ensurepip","venv"],"lib_dynload_root":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload","python_sources_byte_hashed":false},"trusted_system_loader_names":["/System/Library/Frameworks/Accelerate.framework/Versions/A/Accelerate","/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation","/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit","/System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration","/usr/lib/libSystem.B.dylib","/usr/lib/libbz2.1.0.dylib","/usr/lib/libc++.1.dylib","/usr/lib/libedit.3.dylib","/usr/lib/libexpat.1.dylib","/usr/lib/libffi.dylib","/usr/lib/libncurses.5.4.dylib","/usr/lib/libpanel.5.4.dylib","/usr/lib/libz.1.dylib"]}\n' +LINKAGE_RAW = b'{"modules_imported":false,"observed_linkage":{"/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_psutil_osx.abi3.so":["/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation","/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/mpdecimal/4.0.1/lib/libmpdec.4.0.1.dylib":["/opt/homebrew/opt/mpdecimal/lib/libmpdec.4.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/openssl@3/3.6.2/lib/libcrypto.3.dylib":["/opt/homebrew/opt/openssl@3/lib/libcrypto.3.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/openssl@3/3.6.2/lib/libssl.3.dylib":["/opt/homebrew/opt/openssl@3/lib/libssl.3.dylib","/opt/homebrew/Cellar/openssl@3/3.6.2/lib/libcrypto.3.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/Python":["/opt/homebrew/opt/python@3.12/Frameworks/Python.framework/Versions/3.12/Python","/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/bin/python3.12":["/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/Python","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_asyncio.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_bisect.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_blake2.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_bz2.cpython-312-darwin.so":["/usr/lib/libbz2.1.0.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_cn.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_hk.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_iso2022.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_jp.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_kr.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_tw.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_contextvars.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_crypt.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_csv.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_ctypes.cpython-312-darwin.so":["/usr/lib/libffi.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_ctypes_test.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_curses.cpython-312-darwin.so":["/usr/lib/libncurses.5.4.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_curses_panel.cpython-312-darwin.so":["/usr/lib/libpanel.5.4.dylib","/usr/lib/libncurses.5.4.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_datetime.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_dbm.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_decimal.cpython-312-darwin.so":["/opt/homebrew/opt/mpdecimal/lib/libmpdec.4.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_elementtree.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_hashlib.cpython-312-darwin.so":["/opt/homebrew/opt/openssl@3/lib/libcrypto.3.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_heapq.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_json.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_lsprof.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_lzma.cpython-312-darwin.so":["/opt/homebrew/opt/xz/lib/liblzma.5.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_md5.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_multibytecodec.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_multiprocessing.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_opcode.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_pickle.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_posixshmem.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_posixsubprocess.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_queue.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_random.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_scproxy.cpython-312-darwin.so":["/System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration","/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sha1.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sha2.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sha3.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_socket.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sqlite3.cpython-312-darwin.so":["/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_ssl.cpython-312-darwin.so":["/opt/homebrew/opt/openssl@3/lib/libssl.3.dylib","/opt/homebrew/opt/openssl@3/lib/libcrypto.3.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_statistics.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_struct.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testbuffer.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testcapi.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testclinic.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testimportmultiple.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testinternalcapi.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testmultiphase.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testsinglephase.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_uuid.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_xxinterpchannels.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_xxsubinterpreters.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_xxtestfuzz.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_zoneinfo.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/array.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/audioop.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/binascii.cpython-312-darwin.so":["/usr/lib/libz.1.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/cmath.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/fcntl.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/grp.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/math.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/mmap.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/nis.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/pyexpat.cpython-312-darwin.so":["/usr/lib/libexpat.1.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/readline.cpython-312-darwin.so":["/usr/lib/libedit.3.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/resource.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/select.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/syslog.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/termios.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/unicodedata.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/xxlimited.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/xxlimited_35.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/xxsubtype.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/zlib.cpython-312-darwin.so":["/usr/lib/libz.1.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/sqlite/3.53.3/lib/libsqlite3.3.53.3.dylib":["/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib","/usr/lib/libz.1.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/xz/5.8.3/lib/liblzma.5.dylib":["/opt/homebrew/opt/xz/lib/liblzma.5.dylib","/usr/lib/libSystem.B.dylib"]},"requested_realpath_aliases":{"/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_psutil_osx.abi3.so":"/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_psutil_osx.abi3.so","/opt/homebrew/Cellar/openssl@3/3.6.2/lib/libcrypto.3.dylib":"/opt/homebrew/Cellar/openssl@3/3.6.2/lib/libcrypto.3.dylib","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/Python":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/Python","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/bin/python3.12":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/bin/python3.12","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_asyncio.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_asyncio.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_bisect.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_bisect.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_blake2.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_blake2.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_bz2.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_bz2.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_cn.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_cn.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_hk.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_hk.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_iso2022.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_iso2022.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_jp.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_jp.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_kr.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_kr.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_tw.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_tw.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_contextvars.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_contextvars.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_crypt.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_crypt.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_csv.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_csv.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_ctypes.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_ctypes.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_ctypes_test.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_ctypes_test.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_curses.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_curses.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_curses_panel.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_curses_panel.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_datetime.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_datetime.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_dbm.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_dbm.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_decimal.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_decimal.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_elementtree.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_elementtree.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_hashlib.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_hashlib.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_heapq.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_heapq.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_json.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_json.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_lsprof.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_lsprof.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_lzma.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_lzma.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_md5.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_md5.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_multibytecodec.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_multibytecodec.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_multiprocessing.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_multiprocessing.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_opcode.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_opcode.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_pickle.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_pickle.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_posixshmem.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_posixshmem.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_posixsubprocess.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_posixsubprocess.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_queue.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_queue.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_random.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_random.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_scproxy.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_scproxy.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sha1.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sha1.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sha2.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sha2.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sha3.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sha3.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_socket.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_socket.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sqlite3.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sqlite3.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_ssl.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_ssl.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_statistics.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_statistics.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_struct.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_struct.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testbuffer.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testbuffer.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testcapi.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testcapi.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testclinic.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testclinic.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testimportmultiple.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testimportmultiple.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testinternalcapi.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testinternalcapi.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testmultiphase.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testmultiphase.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testsinglephase.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testsinglephase.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_uuid.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_uuid.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_xxinterpchannels.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_xxinterpchannels.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_xxsubinterpreters.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_xxsubinterpreters.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_xxtestfuzz.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_xxtestfuzz.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_zoneinfo.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_zoneinfo.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/array.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/array.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/audioop.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/audioop.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/binascii.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/binascii.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/cmath.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/cmath.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/fcntl.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/fcntl.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/grp.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/grp.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/math.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/math.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/mmap.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/mmap.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/nis.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/nis.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/pyexpat.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/pyexpat.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/readline.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/readline.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/resource.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/resource.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/select.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/select.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/syslog.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/syslog.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/termios.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/termios.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/unicodedata.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/unicodedata.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/xxlimited.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/xxlimited.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/xxlimited_35.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/xxlimited_35.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/xxsubtype.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/xxsubtype.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/zlib.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/zlib.cpython-312-darwin.so","/opt/homebrew/opt/mpdecimal/lib/libmpdec.4.dylib":"/opt/homebrew/Cellar/mpdecimal/4.0.1/lib/libmpdec.4.0.1.dylib","/opt/homebrew/opt/openssl@3/lib/libcrypto.3.dylib":"/opt/homebrew/Cellar/openssl@3/3.6.2/lib/libcrypto.3.dylib","/opt/homebrew/opt/openssl@3/lib/libssl.3.dylib":"/opt/homebrew/Cellar/openssl@3/3.6.2/lib/libssl.3.dylib","/opt/homebrew/opt/python@3.12/Frameworks/Python.framework/Versions/3.12/Python":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/Python","/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib":"/opt/homebrew/Cellar/sqlite/3.53.3/lib/libsqlite3.3.53.3.dylib","/opt/homebrew/opt/xz/lib/liblzma.5.dylib":"/opt/homebrew/Cellar/xz/5.8.3/lib/liblzma.5.dylib"},"schema":1,"sharedcache_bytes_hashed":false,"trusted_system_loader_names":["/System/Library/Frameworks/Accelerate.framework/Versions/A/Accelerate","/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation","/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit","/System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration","/usr/lib/libSystem.B.dylib","/usr/lib/libbz2.1.0.dylib","/usr/lib/libc++.1.dylib","/usr/lib/libedit.3.dylib","/usr/lib/libexpat.1.dylib","/usr/lib/libffi.dylib","/usr/lib/libncurses.5.4.dylib","/usr/lib/libpanel.5.4.dylib","/usr/lib/libz.1.dylib"]}\n' + + +def policy_module() -> ModuleType: + name = "native_package_policy_test_v3" + spec = importlib.util.spec_from_file_location(name, PARENT) + if spec is None or spec.loader is None: + raise RuntimeError("Policy source unavailable") + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module + spec.loader.exec_module(module) + return module + + +def bootstrap_module() -> ModuleType: + name = "native_package_bootstrap_test_v3" + spec = importlib.util.spec_from_file_location(name, BOOTSTRAP) + if spec is None or spec.loader is None: + raise RuntimeError("Bootstrap source unavailable") + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module + spec.loader.exec_module(module) + return module + + +def ctypes_frames(stdlib: Path) -> tuple[ModuleType, SimpleNamespace, object]: + filename = str(stdlib / "ctypes/__init__.py") + module = ModuleType("ctypes") + module.__file__ = filename + source = "class CDLL:\n def __init__(self):\n pass\nclass PyDLL(CDLL):\n pass\n" + exec(compile(source, filename, "exec", dont_inherit=True), module.__dict__) + instance = module.PyDLL.__new__(module.PyDLL) + instance._name = None + initializer = SimpleNamespace( + f_code=SimpleNamespace(co_filename=filename, co_name=""), + f_globals=module.__dict__, + f_back=None, + ) + constructor = SimpleNamespace( + f_code=module.CDLL.__init__.__code__, + f_globals=module.__dict__, + f_locals={"self": instance}, + f_back=initializer, + ) + return module, constructor, instance + + +def manifest_fixture(policy: ModuleType) -> dict[str, object]: + return { + "version": 1, + "stage": "B", + "synthetic": True, + "root_execution_go": True, + "run_root": str(policy.RUN_ROOT), + "home": str(policy.RUN_ROOT / "home"), + "source_root": str(policy.SOURCE_ROOT), + "source_commit": policy.SOURCE_COMMIT, + "sources": dict(policy.SOURCE_HASHES), + "sources_sha256": policy.SOURCE_MAP_SHA, + "data": dict(policy.DATA_HASHES), + "data_sha256": policy.DATA_MAP_SHA, + "runtime_packet_sha256": policy.RUNTIME_PACKET_SHA, + "interpreter_packet_sha256": policy.INTERPRETER_PACKET_SHA, + "supervisor_sha256": "a" * 64, + "bootstrap_sha256": "b" * 64, + "limits": dict(policy.LIMITS), + "cases": [{"id": "package-ping"}], + } + + +def result_fixture( + policy: ModuleType, pid: int = 100, uid: int = 7 +) -> dict[str, object]: + caches = {name: None for name in policy.CACHE_KEYS} + registry = { + "pid": pid, + "metric": "process_rss_sampled", + "effective_limit_bytes": 8589934592, + "owner_count": 1, + "closing_owner_count": 0, + "lease_count": 0, + "reserved_known_bytes": 0, + "high_water_known_bytes": 512, + "unprofiled_stages": list(policy.UNKNOWN_STAGES), + "coverage_truncated": False, + "physical_hard_cap": False, + "os_enforcement": copy.deepcopy(policy.CAPABILITY), + } + details = { + "package_version": "0.7.6.2", + "initializer_executed": True, + "paths_exact": True, + "ping": {"ok": True, "protocol": 1}, + "timeout_ms": 200, + "source_data_loaded": True, + "neural_imports": [], + "caches_before": caches, + "caches_after": dict(caches), + "socket_identity": {"device": 1, "inode": 2}, + "socket_uid": uid, + "socket_mode": 384, + "pid_file": {"pid": pid, "device": 1, "inode": 3, "mode": 384}, + "lock_identity": {"device": 1, "inode": 4}, + "bind_lock_held": True, + "registry": registry, + } + return { + "type": "result", + "version": 1, + "case": "package-ping", + "passed": True, + "proof_scope": policy.PROOF_SCOPE, + "import_mode": "full_source_package", + "source_commit": policy.SOURCE_COMMIT, + "sources_sha256": policy.SOURCE_MAP_SHA, + "source_count": 37, + "data_sha256": policy.DATA_MAP_SHA, + "runtime_packet_sha256": policy.RUNTIME_PACKET_SHA, + "interpreter_packet_sha256": policy.INTERPRETER_PACKET_SHA, + "details": details, + } + + +class PackageAcceptanceContracts(unittest.TestCase): + def test_ctypes_None_equality_forgery_refuses_without_comparing(self) -> None: + comparisons: list[object] = [] + + class EqNone: + def __eq__(self, value: object) -> bool: + comparisons.append(value) + return value is None + + worker = self.bootstrap + guard = worker.AuditGuard.__new__(worker.AuditGuard) + guard.stdlib = Path("/synthetic/stdlib") + guard.ctypes_namespace_loads = 0 + module, frame, _ = ctypes_frames(guard.stdlib) + with ( + patch.dict(worker.sys.modules, {"ctypes": module}), + patch.object(worker.sys, "_getframe", return_value=frame) as frame_lookup, + ): + with self.assertRaises(worker.BootstrapError): + guard("ctypes.dlopen", (EqNone(),)) + frame_lookup.assert_not_called() + self.assertEqual([], comparisons) + self.assertEqual(0, guard.ctypes_namespace_loads) + + @classmethod + def setUpClass(cls) -> None: + cls.policy = policy_module() + cls.bootstrap = bootstrap_module() + + def test_ctypes_actual_initializer_one_shot_only(self) -> None: + worker = self.bootstrap + guard = worker.AuditGuard.__new__(worker.AuditGuard) + guard.stdlib = Path("/synthetic/stdlib") + guard.ctypes_namespace_loads = 0 + module, frame, _ = ctypes_frames(guard.stdlib) + with ( + patch.dict(worker.sys.modules, {"ctypes": module}), + patch.object(worker.sys, "_getframe", return_value=frame), + ): + guard("ctypes.dlopen", (None,)) + self.assertEqual(1, guard.ctypes_namespace_loads) + with self.assertRaises(worker.BootstrapError): + guard("ctypes.dlopen", (None,)) + self.assertEqual(1, guard.ctypes_namespace_loads) + + def test_ctypes_process_namespace_argument_count_and_library_negatives( + self, + ) -> None: + worker = self.bootstrap + for args in ( + [None], + (), + (None, 0), + ("/usr/lib/libSystem.B.dylib",), + (b"library",), + (False,), + (0,), + ): + guard = worker.AuditGuard.__new__(worker.AuditGuard) + guard.stdlib = Path("/synthetic/stdlib") + guard.ctypes_namespace_loads = 0 + with self.subTest(args=args), self.assertRaises(worker.BootstrapError): + guard("ctypes.dlopen", args) + self.assertEqual(0, guard.ctypes_namespace_loads) + for count in (False, True, 1, -1, 1.0): + guard = worker.AuditGuard.__new__(worker.AuditGuard) + guard.stdlib = Path("/synthetic/stdlib") + guard.ctypes_namespace_loads = count + with self.subTest(count=count), self.assertRaises(worker.BootstrapError): + guard("ctypes.dlopen", (None,)) + + def test_ctypes_foreign_module_globals_and_constructor_frames_refuse(self) -> None: + worker = self.bootstrap + for mode in ( + "module_missing", + "foreign_module", + "constructor_globals", + "initializer_globals", + "constructor_file", + "constructor_name", + "foreign_code", + "initializer_file", + "initializer_name", + "initializer_missing", + ): + with self.subTest(mode=mode): + guard = worker.AuditGuard.__new__(worker.AuditGuard) + guard.stdlib = Path("/synthetic/stdlib") + guard.ctypes_namespace_loads = 0 + module, frame, _ = ctypes_frames(guard.stdlib) + current = module + if mode == "module_missing": + current = None + elif mode == "foreign_module": + current = ModuleType("ctypes") + elif mode == "constructor_globals": + frame.f_globals = dict(module.__dict__) + elif mode == "initializer_globals": + frame.f_back.f_globals = dict(module.__dict__) + elif mode in ("constructor_file", "constructor_name", "foreign_code"): + source = ( + "def __init__(self):\n pass\n" + if mode != "constructor_name" + else "def foreign(self):\n pass\n" + ) + namespace = {} + filename = ( + "/synthetic/foreign.py" + if mode == "constructor_file" + else str(guard.stdlib / "ctypes/__init__.py") + ) + exec( + compile(source, filename, "exec", dont_inherit=True), namespace + ) + frame.f_code = namespace[ + "foreign" if mode == "constructor_name" else "__init__" + ].__code__ + elif mode == "initializer_file": + frame.f_back.f_code.co_filename = "/synthetic/foreign.py" + elif mode == "initializer_name": + frame.f_back.f_code.co_name = "foreign" + else: + frame.f_back = None + with ( + patch.dict(worker.sys.modules, {"ctypes": current}), + patch.object(worker.sys, "_getframe", return_value=frame), + ): + with self.assertRaises(worker.BootstrapError): + guard("ctypes.dlopen", (None,)) + self.assertEqual(0, guard.ctypes_namespace_loads) + + def test_ctypes_namespace_type_name_and_pythonapi_negatives(self) -> None: + worker = self.bootstrap + for mode in ( + "CDLL_not_class", + "CDLL_builtin_init", + "PyDLL_not_class", + "CDLL_self", + "PyDLL_subclass", + "named_library", + "name_missing", + "pythonapi_present", + ): + with self.subTest(mode=mode): + guard = worker.AuditGuard.__new__(worker.AuditGuard) + guard.stdlib = Path("/synthetic/stdlib") + guard.ctypes_namespace_loads = 0 + module, frame, instance = ctypes_frames(guard.stdlib) + if mode == "CDLL_not_class": + module.CDLL = None + elif mode == "CDLL_builtin_init": + module.CDLL = type("CDLL", (), {}) + elif mode == "PyDLL_not_class": + module.PyDLL = None + elif mode == "CDLL_self": + frame.f_locals["self"] = module.CDLL.__new__(module.CDLL) + frame.f_locals["self"]._name = None + elif mode == "PyDLL_subclass": + child = type("ForeignPyDLL", (module.PyDLL,), {}) + frame.f_locals["self"] = child.__new__(child) + frame.f_locals["self"]._name = None + elif mode == "named_library": + instance._name = "library" + elif mode == "name_missing": + del instance._name + else: + module.pythonapi = None + with ( + patch.dict(worker.sys.modules, {"ctypes": module}), + patch.object(worker.sys, "_getframe", return_value=frame), + ): + with self.assertRaises(worker.BootstrapError): + guard("ctypes.dlopen", (None,)) + self.assertEqual(0, guard.ctypes_namespace_loads) + + def test_manifest_actual_frozen_positive(self) -> None: + value = manifest_fixture(self.policy) + self.assertIs(self.policy.validate_manifest(value), value) + self.assertEqual(37, len(value["sources"])) + self.assertEqual(18, len(value)) + self.assertEqual(1, value["limits"]["threads"]) + self.assertEqual(4, self.policy.B_OS_THREAD_MAX) + + def test_manifest_bool_path_source_limit_and_extra_refuse(self) -> None: + for field, value in ( + ("version", True), + ("root_execution_go", 1), + ("run_root", "/synthetic/other"), + ("source_commit", "c" * 40), + ("sources_sha256", "c" * 64), + ("data_sha256", "c" * 64), + ("runtime_packet_sha256", "c" * 64), + ): + with self.subTest(field=field): + row = manifest_fixture(self.policy) + row[field] = value + with self.assertRaises(self.policy.SafetyError): + self.policy.validate_manifest(row) + for mode in ("extra", "source_missing", "limit_bool", "limit_raise"): + with self.subTest(mode=mode): + row = manifest_fixture(self.policy) + if mode == "extra": + row["unexpected"] = True + elif mode == "source_missing": + row["sources"].pop("truememory/__init__.py") + elif mode == "limit_bool": + row["limits"]["threads"] = True + else: + row["limits"]["rss_abort_bytes"] += 1 + with self.assertRaises(self.policy.SafetyError): + self.policy.validate_manifest(row) + + def test_case_one_exact_operation(self) -> None: + self.assertEqual( + {"id": "package-ping"}, self.policy.case_contract({"id": "package-ping"}) + ) + for value in ( + {"id": "package-ping", "timeout": 120}, + {"id": "embed"}, + [], + {"id": True}, + ): + with self.assertRaises(self.policy.SafetyError): + self.policy.case_contract(value) + + def test_runtime_actual_raw_hashes_and_qualified_flags(self) -> None: + result = self.policy.runtime_contract(PACKAGE_RAW, INTERPRETER_RAW, LINKAGE_RAW) + self.assertEqual(37, len(result["package"]["sources"])) + self.assertEqual(193, len(result["package"]["numpy_files"])) + self.assertEqual(84, len(result["interpreter"]["exact_content_files"])) + self.assertIs(result["package"]["runtime_closure_complete"], False) + self.assertIs( + result["interpreter"]["stdlib_complete_dependency_bytes_hashed"], False + ) + self.assertIs(result["linkage"]["sharedcache_bytes_hashed"], False) + for index, raw in enumerate((PACKAGE_RAW, INTERPRETER_RAW, LINKAGE_RAW)): + args = [PACKAGE_RAW, INTERPRETER_RAW, LINKAGE_RAW] + args[index] = raw + b" " + with self.assertRaises(self.policy.SafetyError): + self.policy.runtime_contract(*args) + + def test_runtime_source_maps_data_and_metadata_current(self) -> None: + runtime = self.policy.runtime_contract( + PACKAGE_RAW, INTERPRETER_RAW, LINKAGE_RAW + ) + self.assertEqual( + "aeecf25f78279629789d98a29bbd8bf817233391", + runtime["package"]["source_commit"], + ) + self.assertEqual( + { + "truememory/data/l3_weights.json": self.policy.DATA_HASHES[ + "truememory/data/l3_weights.json" + ] + }, + runtime["package"]["data"], + ) + self.assertEqual( + "f723b880e455fea1545c31a9b7fe01def1902af3a36609cfa084fc9548296c51", + runtime["package"]["staged_wheel_sha256"], + ) + self.assertTrue( + all( + "memory-installed-wheel-v2/metadata/" in name + for name in runtime["package"][ + "staged_truememory_distribution_metadata" + ] + ) + ) + files = self.policy.runtime_files(runtime) + self.assertGreater(len(files), 193) + + def test_complete_result_actual_field_shape_and_intact_copy(self) -> None: + row = result_fixture(self.policy) + original = copy.deepcopy(row) + self.assertIs( + self.policy.result_contract(row, manifest_fixture(self.policy), 100, 7), row + ) + self.assertEqual(original, row) + self.assertEqual( + (13, 16, 12, 7), + ( + len(row), + len(row["details"]), + len(row["details"]["registry"]), + len(row["details"]["registry"]["os_enforcement"]), + ), + ) + self.assertLessEqual(len(self.policy.encoded(row)) + 1, 2048) + + def test_result_bool_false_scope_cache_and_unknowns_refuse(self) -> None: + mutations = [ + ("source_count", True), + ("passed", 1), + ("import_mode", "isolated_source_module"), + ("source_commit", "a" * 40), + ] + for field, value in mutations: + row = result_fixture(self.policy) + row[field] = value + with self.assertRaises(self.policy.SafetyError): + self.policy.result_contract(row, manifest_fixture(self.policy), 100, 7) + for mode in ( + "cache", + "unknown_empty", + "unknown_omitted", + "cap_reason", + "hard_cap", + "active_lease", + "wrong_pid", + "protocol_bool", + ): + with self.subTest(mode=mode): + row = result_fixture(self.policy) + d = row["details"] + r = d["registry"] + if mode == "cache": + d["caches_after"]["vector_model"] = "fake" + elif mode == "unknown_empty": + r["unprofiled_stages"] = [] + elif mode == "unknown_omitted": + r["unprofiled_stages"].pop() + elif mode == "cap_reason": + r["os_enforcement"]["reason"] = "" + elif mode == "hard_cap": + r["physical_hard_cap"] = True + elif mode == "active_lease": + r["lease_count"] = 1 + elif mode == "wrong_pid": + d["pid_file"]["pid"] = 101 + else: + d["ping"]["protocol"] = True + with self.assertRaises(self.policy.SafetyError): + self.policy.result_contract( + row, manifest_fixture(self.policy), 100, 7 + ) + + def test_result_large_valid_numbers_refuse_without_mutation(self) -> None: + row = result_fixture(self.policy, pid=(1 << 63) - 1, uid=(1 << 63) - 1) + for key in ("socket_identity", "lock_identity"): + row["details"][key] = {"device": (1 << 63) - 1, "inode": (1 << 63) - 1} + row["details"]["pid_file"].update(device=(1 << 63) - 1, inode=(1 << 63) - 1) + row["details"]["registry"]["high_water_known_bytes"] = 8589934592 + original = copy.deepcopy(row) + self.assertGreater(len(self.policy.encoded(row)) + 1, 2048) + with self.assertRaises(self.policy.SafetyError): + self.policy.result_contract( + row, manifest_fixture(self.policy), (1 << 63) - 1, (1 << 63) - 1 + ) + self.assertEqual(original, row) + + def test_mutation_actual_producer_flags_and_socket_count(self) -> None: + policy = self.policy + control = policy.MutationPolicy({"id": "package-ping"}) + events = [ + ("os.mkdir", control.model_dir, 0o777), + ("os.chmod", control.model_dir, 0o700), + ("open", control.lock, os.O_CREAT | os.O_RDWR), + ("socket.__new__", policy.ENDPOINT, 0), + ("socket.bind", policy.ENDPOINT, 0), + ("os.chmod", policy.ENDPOINT, 0o600), + ("open", control.pid, os.O_CREAT | os.O_WRONLY | os.O_TRUNC), + ("socket.__new__", policy.ENDPOINT, 0), + ("socket.connect", policy.ENDPOINT, 0), + ("socket.__new__", policy.ENDPOINT, 0), + ] + for seq, (event, path, flags) in enumerate(events, 1): + result = control.decide( + { + "type": "mutation", + "version": 1, + "seq": seq, + "event": event, + "path": str(path), + "source": None, + "flags": flags, + } + ) + self.assertEqual(seq, result["seq"]) + self.assertEqual(3, control.socket_objects) + with self.assertRaises(policy.SafetyError): + control.decide( + { + "type": "mutation", + "version": 1, + "seq": 11, + "event": "socket.__new__", + "path": str(policy.ENDPOINT), + "source": None, + "flags": 0, + } + ) + + def test_mutation_extra_flags_wrong_paths_unlink_sequence_refuse(self) -> None: + policy = self.policy + for event, path, flags in ( + ("os.mkdir", policy.ENDPOINT.parent, 0o700), + ("open", policy.ENDPOINT.with_name("other"), os.O_CREAT | os.O_WRONLY), + ( + "open", + policy.ENDPOINT.with_name("model_server.pid"), + os.O_CREAT | os.O_RDWR | os.O_TRUNC, + ), + ("os.remove", policy.ENDPOINT, 0), + ("socket.connect", policy.ENDPOINT, 0), + ): + control = policy.MutationPolicy({"id": "package-ping"}) + with self.assertRaises(policy.SafetyError): + control.decide( + { + "type": "mutation", + "version": 1, + "seq": 1, + "event": event, + "path": str(path), + "source": None, + "flags": flags, + } + ) + control = policy.MutationPolicy({"id": "package-ping"}) + with self.assertRaises(policy.SafetyError): + control.decide( + { + "type": "mutation", + "version": 1, + "seq": 2, + "event": "os.mkdir", + "path": str(control.model_dir), + "source": None, + "flags": 0o777, + } + ) + + def test_native_hash_streaming_closes_on_success_and_failure(self) -> None: + policy = self.policy + data = b"x" * 65537 + info = SimpleNamespace( + st_mode=stat.S_IFREG | 0o444, + st_size=len(data), + st_dev=1, + st_ino=2, + st_mtime_ns=3, + st_ctime_ns=4, + ) + path = Path("/synthetic/runtime.so") + for checksum in (hashlib.sha256(data).hexdigest(), "f" * 64): + with ( + patch.object(Path, "resolve", lambda self: self), + patch.object(Path, "is_symlink", return_value=False), + patch.object(policy.os, "open", return_value=87) as opened, + patch.object(policy.os, "fstat", return_value=info), + patch.object( + policy.os, "read", side_effect=[data[:65536], data[65536:], b""] + ) as read, + patch.object(policy.os, "close") as close, + ): + if checksum.startswith("f" * 64): + with self.assertRaises(policy.SafetyError): + policy.native_file_sha256( + path, {"bytes": len(data), "sha256": checksum} + ) + else: + self.assertEqual( + checksum, + policy.native_file_sha256( + path, {"bytes": len(data), "sha256": checksum} + ), + ) + close.assert_called_once_with(87) + self.assertTrue(opened.call_args.args[1] & os.O_NONBLOCK) + self.assertTrue(opened.call_args.args[1] & os.O_NOFOLLOW) + self.assertTrue( + all(call.args[1] <= 65536 for call in read.call_args_list) + ) + + def test_thread_samples_strict_four_ceiling(self) -> None: + for value in (1, 2, 3, 4): + self.assertEqual(value, self.policy.thread_sample_contract(value)) + for value in (0, 5, True, 4.0, None): + with self.assertRaises(self.policy.SafetyError): + self.policy.thread_sample_contract(value) + self.assertEqual(1, self.policy.LIMITS["threads"]) + + def test_terminal_ACK_whole_object(self) -> None: + self.assertEqual( + self.policy.TERMINAL_ACK, + self.policy.terminal_ACK_contract(dict(self.policy.TERMINAL_ACK)), + ) + for value in ( + {"type": "result_ack", "version": True, "case": "package-ping"}, + {"type": "result_ack", "version": 1, "case": "package-ping", "extra": 1}, + {"type": "result_ack", "version": 1, "case": "other"}, + ): + with self.assertRaises(self.policy.SafetyError): + self.policy.terminal_ACK_contract(value) + + def test_actual_bootstrap_accepted_descriptor_requires_exact_frame(self) -> None: + worker = self.bootstrap + endpoint = worker.RUN_ROOT / "home/.truememory/model.sock" + stdlib = Path("/synthetic/stdlib") + for mode in ( + "accepted", + "foreign_frame", + "foreign_listener", + "foreign_fd", + "regular_fd", + "wrong_uid", + "extra_wrapper", + ): + with self.subTest(mode=mode): + guard = worker.AuditGuard.__new__(worker.AuditGuard) + guard.stdlib, guard.endpoint = stdlib, endpoint + guard.listener = Mock() + guard.listener.getsockname.return_value = str(endpoint) + guard.client = None + guard.accepted = None + guard.accepted_checked = False + guard.socket_count = 3 if mode == "extra_wrapper" else 2 + guard.grant = Mock() + caller = SimpleNamespace( + f_code=SimpleNamespace( + co_filename=str(stdlib / "socket.py"), co_name="accept" + ), + f_locals={"self": guard.listener, "fd": 87}, + f_back=None, + ) + if mode == "foreign_frame": + caller.f_code.co_filename = "/synthetic/foreign.py" + elif mode == "foreign_listener": + caller.f_locals["self"] = Mock() + elif mode == "foreign_fd": + caller.f_locals["fd"] = 88 + initializer = SimpleNamespace( + f_code=SimpleNamespace( + co_filename=str(stdlib / "socket.py"), co_name="__init__" + ), + f_locals={"fileno": 87}, + f_back=caller, + ) + info = SimpleNamespace( + st_mode=stat.S_IFREG if mode == "regular_fd" else stat.S_IFSOCK, + st_uid=8 if mode == "wrong_uid" else 7, + st_dev=1, + st_ino=2, + ) + obj = Mock() + with ( + patch.object(worker.sys, "_getframe", return_value=initializer), + patch.object(worker.os, "fstat", return_value=info), + patch.object(worker.os, "getuid", return_value=7), + ): + args = ( + obj, + int(worker.socket.AF_UNIX), + int(worker.socket.SOCK_STREAM), + 0, + ) + if mode == "accepted": + guard.socket_new(args) + self.assertIs(guard.accepted, obj) + self.assertTrue(guard.accepted_checked) + self.assertEqual((1, 2), guard.accepted_identity) + self.assertEqual(3, guard.socket_count) + guard.grant.assert_called_once_with( + "socket.__new__", endpoint, flags=0 + ) + else: + with self.assertRaises(worker.BootstrapError): + guard.socket_new(args) + guard.grant.assert_not_called() + + def test_actual_bootstrap_grant_lock_and_whole_ACK(self) -> None: + worker = self.bootstrap + path = worker.RUN_ROOT / "home/.truememory/model_server.lock" + for mode in ( + "valid", + "lock_refused", + "wrong_sequence", + "extra_field", + "terminal", + ): + with self.subTest(mode=mode): + guard = worker.AuditGuard.__new__(worker.AuditGuard) + guard.terminal = mode == "terminal" + guard.sequence = 0 + guard.approved = {} + guard.exchange_lock = Mock() + guard.exchange_lock.acquire.return_value = mode != "lock_refused" + answer = { + "type": "grant", + "version": 1, + "seq": 1, + "path": str(path), + "purpose": "Synthetic retained lock", + } + if mode == "wrong_sequence": + answer["seq"] = 2 + elif mode == "extra_field": + answer["extra"] = True + guard.protocol = Mock() + guard.protocol.receive.return_value = answer + if mode == "valid": + guard.grant("open", path, flags=os.O_CREAT | os.O_RDWR) + self.assertEqual({path: os.O_RDWR}, guard.approved) + else: + with self.assertRaises(worker.BootstrapError): + guard.grant("open", path, flags=os.O_CREAT | os.O_RDWR) + self.assertEqual({}, guard.approved) + if mode == "terminal": + guard.exchange_lock.acquire.assert_not_called() + guard.protocol.emit.assert_not_called() + else: + guard.exchange_lock.acquire.assert_called_once_with(timeout=0.1) + if mode in ("terminal", "lock_refused"): + guard.exchange_lock.release.assert_not_called() + guard.protocol.emit.assert_not_called() + else: + guard.exchange_lock.release.assert_called_once_with() + guard.protocol.receive.assert_called_once_with(2048, 0.1) + self.assertEqual(1, guard.protocol.emit.call_args.args[0]["seq"]) + + def test_actual_bootstrap_terminal_requires_ACK_before_hard_exit(self) -> None: + worker = self.bootstrap + for mode in ("valid", "wrong_case", "lock_refused"): + with self.subTest(mode=mode): + guard = worker.AuditGuard.__new__(worker.AuditGuard) + guard.terminal = False + guard.policy = self.policy + guard.exchange_lock = Mock() + guard.exchange_lock.acquire.return_value = mode != "lock_refused" + guard.protocol = Mock() + guard.protocol.receive.return_value = dict(self.policy.TERMINAL_ACK) + if mode == "wrong_case": + guard.protocol.receive.return_value["case"] = "other" + with patch.object( + worker.os, "_exit", side_effect=RuntimeError("Inert hard exit") + ) as hard_exit: + if mode == "valid": + with self.assertRaisesRegex(RuntimeError, "Inert hard exit"): + guard.terminal_result({"type": "result"}) + hard_exit.assert_called_once_with(73) + elif mode == "wrong_case": + with self.assertRaises(self.policy.SafetyError): + guard.terminal_result({"type": "result"}) + hard_exit.assert_not_called() + else: + with self.assertRaises(worker.BootstrapError): + guard.terminal_result({"type": "result"}) + hard_exit.assert_not_called() + if mode == "lock_refused": + guard.protocol.emit.assert_not_called() + else: + guard.protocol.receive.assert_called_once_with(2048, 0.1) + guard.exchange_lock.release.assert_called_once_with() + + def test_original_admission_and_attempt_guards(self) -> None: + policy = self.policy + now = 4000000000 + rows = [ + policy.Sensor(stamp, 1, 268435456, 5) + for stamp in (1000000000, 2000000000, 3500000000) + ] + policy.admission(rows, now) + for mode in ("free", "warning", "swap", "stale"): + changed = list(rows) + if mode == "free": + changed[-1] = policy.Sensor(3500000000, 1, 268435455, 5) + elif mode == "warning": + changed[-1] = policy.Sensor(3500000000, 2, 268435456, 5) + elif mode == "swap": + changed[-1] = policy.Sensor(3500000000, 1, 268435456, 6) + else: + changed[-1] = policy.Sensor(3000000000, 1, 268435456, 5) + with self.assertRaises(policy.SafetyError): + policy.admission(changed, now) + for value in ("v1", "v2", "v99"): + self.assertEqual(value, policy.validate_attempt_version(value)) + for value in ("v0", "v01", "v100", True): + with self.assertRaises(policy.SafetyError): + policy.validate_attempt_version(value) + + def test_static_worker_real_source_and_bounded_grant_terminal(self) -> None: + tree = ast.parse(BOOTSTRAP.read_text()) + text = BOOTSTRAP.read_text() + self.assertIn('importlib.import_module("truememory")', text) + self.assertIn("ModelServer(automatic_rss_budget=True)", text) + self.assertIn('_send_request({"op":"ping"},timeout=0.2)', text) + worker = next( + node + for node in tree.body + if isinstance(node, ast.FunctionDef) and node.name == "execute_case" + ) + worker_body = "\n".join( + ast.get_source_segment(text, node) for node in worker.body + ) + self.assertNotIn("types.ModuleType", worker_body) + self.assertIn("registry = dict(snapshot)", text) + self.assertIn('asdict(snapshot["os_enforcement"])', text) + self.assertIn('str(os.getpid()).encode("ascii")', text) + self.assertIn("exchange_lock.acquire(timeout=0.1)", text) + self.assertIn("self.policy.terminal_ACK_contract(answer)", text) + self.assertIn("os._exit(73)", text) + self.assertTrue( + any( + isinstance(node, ast.FunctionDef) and node.name == "socket_new" + for node in ast.walk(tree) + ) + ) + parent = PARENT.read_text() + self.assertIn("thread_sample_contract(process.num_threads())", parent) + self.assertIn('result.get("terminal_ACK_sent") is not True', parent) + self.assertNotIn("((signal.SIGTERM, 0.25)", parent) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-mac-native-package-acceptance-v4.py b/tests/test-mac-native-package-acceptance-v4.py new file mode 100644 index 00000000..8e3163a3 --- /dev/null +++ b/tests/test-mac-native-package-acceptance-v4.py @@ -0,0 +1,1098 @@ +"""Inert Stage B contracts; no child, model, socket or native limit execution.""" + +from __future__ import annotations + +import ast +import copy +import hashlib +import importlib.util +import os +import stat +import sys +import unittest +from pathlib import Path +from types import ModuleType, SimpleNamespace +from unittest.mock import Mock, patch + +PARENT = Path(__file__).parent / "native/mac-native-package-acceptance-v4.py" +BOOTSTRAP = Path(__file__).parent / "native/mac-native-package-bootstrap-v4.py" +PACKAGE_RAW = b'{\n "A_runtime_packet_path": ".work/reports/mac-native-runtime-pins-v1.json",\n "A_runtime_packet_sha256": "8db57e3f700aebb3adeb383c719a4bc5e2525917f4c017c853ee4694be1ee1e4",\n "OS": {\n "kernel_release": "25.6.0",\n "kernel_version": "Darwin Kernel Version 25.6.0: Tue Aug 18 17:49:54 PDT 2026; root:xnu-12377.161.15.700.19~2/RELEASE_ARM64_T8142",\n "machine": "arm64"\n },\n "all_publication_held": true,\n "created_utc": "2026-10-12T09:07:42.262934+00:00",\n "current_wheel_artifact_packet_path": ".work/reports/memory-installed-wheel-artifact-v2.json",\n "current_wheel_artifact_packet_sha256": "4b82143e4adaa5e26d81bbdfb32fc725d171bf0e466fe7ee0bfe14c1bc0c062f",\n "data": {\n "truememory/data/l3_weights.json": "5d91643f843b4639375a4324aa87cbd09815a4242b11f3fe4138bae641b2d5b8"\n },\n "data_map_sha256": "19d20af55c695fe0952cfc26e4fd339037ac3bbfe05fab4b14bb1ae814ca4ea2",\n "interpreter_linkage_packet_path": ".work/reports/mac-native-interpreter-linkage-v1.json",\n "interpreter_linkage_packet_sha256": "762d535cdb9f021729fe101c654966e52a9d8da285d5fe77bc0b1db815d43843",\n "interpreter_runtime_packet_path": ".work/reports/mac-native-interpreter-runtime-v1.json",\n "interpreter_runtime_packet_sha256": "73a77a325ba667ff84d395c8ecddd9d21d63be27914c4586c6d5e65e2ca55596",\n "live_service_queried": false,\n "metadata_identity_mode": "Actual current Linux py3-none-any wheel metadata staged privately; not installed Mac package",\n "model_loads": 0,\n "modules_imported": false,\n "native_GO": false,\n "numpy_MachO_dependency_names": {\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_multiarray_tests.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_multiarray_umath.cpython-312-darwin.so": [\n "/System/Library/Frameworks/Accelerate.framework/Versions/A/Accelerate",\n "/usr/lib/libc++.1.dylib",\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_operand_flag_tests.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_rational_tests.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_simd.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_struct_ufunc_tests.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_umath_tests.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/fft/_pocketfft_umath.cpython-312-darwin.so": [\n "/usr/lib/libc++.1.dylib",\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/linalg/_umath_linalg.cpython-312-darwin.so": [\n "/System/Library/Frameworks/Accelerate.framework/Versions/A/Accelerate",\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/linalg/lapack_lite.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib",\n "/System/Library/Frameworks/Accelerate.framework/Versions/A/Accelerate"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_bounded_integers.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_common.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_generator.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_mt19937.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_pcg64.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_philox.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_sfc64.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/bit_generator.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ],\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/mtrand.cpython-312-darwin.so": [\n "/usr/lib/libSystem.B.dylib"\n ]\n },\n "numpy_dependency_root": "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy",\n "numpy_files": {\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/__config__.py": {\n "bytes": 4867,\n "sha256": "062c14b924ddd2c1297088a9a9623607a90ab73c8bc56dd6087ca4bde928e739"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/__init__.py": {\n "bytes": 26277,\n "sha256": "92a46f791e453926d3292af2582b89995a289475f0eaaea71a949823200b838a"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_array_api_info.py": {\n "bytes": 10385,\n "sha256": "ce6ec789062aca14aa165dc9586ece2e1130f891843eaefeefea968575aa559f"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_configtool.py": {\n "bytes": 1007,\n "sha256": "105449de96b34f162113da29fa8716c8a4cb66bae91617e6992fed5ababc0b1a"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/__init__.py": {\n "bytes": 6662,\n "sha256": "837ce8aec8693095d2e1c7c306d385d1785a50be97a009935c64cae04e3555d2"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_add_newdocs.py": {\n "bytes": 216086,\n "sha256": "bdb68c6da2f3a1048cb2d6123afb6ede6f2bee88c88acb4d330c5485dc1324ce"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_add_newdocs_scalars.py": {\n "bytes": 12933,\n "sha256": "db6f2b889f9dcfd7d5df64ef3a430f532b3fdbfc279c3b94f8451b1757a16efb"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_asarray.py": {\n "bytes": 3894,\n "sha256": "f5aa779032cd51c8ca49039fae454fcebd2d64647513576d0884ff61e68525d2"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_dtype.py": {\n "bytes": 10547,\n "sha256": "70ce899e3a072d445608780df15990ca37a28838dcc210792ff7cf30e7b5bae3"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_dtype_ctypes.py": {\n "bytes": 3726,\n "sha256": "28f3e56a40ec3e4b938523abe6a1705b48c9f559e36ef5b811684e6c21d81881"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_exceptions.py": {\n "bytes": 5159,\n "sha256": "5fc120d61ab5b94f0bf7088ec05a368d1aba4b4be78c27606051e3de1016f42a"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_internal.py": {\n "bytes": 29437,\n "sha256": "c15b426aca730fccd5261139ee1a825abaf298d21d2522aa7f37338cd44598ee"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_methods.py": {\n "bytes": 9393,\n "sha256": "724facf7e63c5b8fcc2fac14bf31a02cb048d9e28f516d86502ba5077a425476"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_multiarray_tests.cpython-312-darwin.so": {\n "bytes": 120952,\n "sha256": "7eebe35f8c33291691312a8418d770da61837330588f966fbae8ad88099631eb"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_multiarray_umath.cpython-312-darwin.so": {\n "bytes": 3678408,\n "sha256": "b96123b811488473c23efa6d45f196641463c928372ff896785813b821aed652"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_operand_flag_tests.cpython-312-darwin.so": {\n "bytes": 51240,\n "sha256": "349c39c8494d2439e6732a0f8caf105a4737f21cbf1b962e1b07b36d853b58f6"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_rational_tests.cpython-312-darwin.so": {\n "bytes": 72808,\n "sha256": "0bfbea6b715e9c44c438b868b76e252178a478ecd094a1521d086165b55fdf22"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_simd.cpython-312-darwin.so": {\n "bytes": 339592,\n "sha256": "3178cd1f5ab8625fa8371bf48dbe54b4ed88896f9f70519c34ccaea970374197"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_string_helpers.py": {\n "bytes": 2845,\n "sha256": "e929a0a22ea80f60ae9e3c014abf41676d079029ef5ba9d33db953394de95a78"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_struct_ufunc_tests.cpython-312-darwin.so": {\n "bytes": 51480,\n "sha256": "9337744b95313d29845b741049a8cdaa8abe57520524e7f16c396b2ae64f1d32"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_type_aliases.py": {\n "bytes": 3755,\n "sha256": "a4d4a14f10ba6af2c1d912fbf8f38eb69150469e773bee5480e4f1962cf58296"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_ufunc_config.py": {\n "bytes": 15611,\n "sha256": "0e938bb63600619bfe9028e8f285f9ddcd925b62d0220690ec4a650a48ac45a5"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/_umath_tests.cpython-312-darwin.so": {\n "bytes": 71712,\n "sha256": "ba6ac034d6e710660862a3260c4e8d62171de92a9b3ea32029bc17774ea7f84f"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/arrayprint.py": {\n "bytes": 65271,\n "sha256": "f7a7c9a96b764436f675560f2dda3990d0d042ae8aa898d2baa40e7f5918209b"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/cversions.py": {\n "bytes": 347,\n "sha256": "1ff88d229c7dfa1635710371aa34f677fe525d98496cca3f71aab8feae8b07b2"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/defchararray.py": {\n "bytes": 37817,\n "sha256": "30b125c1f98ae7bea0cf9f80fbb49558867dece970d72e675ff7f0c6c84d22f7"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/einsumfunc.py": {\n "bytes": 57930,\n "sha256": "b91287b000d50c68c3be44597f9d968d4e18a967c729143aa5efe012bf4f847d"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/fromnumeric.py": {\n "bytes": 142658,\n "sha256": "6055d8e195cf7aaae417d23bda203cd35014e3db3d7c28a32e7eb71e858e945f"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/function_base.py": {\n "bytes": 19677,\n "sha256": "b8b8f508410c567b0ea4b44db18e28eb09c62e9ec33680fcefdea7de82eeb9c1"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/getlimits.py": {\n "bytes": 15003,\n "sha256": "46d3015808f29584e67eab482f52542a02fb6f0c95c2ca1bd386dc42b9a9ed72"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/memmap.py": {\n "bytes": 12651,\n "sha256": "c88b10ea7f64a59ba58204492459136e35709c1e257a8ca2cef529736894e27f"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/multiarray.py": {\n "bytes": 56786,\n "sha256": "ae9b35e60d0150e2ade05d45c1a31dbfeee627dbe668b2b7a1bfc2e3fe53f77f"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/numeric.py": {\n "bytes": 83038,\n "sha256": "d75e4d3072e59b477cad20f088d964f6b41c840e7304d3f76fe9f6832d760a9f"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/numerictypes.py": {\n "bytes": 15967,\n "sha256": "d7924b057d26fcc4206a21ffc81048e6096fd2f5ee794d1aae74b9e915df5319"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/overrides.py": {\n "bytes": 7480,\n "sha256": "2bdc18ac0e766e23be366f0605d0f382ea9272d2500e8baf03821e91395fc555"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/printoptions.py": {\n "bytes": 1056,\n "sha256": "345a6fcb96e78dbbea9ca7aa42dd2f784131a400180153688865c7de98255807"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/records.py": {\n "bytes": 36754,\n "sha256": "fd40158fe835806fa832a449440e04f414f72331b733825fc58dd48e6255330d"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/shape_base.py": {\n "bytes": 32712,\n "sha256": "1d6e897dfdb7edf2d71e8ed75254a6e7106b1a947592f88d91af96d1ad2c469e"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/strings.py": {\n "bytes": 50578,\n "sha256": "a9c5e0f420035ed0f3a5f51bd95307c0a78f3389d764f84da46a554e35097065"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_core/umath.py": {\n "bytes": 2140,\n "sha256": "450a42560a838a2260e85f23c8398b7d09e7a8d8dd4170376e0d920463060c0b"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_distributor_init.py": {\n "bytes": 421,\n "sha256": "14148976054795071ae41ad011560fa059ba2924c98481675ad59b1241214d2a"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_expired_attrs_2_0.py": {\n "bytes": 3771,\n "sha256": "735b4637a636441b0324e9498dcff127231ea3a1d321d939c75048c6dfab2684"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_globals.py": {\n "bytes": 4155,\n "sha256": "d37b1eb1df7d7ae55fdc2bd9d757631344be0bacfe56d8456062b60b23e3722b"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_pyinstaller/__init__.py": {\n "bytes": 0,\n "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_pyinstaller/hook-numpy.py": {\n "bytes": 1362,\n "sha256": "314db6a50e0091460f416bb90bca510e99d85c494b27c4741463582544298951"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_pytesttester.py": {\n "bytes": 6328,\n "sha256": "0e39582fcb8834dd9759ade79e55fa80f1aea0b8dcc750627bf3d0cdba767290"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_typing/__init__.py": {\n "bytes": 5466,\n "sha256": "33034ac192a99a0eecf9babd14db781811274926d04a6923f92a71b66329a229"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_typing/_add_docstring.py": {\n "bytes": 3999,\n "sha256": "ff783b0fee87010dcc4f85fa0c4d3bc8bb9af4ba96161b24355c754d2ed7f4ee"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_typing/_array_like.py": {\n "bytes": 4188,\n "sha256": "10f6547c94a36a6bec589e91b39670c00ff916106961da227dc555b557163e7d"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_typing/_char_codes.py": {\n "bytes": 8764,\n "sha256": "8f4ee7a64f3635bec8adadb3ed94e553751c38fc2dda033ba992ab3cb7634f8f"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_typing/_dtype_like.py": {\n "bytes": 3907,\n "sha256": "1a299be0d4de9405780e7c09bceebd8aad17f7fbdd438071433dd0232632f4c1"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_typing/_extended_precision.py": {\n "bytes": 434,\n "sha256": "a649d4aa06a4d0504d33eb0444fa96fa91461fbd7f2be89e84549e7b9a108aa1"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_typing/_nbit.py": {\n "bytes": 632,\n "sha256": "2926cac0e2adb686fee72b53e6f095907ac3327d181efca9b534f28ffe8061cc"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_typing/_nbit_base.py": {\n "bytes": 3058,\n "sha256": "9cf669b1096db91e41d22680605f6a0f685efe45e79acb2ffd185a50d16c5beb"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_typing/_nested_sequence.py": {\n "bytes": 2639,\n "sha256": "c2678328b566bc49c1f5e61d77f982355584a1caa6068f0686929661018541ec"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_typing/_scalars.py": {\n "bytes": 944,\n "sha256": "2e15d8d814c799e60acde2197e98efb8c9fee5e38b8d4da7f73ef3d65e5b29ff"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_typing/_shape.py": {\n "bytes": 275,\n "sha256": "e9c16ff8b6d2c86f6695f4813861b2ba5f50fc652b95c1d00bd2596be9b6d1c0"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_typing/_ufunc.py": {\n "bytes": 156,\n "sha256": "1ce91a13eeb05747dddeb9876460b7f580072087fcb72be5cde903e32e064310"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_utils/__init__.py": {\n "bytes": 3477,\n "sha256": "8559d9ec2d0c09235b330f99caaf8c282612b5a197e91cea14c9e787b79de1d1"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_utils/_convertions.py": {\n "bytes": 329,\n "sha256": "d3133175e2cece20e61ec44cffc96e121e12fa441d328320e86c4d0c36acebd9"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_utils/_inspect.py": {\n "bytes": 7436,\n "sha256": "cc5b890011f4f03d4a82afde79c6240f53a0834397a75b78a2a8d9c4cd2474b9"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/_utils/_pep440.py": {\n "bytes": 13988,\n "sha256": "8adf4fe3fa075d6c37071768573ec93ccba3e64c45e4ceff049f19d66f67bb4c"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/char/__init__.py": {\n "bytes": 93,\n "sha256": "c6cea9a6b31d98d79755fb934645379c5f6a761bad59d3eee6869ea760235a67"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/conftest.py": {\n "bytes": 8652,\n "sha256": "3193ef75f63f7ccd1bf9c303be34026ab6a3a3c8c4bab62f3647eb485abf1c1d"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/__init__.py": {\n "bytes": 1290,\n "sha256": "c0935a445d5414e9d92aa881aecb2158b8d31a2119f6bbd695c8add318fb634c"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/_dtype.py": {\n "bytes": 323,\n "sha256": "1870617d5b6c56b3fbbf5dc8ba3133f5a18810d91821d6dfb91bbe35ec355275"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/_dtype_ctypes.py": {\n "bytes": 351,\n "sha256": "c17e26dfb6f4cd08319734f93a313fba3d84e42a625fd13b1cb1693ba87f9436"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/_internal.py": {\n "bytes": 949,\n "sha256": "ab1a472442d735471826426dd4b92d42e666e01c18bb86d79ccb8110ea7a3ce5"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/_multiarray_umath.py": {\n "bytes": 2098,\n "sha256": "4fcf07660143e550ae5d109278b6cfa23f7d9ca512760cb0f315b27fa7aa8714"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/_utils.py": {\n "bytes": 923,\n "sha256": "e5f935f09378dd183a607bda9fa42376b39e3ae2ed4652d598fe8c69d0442550"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/arrayprint.py": {\n "bytes": 339,\n "sha256": "2db7b8b26597605cddf6c3bd2cb2793d94b80b76d2bcbb7a1d1b70484e7ac4df"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/defchararray.py": {\n "bytes": 347,\n "sha256": "6bd96ebef9e2f2046b19574a3bb53fc70b0516f9339719d58312fbe632d19822"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/einsumfunc.py": {\n "bytes": 339,\n "sha256": "08db9c20d81422ba622d09f8c4f23f9a88308df2a5140de1ee0c00bd15706f93"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/fromnumeric.py": {\n "bytes": 343,\n "sha256": "e536a89c956e0b5d74aafddfddca93b668dac935f4066a89020a00267e47dd92"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/function_base.py": {\n "bytes": 351,\n "sha256": "be18e1cec1330ddd7544783aa6295f3093b75fa93de1a9f944026a8fe9e5ce6b"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/getlimits.py": {\n "bytes": 335,\n "sha256": "ea70a4e13c342e35bb8e85aca6b2392e23338ded60b0e8ceda5490fcec0107c2"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/multiarray.py": {\n "bytes": 793,\n "sha256": "6e374f2dbbc9ba3eb533a4d991b0793573823661f841b52aea0dde3e428fe930"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/numeric.py": {\n "bytes": 360,\n "sha256": "0ad93f42293207698cd31234941781f184d47d3c104977d576920c46dba76cca"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/numerictypes.py": {\n "bytes": 347,\n "sha256": "6d7c13c3351a8736c7ac51a14b946424ebdbe93604b109d00b9c30f6637ed55c"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/overrides.py": {\n "bytes": 335,\n "sha256": "d456726f453a249bb2a23b7116f43b1d2276ae97e159e73417e5f499aa428dcf"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/records.py": {\n "bytes": 327,\n "sha256": "f727c50f1c8e73af255ea7db6a8b2044d970d5d6d63fc0911f320f12d12d4a07"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/shape_base.py": {\n "bytes": 339,\n "sha256": "dacadd42d17577c2e951b0e318c5d3f93ab3d8ad8d6933be6440b8be2098b306"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/core/umath.py": {\n "bytes": 319,\n "sha256": "84c56636b20276a5d18a2446ed4315d06afef7162a2469a438d1909f6d222bdf"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/ctypeslib/__init__.py": {\n "bytes": 193,\n "sha256": "585c0c8695762c93fe2103aca5a227855f1ce973ca670aa9a44f9cbed229aaf5"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/ctypeslib/_ctypeslib.py": {\n "bytes": 19079,\n "sha256": "36d114a62b108437c44c12c092a61fec08ead3188d859bab6f94a61861f9e290"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/doc/ufuncs.py": {\n "bytes": 5414,\n "sha256": "f71b7c1f7e0686b5eb16af5c585506bc915e3daf58b87f53ab50f30279b61361"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/dtypes.py": {\n "bytes": 1312,\n "sha256": "cee3f0802d228c5da80d100e27a23d24a85a26e857140ca00722d06a856d4f9e"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/exceptions.py": {\n "bytes": 7709,\n "sha256": "df16e967d97b1779a1c2dca6be5d2fa84e346a237c6e471e6a8f1a5df66c9795"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/__init__.py": {\n "bytes": 2448,\n "sha256": "7008141d680941966c7eff1ca3c28136bfe6f01e9fa7374169436f25e05ffcda"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/__main__.py": {\n "bytes": 130,\n "sha256": "ea2da3547d9f3eb895d5aa1c4d8fdd505bd62b5f2a6bece3a6721203e3a9177c"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/__version__.py": {\n "bytes": 48,\n "sha256": "f7d4ba9927afba1c0698ef64bbefbb55425e921374a2de3e2747192a21dcaa9c"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/_backends/__init__.py": {\n "bytes": 299,\n "sha256": "eff6c0edcff10e92dce3ff2f3df1f7dbe2f19fd7dc513823436e6caddbf0bc03"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/_backends/_backend.py": {\n "bytes": 1151,\n "sha256": "a055d9f3e57071049b96d97cfe98162cfa82399f18fe9c7ba1e4e4fc19712537"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/_backends/_distutils.py": {\n "bytes": 2385,\n "sha256": "8444f4581e2acbe0f80739de906016864f78e64e707aada51940d1ea1ae25cca"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/_backends/_meson.py": {\n "bytes": 8626,\n "sha256": "398d3089f27dddd148e357c1aba0ddb2f9d0fdeac0e6aba2c6fa7ad1eb5a888e"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/_isocbind.py": {\n "bytes": 2360,\n "sha256": "cda060a5f3cd466c551b776850895b6488b207df7432c5e2c0369bad28fd1e74"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/_src_pyf.py": {\n "bytes": 7695,\n "sha256": "3c7a68f43dbc2aadeaff7f8a157f03dec143f5e5fc0357372ee2cdcd7cc1c8ec"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/auxfuncs.py": {\n "bytes": 26920,\n "sha256": "767694c2b740bf8f8b6c488735b4b5beb8d03423b4941bb25a48f746dfd48b31"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/capi_maps.py": {\n "bytes": 30083,\n "sha256": "fc3a1b487a90a24faa4ea2299923493c0ef37f4006316426a1b090d72ec735c7"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/cb_rules.py": {\n "bytes": 25051,\n "sha256": "e8a6cfbbdc9f27eee901adb8223f47df82e5d7941cf025d3a8216250923a47c6"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/cfuncs.py": {\n "bytes": 52660,\n "sha256": "e09e0fd76a06a725991dbd4054a025ed827741482783018c0a7075221ac99fb5"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/common_rules.py": {\n "bytes": 5030,\n "sha256": "ffdcb32289493068297770fde0b741b0e0e77d4b2431182ddafd37ac40881c94"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/crackfortran.py": {\n "bytes": 146861,\n "sha256": "642ff279b193b0600ce150718cc18577d6ff9ac5e4f167e694c9ce516034c41d"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/diagnose.py": {\n "bytes": 5075,\n "sha256": "616363d6f33af1ee3b2dbdbbd30959939cab714fb24e5cc669834f059ee74c05"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/f2py2e.py": {\n "bytes": 28827,\n "sha256": "eb0439f72fc8ab8530f5461d129265c9ba52dbf6ffc3da04b60c253c270272c3"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/f90mod_rules.py": {\n "bytes": 9810,\n "sha256": "ed9e6fa2b538c215f8d39c4c2faea1af88b589c09473d82be9a9f847e00c87b3"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/func2subr.py": {\n "bytes": 10045,\n "sha256": "0d752c605d9327127de7beb3fcc927e3bf584bda6cb56b0a52dbbdfc9e858c1f"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/rules.py": {\n "bytes": 63094,\n "sha256": "f574d1eec4a0c245129e07119ea77ea4ddd86aad6cc230753ebacd8fb602a872"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/symbolic.py": {\n "bytes": 53310,\n "sha256": "a83d5d2d5d592ecb881814f794736c91abdbdfd866c2ee0311a934916928e681"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/f2py/use_rules.py": {\n "bytes": 3376,\n "sha256": "7a666148b3db343c811e7b1f28a5c39c6cf83ff830ae02f475f099703de7f43e"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/fft/__init__.py": {\n "bytes": 8156,\n "sha256": "251bd35d9a814b98e076ab2e0da2a5a7eabbfb11427a7e873ae127d5a26137c0"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/fft/_helper.py": {\n "bytes": 6787,\n "sha256": "8489f66721181b87cb077306bc23efa52acb5c57e1fb13b8cc62a58e4fd34236"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/fft/_pocketfft.py": {\n "bytes": 62599,\n "sha256": "8c4a13d3b123e678aaccacaf67c541935a7b80dd55283f2aca81bf315d02a3fa"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/fft/_pocketfft_umath.cpython-312-darwin.so": {\n "bytes": 329912,\n "sha256": "d902fbb947127a8e78f56d67057fad07f73563a91a60a761423e3ba09ffb8731"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/__init__.py": {\n "bytes": 3004,\n "sha256": "cd81aea847cfaaaecb0db89da7162cf068023409a8278c50805bc5dd7289e518"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_array_utils_impl.py": {\n "bytes": 1697,\n "sha256": "1985a2c8da8b43b0c6512057cf46db47a000a994ad0c8c147bbb6c6d9fffd793"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_arraypad_impl.py": {\n "bytes": 33519,\n "sha256": "69f17907431860505f879f7ac0f69be927464119e4068583b5f84f11fee63131"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_arraysetops_impl.py": {\n "bytes": 37341,\n "sha256": "9f896cc0ddc2f4f94ed7cf0b28fa5dba777ec77df3c44788afcd6448d55416b4"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_arrayterator_impl.py": {\n "bytes": 7218,\n "sha256": "1ed3800c81ee1bd0036db3138268783ff9ae91ed55fbc13e14d10eddb54e18f0"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_datasource.py": {\n "bytes": 22731,\n "sha256": "ce4f956e7e099431c455adc37ba0373608e79e2cd2262f8717466fbc0e98228e"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_format_impl.py": {\n "bytes": 36884,\n "sha256": "2a90cc1f9fc635b13815c166b03eda58e1fb11f8ac511e4d9b6d67d147e83691"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_function_base_impl.py": {\n "bytes": 193738,\n "sha256": "b2310fd26d09b566bc50c94be4d3032866503c16a75a5ff67ae994a4a7aea7c9"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_histograms_impl.py": {\n "bytes": 38432,\n "sha256": "52dbbb68041ced9a6c1e7ff4e288149d9ab565c7477e2a5cabd791abcd7ba155"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_index_tricks_impl.py": {\n "bytes": 31519,\n "sha256": "0cde783785ad86729ac3dc079a9f77ae8e1abb7d11d2cde9e35447ec91463e2b"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_iotools.py": {\n "bytes": 30876,\n "sha256": "d23b69be99792feff5383236d45fb58b5f6dd5edcbfbac09c517750922dec0bd"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_nanfunctions_impl.py": {\n "bytes": 71399,\n "sha256": "62d1c87a064ab166c6795a192ea7dd4004797f7b63d7a9a33c86cec6ca05e4b4"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_npyio_impl.py": {\n "bytes": 98636,\n "sha256": "557dbdca775fdf05ab660f6694c5d25194634426ddb2a22a7e1706828dc83b82"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_polynomial_impl.py": {\n "bytes": 44125,\n "sha256": "048794e3f17a3923deb56fdfa30544f29d03775bafb13bb57c9737b928130521"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_scimath_impl.py": {\n "bytes": 15684,\n "sha256": "6f9ffec91cd62483bc75afd9e01f2a41eebc8fc9693bc82db4696f18da65cadc"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_shape_base_impl.py": {\n "bytes": 39013,\n "sha256": "c9858af506f79e40dae1a4fee13900a06216aa78b95813d0a952156c7c37b311"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_stride_tricks_impl.py": {\n "bytes": 19115,\n "sha256": "d4ff05ee3167787eff06d5fbe0ebdc23a48d9f6890a733e348aa266e1355366c"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_twodim_base_impl.py": {\n "bytes": 33923,\n "sha256": "0fe52393e90c6565cc23ac1b8ba44fb347a02e084827309ece0468ac88b085c8"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_type_check_impl.py": {\n "bytes": 19914,\n "sha256": "d2b5b3bcb84c16ab9ddbeca1e7c45515063c0f61a98146039a77e99782b7b563"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_ufunclike_impl.py": {\n "bytes": 6014,\n "sha256": "2987a20d544c11fcfdf243a4c3e5d1cb47be8eb4f38c238399d946252afb8cd3"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_user_array_impl.py": {\n "bytes": 8026,\n "sha256": "65c91b14c34cdbc1136e97b5d9a6262780b58917a2a1233e4ded8dde217f5195"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_utils_impl.py": {\n "bytes": 23499,\n "sha256": "96db16f4c329de4d6fc3b22789d8584f97e8f60ae0c33eb6e1fbd17a21c3c967"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/_version.py": {\n "bytes": 4783,\n "sha256": "0affabe9bf73540e9b0febf2158f2d09746ef5de4eb069e3a976a57d8d9194e0"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/array_utils.py": {\n "bytes": 144,\n "sha256": "5db732849f52d0894d9cff4dcbacb280b30400259650f34e53cbdebe3d531292"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/format.py": {\n "bytes": 477,\n "sha256": "9e9274789853eee28d2b96b49423067e226ff91e23c8d86220f7996c970d5c1b"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/introspect.py": {\n "bytes": 2749,\n "sha256": "e97a1b86c7a928352ff339a83e47d53391206421afeb53f63ca97f6916c49074"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/mixins.py": {\n "bytes": 7195,\n "sha256": "cd047e8888c2492796c5ea4ae62fbbba34967f0163ad467fab18deac88a4c3a6"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/npyio.py": {\n "bytes": 68,\n "sha256": "79a3ef7c7192cd413bd132471cb3823c85fd1b98a132e0447b1cbafcf36127d4"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/recfunctions.py": {\n "bytes": 59539,\n "sha256": "4f869ae715dabfd9ed7f0e589b33e2abf614921d76c06938d17c812d03c21335"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/scimath.py": {\n "bytes": 169,\n "sha256": "aa315a41eab4cc4225ee02aa3a16a3fef9820bf29a15dc9398774b514912a792"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/stride_tricks.py": {\n "bytes": 88,\n "sha256": "c74fc17f097270102547706fa714e775e3fde9d1c14ff7c049b9134d3cc16202"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/lib/user_array.py": {\n "bytes": 63,\n "sha256": "ceceaee93017a00c9218065cd6a13a7ca0f800dfadeaead909a89968a9878aca"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/linalg/__init__.py": {\n "bytes": 2076,\n "sha256": "b50025b9aa09f4e77b1a67d7c03b8915a86cc1549179084da21a1a1a5d613d02"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/linalg/_linalg.py": {\n "bytes": 114861,\n "sha256": "e47d0e2aed21361291cd6651999a59e6479ba5866abb8eabb1dbea5b0d1618ac"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/linalg/_umath_linalg.cpython-312-darwin.so": {\n "bytes": 154848,\n "sha256": "86f2c3a5e1a516211e144ba13d90c18b9db903477fa66154ee5cdc4cc59c3067"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/linalg/lapack_lite.cpython-312-darwin.so": {\n "bytes": 53008,\n "sha256": "3f67c0ee45da7ae027d4e066495953574d9a901fe1304d9ddfbba79397a5d054"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/ma/__init__.py": {\n "bytes": 1406,\n "sha256": "5e90d6617c1ab83738f7e20db24e39e43d37530e29eb115db1d58e9f6aedf3b5"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/ma/core.py": {\n "bytes": 289255,\n "sha256": "9970fa92caa4f0c10ab51362b6347fe75d807461f98f931ef7d6daa9f30b003b"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/ma/extras.py": {\n "bytes": 67790,\n "sha256": "0089da9a60816d1bfae48d67d4bd416c54a6bdd42e6470e7ad9d68a2b1a84c81"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/ma/mrecords.py": {\n "bytes": 26486,\n "sha256": "01e5c14904591fd9946ad35ace8f23c661d3b8bf0e35ea3500c9476dab5645c4"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/ma/testutils.py": {\n "bytes": 10213,\n "sha256": "1d69f3dacc7244e58aedfadcd5a4768183cc36c4d4004e5e4e0b1c8d3268361c"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/matlib.py": {\n "bytes": 10638,\n "sha256": "fd2f4df12d8d36c1c6414725a31af1001b490de8c7894c8c74c24eed26b23e40"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/matrixlib/__init__.py": {\n "bytes": 243,\n "sha256": "52de88a9f8ee03e930c28e8704e6008058d75c2fe1ed857fdc7c83b0a33bd9d9"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/matrixlib/defmatrix.py": {\n "bytes": 30875,\n "sha256": "c29c3a95953d5faaa9f30009a240d7b76441acbd5e5ea96606dfa88c8c18ce55"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/polynomial/__init__.py": {\n "bytes": 6726,\n "sha256": "8064b02cda4f0a95df3e08894ac815a15b09d004b573efcc5a518e7a21b9e6c2"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/polynomial/_polybase.py": {\n "bytes": 39358,\n "sha256": "6f49028938149bc0f9402fcb5929bac8dbf00bbf67a4301e92c2b4bd03dc8824"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/polynomial/chebyshev.py": {\n "bytes": 62286,\n "sha256": "1621159a3acd55b85c6301b951012af66a910584cf86f1ab902ff2e80b314688"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/polynomial/hermite.py": {\n "bytes": 54571,\n "sha256": "8bc58c5e0038edc2792e214a8077556299a14326abbaceda26bb04debe096e6a"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/polynomial/hermite_e.py": {\n "bytes": 52273,\n "sha256": "244c341316423611ca5a98fb0b948a3f7dd04ebe88e307a04d4c8fbbd50ac8a3"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/polynomial/laguerre.py": {\n "bytes": 52447,\n "sha256": "2d8568238e6285355703b4af0dff07fcaac0bff2fac055dd0fbba3f830f6d7eb"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/polynomial/legendre.py": {\n "bytes": 51102,\n "sha256": "65667892aeceb7e239982303ed6cb910a06d23c8dcf3caac74f1b2d0c65a160f"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/polynomial/polynomial.py": {\n "bytes": 52669,\n "sha256": "9da743f753387a41e92d56c6c5763307e83b8932bdc8ce039408e0f7926c8cff"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/polynomial/polyutils.py": {\n "bytes": 22635,\n "sha256": "68064957a6f465962e2b520a0cf1e188981299757f7948a48d7714a132c7c3b6"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/__init__.py": {\n "bytes": 7480,\n "sha256": "585ce7b73b5454d6a25c2a50967f2dc322fc1d214d4bb5c0589949b105e06ea9"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_bounded_integers.cpython-312-darwin.so": {\n "bytes": 279912,\n "sha256": "0e08cf005eb9fb8378086928485ce30cfb12cd2bba411b6dd7202124734c2b75"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_common.cpython-312-darwin.so": {\n "bytes": 232000,\n "sha256": "ecd16f24eea4c44bf28cb718300f4156cc99c1725dbeda82bb447066976cacdd"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_examples/cffi/extending.py": {\n "bytes": 884,\n "sha256": "8e920bd678cc85fd277a19a53079206642319ecd890bd1840d88000a15fcec6f"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_examples/cffi/parse.py": {\n "bytes": 1750,\n "sha256": "3caf6f754c709af76716f1f7acea609e7a484b09e277bae2e573a606b316a49c"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_examples/numba/extending.py": {\n "bytes": 1959,\n "sha256": "67b67f5e9ec73c4e0ae4167b030a59dbdaafb9fb45024be132d357e77b6530cc"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_examples/numba/extending_distributions.py": {\n "bytes": 2036,\n "sha256": "7dd78f5de523e3ac972b430ad5cb33c541d088e4e236e36cadb66a3f0e00746b"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_generator.cpython-312-darwin.so": {\n "bytes": 651216,\n "sha256": "391cf5d6d41959c14abe10d1108a6bfe664dd12677ae9f88abf57d35447524a8"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_mt19937.cpython-312-darwin.so": {\n "bytes": 132176,\n "sha256": "94083b8c1e0d99556bc732b8a59ffc273cbcb981836da95459919f3158faf475"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_pcg64.cpython-312-darwin.so": {\n "bytes": 133456,\n "sha256": "0230f385549bca10636d49991e15c021ced23c705bedde086fc72a6276c611b4"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_philox.cpython-312-darwin.so": {\n "bytes": 115648,\n "sha256": "9cebd1d7e17a5d9296bd1b6b1769466f71e773a31bf5505958cc2723af601730"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_pickle.py": {\n "bytes": 2742,\n "sha256": "2ede3b99afef9e72477674257398d9fc3a811ec74a8b442250d68890c7fde6a0"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/_sfc64.cpython-312-darwin.so": {\n "bytes": 97328,\n "sha256": "359fbd1de581fc5d26d87c9596675f9ceb35e394cc63d47812d4e6f73c5866ca"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/bit_generator.cpython-312-darwin.so": {\n "bytes": 189920,\n "sha256": "8a1bba22fafd2ed834c3cd7e95cc2a5a4abc09fe27004949d18e04ab2dd4d277"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/random/mtrand.cpython-312-darwin.so": {\n "bytes": 527616,\n "sha256": "8152e184801e01d2c92e547bece71b1eba501d6bfce6fbd3308575852e50055c"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/rec/__init__.py": {\n "bytes": 83,\n "sha256": "90d0186284800348b3a545516fed7cb09c3e88ab45ab6465d94e52387de91d13"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/strings/__init__.py": {\n "bytes": 83,\n "sha256": "a36ef01d6f2319a51f6c3a294b2126603e918dea37dc0ce4181053816ae5187e"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/testing/__init__.py": {\n "bytes": 581,\n "sha256": "12a7be3b1fb7252aa4e904679cf3c52c25bd224aaff4eb890e19e6dae18cdf37"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/testing/_private/__init__.py": {\n "bytes": 0,\n "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/testing/_private/extbuild.py": {\n "bytes": 7716,\n "sha256": "2e0d6ca80f7843be0a8b9bbeb31d0f123eeeaebdd83f8ef4f810a2caf2701314"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/testing/_private/utils.py": {\n "bytes": 98720,\n "sha256": "0514e16f2db0215fed604fb3015bae396ce9ff20476c27ec395d1e7bd0786491"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/testing/overrides.py": {\n "bytes": 2134,\n "sha256": "07c63c3e5a6f2bbd48712ba8b9b5d68f82f935598b5529fb58c8352fbc593bc9"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/testing/print_coercion_tables.py": {\n "bytes": 6286,\n "sha256": "49ba0d9822dce45c95f9447c80a8c97363c8a233755d04ef1f45b30eaef93363"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/typing/__init__.py": {\n "bytes": 6984,\n "sha256": "658ba47953a80a0da7db6408a1a78dfda3cd538228a58dcbbf8540f66e284007"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/typing/mypy_plugin.py": {\n "bytes": 6846,\n "sha256": "1d7ad11a80a461471cb56e06213a01c7fa973df6721b617723a4ef9da2c54a58"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy/version.py": {\n "bytes": 293,\n "sha256": "833eb1051581de6bf4d23cd6f090e0fa4cbab6c548463dc718b5be62920fa2d7"\n }\n },\n "numpy_metadata": {\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy-2.4.6.dist-info/METADATA": {\n "bytes": 6608,\n "sha256": "b082c52ccbe3b880bae177c35171b52ed6c947f3ad71e3762f2723f60a411de0"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy-2.4.6.dist-info/RECORD": {\n "bytes": 110004,\n "sha256": "badf350b4082ff0798860f09e89c00ecbb809948d453b1c54b5ddb951d7eee45"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/numpy-2.4.6.dist-info/WHEEL": {\n "bytes": 122,\n "sha256": "26726246f91715a154e07973ca8a82029674ee3cba86914a77f2e47c1a566739"\n }\n },\n "numpy_total_bytes": 10961565,\n "numpy_version": "2.4.6",\n "original_truememory_source_config_data_read": false,\n "original_venv_written": false,\n "psutil_metadata": {\n "bytes": 22187,\n "path": "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil-7.2.2.dist-info/METADATA",\n "sha256": "525f8ba78523cf7bc3759d62a105e7c57fd679ea702402b9449fb0746aa0217f"\n },\n "psutil_pins": {\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/__init__.py": {\n "bytes": 89878,\n "sha256": "d138a5786b163b56ba86ea0b2d5589dfca37e1bcdf8de1057fe1e933d6ab808a"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_common.py": {\n "bytes": 25254,\n "sha256": "1bbce9fd97e6f5439a0d385f28401f527c5a9d978d9d062593d7ef6fe215fc75"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_ntuples.py": {\n "bytes": 10970,\n "sha256": "9b8e8b938abae2786a515f686f4d8132218be897eb6b459b9324ed17d88a31c7"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_psaix.py": {\n "bytes": 17616,\n "sha256": "afe4038d5e1454559f17dac9e6c2c56ad8cf873d43880f6503e8549cc06a18ec"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_psbsd.py": {\n "bytes": 29263,\n "sha256": "c1a8314a9640649c99197ff261dbeec1f0080175e4a3f6b5cb370a39332deb94"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_pslinux.py": {\n "bytes": 84787,\n "sha256": "a8a09bab87848075b691c46112b00692745785a8c9d5a2c87037ae124b469749"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_psosx.py": {\n "bytes": 15913,\n "sha256": "89f0ea0e9c1fe38f224b612792bf31d07c8880d79281806e5379c00514f6b3f2"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_psposix.py": {\n "bytes": 11575,\n "sha256": "288887b58f5939e3b00e021f6e7b490c2f7f4852804a359918ab7d9c6fb39839"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_pssunos.py": {\n "bytes": 23934,\n "sha256": "cc348442008a54cb6054299b2ecae0d9a123c07bb4ba1f8e14b319487f83cee5"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_psutil_osx.abi3.so": {\n "bytes": 105920,\n "sha256": "ff4568b9187ca7b37c5134aaf614ffb061e7fedccebd84158343afb05991b8e2"\n },\n "/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_pswindows.py": {\n "bytes": 35370,\n "sha256": "580ad391de734c113682200ee9f4245e34737f5e26af0a73a4e8c9b4458de220"\n }\n },\n "python_realpath": "/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/bin/python3.12",\n "python_sha256": "ae711ab41ea1e6d9c2daa0ef816a0e041f9388ffd7c5bccb7d167d5b8afacfaa",\n "python_version": "3.12.12",\n "runtime_closure_complete": false,\n "runtime_trust_policy": "Exact pinned package/NumPy/psutil/interpreter/framework/native dependencies; exact canonical trusted-unhashed stdlib Python and observed OS loader inputs. Qualified source proof, no native security sandbox",\n "schema": 2,\n "source_bytes": {\n "truememory/__init__.py": 6932,\n "truememory/_platform.py": 5947,\n "truememory/client.py": 13639,\n "truememory/clustering.py": 22412,\n "truememory/consolidation.py": 62951,\n "truememory/embedding_target.py": 5225,\n "truememory/engine.py": 174355,\n "truememory/fts_search.py": 10285,\n "truememory/hybrid.py": 12551,\n "truememory/hyde.py": 8143,\n "truememory/maintenance.py": 137455,\n "truememory/memory_budget.py": 41094,\n "truememory/model_client.py": 34164,\n "truememory/model_server.py": 100943,\n "truememory/personality.py": 68823,\n "truememory/personality_style_vec.py": 21995,\n "truememory/predictive.py": 18593,\n "truememory/query_classifier.py": 5045,\n "truememory/rebuild_source.py": 19160,\n "truememory/reranker.py": 25401,\n "truememory/salience.py": 20238,\n "truememory/storage.py": 68710,\n "truememory/temporal.py": 49851,\n "truememory/tier_config.py": 6988,\n "truememory/tier_switch/__init__.py": 319,\n "truememory/tier_switch/activation.py": 33404,\n "truememory/tier_switch/cache.py": 7779,\n "truememory/tier_switch/job.py": 25028,\n "truememory/tier_switch/manager.py": 47952,\n "truememory/tier_switch/runtime.py": 38531,\n "truememory/tier_switch/sensors.py": 4336,\n "truememory/tier_switch/serving.py": 18642,\n "truememory/tier_switch/source.py": 28269,\n "truememory/tier_switch/state_machine.py": 5509,\n "truememory/tier_switch/throttler.py": 14364,\n "truememory/tier_switch/worker.py": 27782,\n "truememory/vector_search.py": 96703\n },\n "source_commit": "aeecf25f78279629789d98a29bbd8bf817233391",\n "source_map_sha256": "e1cbacd6cdfa558110647572b614d0d3983413af3dc988530a30b56d6a06cbd6",\n "source_root": "/Users/j/dev/truememory-maintenance-v1/.work/worktrees/whole-process-memory-297-v1",\n "sources": {\n "truememory/__init__.py": "00d3ab14540092b63b449d14533b17292018ae4a69cd3f3ef1345e1a02b54d98",\n "truememory/_platform.py": "0688763a6fc26ef8db2b87866aabd5a2373a18f4ee735859cc6d595147e310c0",\n "truememory/client.py": "f020ac930617cd5d4848775ddd7390a61665e0fbdc62d51ccaf19bccdeeb3a33",\n "truememory/clustering.py": "ea9c2dbb7d083217983bb962f696e7bfcf08359ba5969f916fa63241baa535a5",\n "truememory/consolidation.py": "dcff2bf4c8f80315453b5078cdae575e4414adf879b69ef8f7983e72478fd474",\n "truememory/embedding_target.py": "da61626141f1ca5587eaa2fa3c27381f80834e79bc32351b5d7a3c20e86c7ddc",\n "truememory/engine.py": "898f2b8dd79e8ca18154f8e4b3ef0cbfe785c27b543719f975f4a1f2cf0a010c",\n "truememory/fts_search.py": "9c954bd2513ea8e3c82b33e0fbc311cd5b7346d12ee70da011df3f4244dc4389",\n "truememory/hybrid.py": "17b281113b2927c6e63088c0d48dc34c2681a9943e1d4f4aa4d0a70d1c36dd17",\n "truememory/hyde.py": "127e4d1c01767e13c0d963d6878f01a7329bb749b4510e831ef51c9c879276e4",\n "truememory/maintenance.py": "413ffbc08f92ba3ed331cab5e0aebd67ba2cadf2e5400f8b5c09c47f80521819",\n "truememory/memory_budget.py": "2d2578554d1c029c5205ac1fd27511f30e3d372a1178bf88cb7b81b0501409a4",\n "truememory/model_client.py": "a8a9332ab7cf532c98594f4f7455642f00d6235d9ca8a4552a75c68a49b7d0f3",\n "truememory/model_server.py": "424986a108d0ecd9e984ed77e352fba36305bf9aa3d79517de8fd1cfa4f8701b",\n "truememory/personality.py": "4a55a594360435bd56152738af061988b73e3374c3e8757e934c0b88cf4cd2af",\n "truememory/personality_style_vec.py": "f294e67b46ce4473b2cf15a49c80fc348de27f5236834dfbec832fc092c5362e",\n "truememory/predictive.py": "88ae1a2f8436b305855a5884e3ac5ebe435106808533dc0bc563d2054d374c41",\n "truememory/query_classifier.py": "52f1f3bfe5a75f45eb5aef389b490953422bf1d8fb03c5181bff816440bf2476",\n "truememory/rebuild_source.py": "e46639a50d35b711bc0c0b0de9e7a90652b1386b25356c14d095551673c7bd76",\n "truememory/reranker.py": "c02740b54246ca9f52fd6df90d424f18d3417c36734346ede128c91d3c66e90d",\n "truememory/salience.py": "a005256ecc20c44ad7d4825a11fafa2c2b5951a762450dfce9fd80ec7e12940e",\n "truememory/storage.py": "adc89850a897a2f9b478c8a23ad62fa438d7a214405ea21df886e029cdf4481a",\n "truememory/temporal.py": "52e98b3924573db6a209ef27d0318e254e5c976a7a65d75762f901d712505a65",\n "truememory/tier_config.py": "a8e690000c74f7629690c67fb99e34d4bd87f02b78795fb68ec2009f18348489",\n "truememory/tier_switch/__init__.py": "2333416f4985986b9fcf6558478f5c3cf9f75c277961c14902162f8f11ea1705",\n "truememory/tier_switch/activation.py": "24b820b0ff129fb42b9c13544e42b7c4b2ec8397b427d23b61c60c3e8ff19d47",\n "truememory/tier_switch/cache.py": "77bc24c176763629f83d199cc588eca9a9e7432e36e29584a2bd3d475ac314b4",\n "truememory/tier_switch/job.py": "c1def76a9510c3cd600ec11737ee068d71cedae2bc980048fba6c70bdee7a1f0",\n "truememory/tier_switch/manager.py": "160a61ff2b2a4b2a7fdeb3c7fa2442ce161f5b1a426735ea3cac53829b6b8c4b",\n "truememory/tier_switch/runtime.py": "861893308506c0189e50eea2d21a9da43b2c7be0eb90c56f8fd62e6bb40dc5be",\n "truememory/tier_switch/sensors.py": "6471176f75ab5ddce3aa523340a6567ec24f9243c3551f4203baa8ea2f15b55b",\n "truememory/tier_switch/serving.py": "25c1524a02affc127e170c16ceee0f61c2b3b221e28c5a19e9fb347aec57bef7",\n "truememory/tier_switch/source.py": "f8ffe9cd5890c5e6520ca52cfe20bad49cb789d2222e29fffc4300bdbc64f227",\n "truememory/tier_switch/state_machine.py": "39ccf8d3e944728f7bf18e6efe10d0e8f9db625f9d812220554dd980a932d1b9",\n "truememory/tier_switch/throttler.py": "3416ad7681a22b2478e9e4375041f7d2d364b474a81255103e11abe79d5b5f32",\n "truememory/tier_switch/worker.py": "8bae8daf54ab06c0693df46578e340cbde874dda0dccfdb5bcfc2a30d3081ef7",\n "truememory/vector_search.py": "2acd58a0dd858cc6a3a8c69117c1666f5e74d166ed9aeb57b81da4da45804739"\n },\n "staged_truememory_distribution_metadata": {\n ".work/artifacts/memory-installed-wheel-v2/metadata/METADATA": {\n "bytes": 20005,\n "sha256": "6f9399e0fa68584949e48662a5a9c53989eacecc78e39bb7e52cddccdd34662e"\n },\n ".work/artifacts/memory-installed-wheel-v2/metadata/RECORD": {\n "bytes": 9361,\n "sha256": "692cc8dac4cc7fb0e7a480ca1c836a5c6340fd8c1e256b789ad124ba1a02c844"\n },\n ".work/artifacts/memory-installed-wheel-v2/metadata/WHEEL": {\n "bytes": 87,\n "sha256": "5b77e4a649bbfb07fdbc123967eeecd1e5a478cfaca6eefc23c35e6fdf037848"\n },\n ".work/artifacts/memory-installed-wheel-v2/metadata/entry_points.txt": {\n "bytes": 164,\n "sha256": "0a30f6f99c5f7a3e9c87327c34fa7a56ec295a3f3237c7e084d0ca76ee668c9c"\n },\n ".work/artifacts/memory-installed-wheel-v2/metadata/licenses/LICENSE": {\n "bytes": 34523,\n "sha256": "0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0"\n }\n },\n "staged_wheel_sha256": "f723b880e455fea1545c31a9b7fe01def1902af3a36609cfa084fc9548296c51",\n "stdlib_complete_dependency_bytes_hashed": false,\n "supersedes_packet_sha256": "f6418ee79b7f63783f97e9bb3cde468655b952b21279a522cc8ae5f6bfaaffe4",\n "trusted_OS_sharedcache_bytes_hashed": false,\n "unresolved": [\n "Complete interpreter/stdlib/extension/dylib path admission closure requires separate review",\n "System dylibs may be in macOS sharedcache; linkednames/OSbuild are not byte-hash proof",\n "Actual NumPy/fullpackage64MiB/CPU2/5s fit is unproven"\n ]\n}\n' +INTERPRETER_RAW = b'{"all_publication_held":true,"created_utc":"2026-10-12T09:02:33.473994+00:00","exact_content_files":{"/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_psutil_osx.abi3.so":{"bytes":105920,"device":16777232,"inode":13393584,"kind":"runtime native dependency","sha256":"ff4568b9187ca7b37c5134aaf614ffb061e7fedccebd84158343afb05991b8e2"},"/opt/homebrew/Cellar/mpdecimal/4.0.1/lib/libmpdec.4.0.1.dylib":{"bytes":189376,"device":16777232,"inode":5790118,"kind":"runtime native dependency","sha256":"1013bf3eafc5969ab5388fbf283b50c132b208635b59fd5b87b5c71b706aa784"},"/opt/homebrew/Cellar/openssl@3/3.6.2/lib/libcrypto.3.dylib":{"bytes":4845712,"device":16777232,"inode":5810980,"kind":"runtime native dependency","sha256":"ef2239cec921003b54b61968f00489271e2a2805d7d1b4e69d632423f38efe1e"},"/opt/homebrew/Cellar/openssl@3/3.6.2/lib/libssl.3.dylib":{"bytes":884560,"device":16777232,"inode":5810979,"kind":"runtime native dependency","sha256":"26ecfd43c742914a142c8b414b1f308cefe0531fcc6e38b6d45021649850e758"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/Python":{"bytes":6036784,"device":16777232,"inode":5801272,"kind":"CPython framework","sha256":"c365f0090d2bcd97df69c194dbe0293224ef09d88cc8c1e59260c18250e11ac6"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/bin/python3.12":{"bytes":52640,"device":16777232,"inode":5797918,"kind":"runtime native dependency","sha256":"ae711ab41ea1e6d9c2daa0ef816a0e041f9388ffd7c5bccb7d167d5b8afacfaa"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_asyncio.cpython-312-darwin.so":{"bytes":118304,"device":16777232,"inode":5798190,"kind":"runtime native dependency","sha256":"8efe7fe97e42bfa00cf4702a1a37e5e665ff9f6fa2ae45093c68a4885cffd8de"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_bisect.cpython-312-darwin.so":{"bytes":71024,"device":16777232,"inode":5798189,"kind":"runtime native dependency","sha256":"4d8df53fb16339db1e9c832d5e7559bc91fc4c7912d586051490c0cbf9506fc4"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_blake2.cpython-312-darwin.so":{"bytes":89824,"device":16777232,"inode":5798167,"kind":"runtime native dependency","sha256":"d0f8d9738e2a76eab4d1118bcc8d75af2279f841b4e21d3cf2285f85d08cee6b"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_bz2.cpython-312-darwin.so":{"bytes":73200,"device":16777232,"inode":5798204,"kind":"runtime native dependency","sha256":"1012bb5a539948ddd2732162694616155d964487d73e7e8206b25fbe0965d162"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_cn.cpython-312-darwin.so":{"bytes":186208,"device":16777232,"inode":5798191,"kind":"runtime native dependency","sha256":"097bd9bef01f21790d15dca5abf8dfa7621fffce584a53a2ab515d379b41d6ac"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_hk.cpython-312-darwin.so":{"bytes":202816,"device":16777232,"inode":5798236,"kind":"runtime native dependency","sha256":"c9a7ed2f260c8a4a3c7b3d2c2677f33bb6a5e6ed01e8ebb6b67462bf2c4bde90"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_iso2022.cpython-312-darwin.so":{"bytes":72320,"device":16777232,"inode":5798217,"kind":"runtime native dependency","sha256":"ccb4d633b05ab2ccbbde4f064a569477cbb93462eb50d07cc7642d9112097786"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_jp.cpython-312-darwin.so":{"bytes":302720,"device":16777232,"inode":5798203,"kind":"runtime native dependency","sha256":"22e89fa609becb4bfb21563299c2b5b7218a8ec6c609db86983b1d432ca529b7"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_kr.cpython-312-darwin.so":{"bytes":169776,"device":16777232,"inode":5798221,"kind":"runtime native dependency","sha256":"b75f197cc6ff439429309e3d01e7521afd88965ad3a59780c73829d54f65262d"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_tw.cpython-312-darwin.so":{"bytes":169280,"device":16777232,"inode":5798219,"kind":"runtime native dependency","sha256":"4fe57867459470fc8778fdad724992dd39a9766aa69f363cdb04021489b6383d"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_contextvars.cpython-312-darwin.so":{"bytes":68672,"device":16777232,"inode":5798232,"kind":"runtime native dependency","sha256":"3f4033c0bd82bd65ff9a0dec226069c2c15f1f61d8dcd00ee4f8200c9a2e1037"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_crypt.cpython-312-darwin.so":{"bytes":68848,"device":16777232,"inode":5798227,"kind":"runtime native dependency","sha256":"4178beda9a6f47ff4835ea7aa2c295213909ccc036785312fd9034f5180c0414"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_csv.cpython-312-darwin.so":{"bytes":91632,"device":16777232,"inode":5798179,"kind":"runtime native dependency","sha256":"369b952f7a8815f26fe0838696030a5193f5a3ce0296e7c4056ba513173776dd"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_ctypes.cpython-312-darwin.so":{"bytes":158880,"device":16777232,"inode":5798205,"kind":"runtime native dependency","sha256":"4e7d8ebb1e94961ac689c37538a7e7d94661271eaea872de1956e78e003d10a5"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_ctypes_test.cpython-312-darwin.so":{"bytes":74416,"device":16777232,"inode":5798187,"kind":"runtime native dependency","sha256":"279b9c93123eefa0d0b3da516123ad0d56121fb7236bf0c2cdb34f5f92bbd4c0"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_curses.cpython-312-darwin.so":{"bytes":159808,"device":16777232,"inode":5798215,"kind":"runtime native dependency","sha256":"dd35ae0d20ee960d3485ab74bc7b1475ab6bc758776528ecf3d7a11b2eb4626a"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_curses_panel.cpython-312-darwin.so":{"bytes":73312,"device":16777232,"inode":5798173,"kind":"runtime native dependency","sha256":"8948ea0a8f26f23668f0d50aa27e8a9fd08bf9a69fd9084b8d1fc5b95bdafd2f"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_datetime.cpython-312-darwin.so":{"bytes":134080,"device":16777232,"inode":5798208,"kind":"runtime native dependency","sha256":"b0cfd7a9447561997dc02726cfa4c0967c58e3e28591e019421d552fcdd9c26a"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_dbm.cpython-312-darwin.so":{"bytes":72128,"device":16777232,"inode":5798216,"kind":"runtime native dependency","sha256":"13734b3815ea839aa7da5d0df61645138aef13598f74f791385bb37d3dced490"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_decimal.cpython-312-darwin.so":{"bytes":194160,"device":16777232,"inode":5798177,"kind":"runtime native dependency","sha256":"f12c874f31c111067580a340da7aa7f528d354ab282782ea876483d4d524f263"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_elementtree.cpython-312-darwin.so":{"bytes":118352,"device":16777232,"inode":5798228,"kind":"runtime native dependency","sha256":"bc0206adb8a4cab24ed39b244fa02b8f6f6e8700ac5bf95803d42c58780367f8"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_hashlib.cpython-312-darwin.so":{"bytes":98640,"device":16777232,"inode":5798171,"kind":"runtime native dependency","sha256":"bd270aacae15d88ca2e9c63ef8011b5fd96805db6e7bb3fe281abeb3fb5650ba"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_heapq.cpython-312-darwin.so":{"bytes":69920,"device":16777232,"inode":5798222,"kind":"runtime native dependency","sha256":"2b561bbb0b50dcea90638e035574ec811531a028f7d340fed8b82c1197288eb6"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_json.cpython-312-darwin.so":{"bytes":90560,"device":16777232,"inode":5798238,"kind":"runtime native dependency","sha256":"332c8737a959c43c649d694d0cb2d5241155e01dadbb3c1cc809dbc5700d11e0"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_lsprof.cpython-312-darwin.so":{"bytes":72912,"device":16777232,"inode":5798235,"kind":"runtime native dependency","sha256":"5a6cbf0cff49c8bf7e787a99a2a4f206c47703efaf95e6143a6a3133b4a371e4"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_lzma.cpython-312-darwin.so":{"bytes":92880,"device":16777232,"inode":5798199,"kind":"runtime native dependency","sha256":"31d89874e46ad2c229c9d9a575f731fcde92f2e6cd5b0c3f2985a247f67377d4"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_md5.cpython-312-darwin.so":{"bytes":71824,"device":16777232,"inode":5798224,"kind":"runtime native dependency","sha256":"4bbf4933c4274e6de96e861662b378a7b07d3dd4ae6a078270a7d0e4b0028fa0"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_multibytecodec.cpython-312-darwin.so":{"bytes":96672,"device":16777232,"inode":5798192,"kind":"runtime native dependency","sha256":"1806b8076b5b671cad0dbbb4b7a61f0ad37e7342960b6ef71ae7c35f730cb5c7"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_multiprocessing.cpython-312-darwin.so":{"bytes":73952,"device":16777232,"inode":5798225,"kind":"runtime native dependency","sha256":"30ed202b88240c2b001166bbc7bdf009afc00faaa019d4f555d5e94d2c02d5bc"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_opcode.cpython-312-darwin.so":{"bytes":69344,"device":16777232,"inode":5798220,"kind":"runtime native dependency","sha256":"82561570fe57a1f968ef9ed3b8e54ae062037db6ca910bb5d76562c0de57d42c"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_pickle.cpython-312-darwin.so":{"bytes":152736,"device":16777232,"inode":5798202,"kind":"runtime native dependency","sha256":"9c0fef7faef33c2b0a8a9d8edb05d0f4b58bd4b54f869abed97cd84d65baf186"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_posixshmem.cpython-312-darwin.so":{"bytes":69920,"device":16777232,"inode":5798169,"kind":"runtime native dependency","sha256":"ee89ff1dbdc18da313a65d56f3d2505bd669d5491d4ef1d14c9b68c78f8915c6"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_posixsubprocess.cpython-312-darwin.so":{"bytes":72496,"device":16777232,"inode":5798195,"kind":"runtime native dependency","sha256":"5f17a49a4d6bec05a5b2127b2cf173762577175ec40236ae8fe7b242b2e222e4"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_queue.cpython-312-darwin.so":{"bytes":72576,"device":16777232,"inode":5798166,"kind":"runtime native dependency","sha256":"35aec636c3d05307a9a17f320e1bf6a38314b276d431a9ea8528578292d8d8d0"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_random.cpython-312-darwin.so":{"bytes":71888,"device":16777232,"inode":5798230,"kind":"runtime native dependency","sha256":"82a7ed4d7519482d36a3ae06953cbf117d59838d686e818b741f17421256e349"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_scproxy.cpython-312-darwin.so":{"bytes":71296,"device":16777232,"inode":5798223,"kind":"runtime native dependency","sha256":"594511ebdf53542b712a079ae41a5e9ad35d987ba43d8e6cefd4cd8ba033c3dd"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sha1.cpython-312-darwin.so":{"bytes":71872,"device":16777232,"inode":5798168,"kind":"runtime native dependency","sha256":"4b6c1e2c28fc4ef9a9e16e53dcec2e5c65acb73489e32209d98f51a0e4ea35ed"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sha2.cpython-312-darwin.so":{"bytes":90112,"device":16777232,"inode":5798213,"kind":"runtime native dependency","sha256":"f44daa3f719025ede5455e5dcec063d61bf3d952051c3c7ce47cd6dc05319c7a"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sha3.cpython-312-darwin.so":{"bytes":73632,"device":16777232,"inode":5798181,"kind":"runtime native dependency","sha256":"0c31f8df357e9ea942c05b671e4f5449127a6c74a1f6a2f2034b678949666938"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_socket.cpython-312-darwin.so":{"bytes":132720,"device":16777232,"inode":5798176,"kind":"runtime native dependency","sha256":"39072a435f5c496bb7e0493e666b1c4fb7fd4fd3af6e737cc37973ecfb3b438b"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sqlite3.cpython-312-darwin.so":{"bytes":165664,"device":16777232,"inode":5798226,"kind":"runtime native dependency","sha256":"ed57e5b3f9a36b4fc626b9511fa85641d1bd20bddf91d51b4399d4fed12f463a"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_ssl.cpython-312-darwin.so":{"bytes":220192,"device":16777232,"inode":5798183,"kind":"runtime native dependency","sha256":"b8b10518a2e538fe52f2f9c697c5c04c8f78adfbd97dd2e3e1c23698c63600fe"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_statistics.cpython-312-darwin.so":{"bytes":68864,"device":16777232,"inode":5798180,"kind":"runtime native dependency","sha256":"acc8b5429de2de1c493bfb5bf63c18a783c8aff6bc5d438dc69f3d992981fe52"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_struct.cpython-312-darwin.so":{"bytes":95008,"device":16777232,"inode":5798163,"kind":"runtime native dependency","sha256":"faad693f3025bad2bad7a846c45cbaa2e6f59311e6033ae5a014541be5ad350c"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testbuffer.cpython-312-darwin.so":{"bytes":92128,"device":16777232,"inode":5798165,"kind":"runtime native dependency","sha256":"6e5c46995d00fa2589cf3aea5321d6c82e4a5e5033c43d73a1a26adddbfd68bc"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testcapi.cpython-312-darwin.so":{"bytes":368896,"device":16777232,"inode":5798194,"kind":"runtime native dependency","sha256":"742971e338168594364e73acbddc8baed68ce803313d7e7f555ef25a374bcb42"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testclinic.cpython-312-darwin.so":{"bytes":113040,"device":16777232,"inode":5798209,"kind":"runtime native dependency","sha256":"5508c03dc49992eb4f355da7892c4d02522f6420945d1e760e6ba43447ca857b"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testimportmultiple.cpython-312-darwin.so":{"bytes":68320,"device":16777232,"inode":5798175,"kind":"runtime native dependency","sha256":"e2bc9041f0af2c72dbf97328b0cbfeb7bda6f5972bf9dbdbdfd9f66c0c9af186"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testinternalcapi.cpython-312-darwin.so":{"bytes":94576,"device":16777232,"inode":5798231,"kind":"runtime native dependency","sha256":"e94180a61ef7448e05efd93f3baff65751829ca60962dde09656b06e4adca7c0"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testmultiphase.cpython-312-darwin.so":{"bytes":77152,"device":16777232,"inode":5798211,"kind":"runtime native dependency","sha256":"7d4ae01dbd58d96ba3095b09d9714db63da59bc9327e0cc02e162d02137f52e7"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testsinglephase.cpython-312-darwin.so":{"bytes":70256,"device":16777232,"inode":5798214,"kind":"runtime native dependency","sha256":"807d2c24503b6d3ab95771ace413a2be1b2ae67bf2571b530f015ceac57abe3d"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_uuid.cpython-312-darwin.so":{"bytes":68624,"device":16777232,"inode":5798218,"kind":"runtime native dependency","sha256":"653ea323d372448beab3de0f315a364fa5b9cb324ccfe9e9427c2b525751eb7d"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_xxinterpchannels.cpython-312-darwin.so":{"bytes":74928,"device":16777232,"inode":5798174,"kind":"runtime native dependency","sha256":"793e428e1eac546753e8476e5a2f2d201fbbfdc58ac5688ed38d79e6fb2cf14f"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_xxsubinterpreters.cpython-312-darwin.so":{"bytes":73936,"device":16777232,"inode":5798188,"kind":"runtime native dependency","sha256":"0c78f075c14580405c60a57396385f0d7dc22def54729a712e58040c646090ad"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_xxtestfuzz.cpython-312-darwin.so":{"bytes":71328,"device":16777232,"inode":5798210,"kind":"runtime native dependency","sha256":"1bbfa1d6d2758737f7858066148406054b3303f9e72551b5873c67c99a56e168"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_zoneinfo.cpython-312-darwin.so":{"bytes":91968,"device":16777232,"inode":5798170,"kind":"runtime native dependency","sha256":"013df37ed8270df3a08c37b092440b308e14236e36f850a85cee4f00543c84b5"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/array.cpython-312-darwin.so":{"bytes":97488,"device":16777232,"inode":5798164,"kind":"runtime native dependency","sha256":"1c1eae5ebe6616222a551aab4b98e04869096570083b639a938d4108d649b916"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/audioop.cpython-312-darwin.so":{"bytes":89696,"device":16777232,"inode":5798233,"kind":"runtime native dependency","sha256":"8fb121e801de3b95ddff68203c6424094e231e0cdeb6449a0bac73b617cecfa2"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/binascii.cpython-312-darwin.so":{"bytes":89056,"device":16777232,"inode":5798237,"kind":"runtime native dependency","sha256":"c3973d4e15c7fff739fedcaabdcdcd4306ddba019dd3ed64501172bf4d0438e6"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/cmath.cpython-312-darwin.so":{"bytes":88800,"device":16777232,"inode":5798229,"kind":"runtime native dependency","sha256":"bae2872aba117e1411e7893e771a76a0b4f23c75b1db312ffb7aef23a9bf77b6"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/fcntl.cpython-312-darwin.so":{"bytes":70576,"device":16777232,"inode":5798182,"kind":"runtime native dependency","sha256":"e55c4d1ab8fde9d06febb8dd14929c80ccc536433fa426b20693a0c7c54bde5e"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/grp.cpython-312-darwin.so":{"bytes":70832,"device":16777232,"inode":5798172,"kind":"runtime native dependency","sha256":"2001fe992d994e07207f4a110e453441ec61948ddc4dff865bda6ed90a63a7e9"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/math.cpython-312-darwin.so":{"bytes":111968,"device":16777232,"inode":5798212,"kind":"runtime native dependency","sha256":"deeeae703eca73e6bf0ae53404fa4835834c4bd9bd32c889cc9c238a017a24af"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/mmap.cpython-312-darwin.so":{"bytes":73568,"device":16777232,"inode":5798207,"kind":"runtime native dependency","sha256":"f209d7eee73b2ae94faed176d23682173bf4d42a622a9eda7f57d02b1e23d262"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/nis.cpython-312-darwin.so":{"bytes":71024,"device":16777232,"inode":5798206,"kind":"runtime native dependency","sha256":"6d399051d7bbba339df38e22f2f64f789e91a917e0f69034784b7311b37fd22a"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/pyexpat.cpython-312-darwin.so":{"bytes":99600,"device":16777232,"inode":5798200,"kind":"runtime native dependency","sha256":"be320ffc44952341adfe75b8d246c24837fb5a518b4db692bedea350db3b45a0"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/readline.cpython-312-darwin.so":{"bytes":95280,"device":16777232,"inode":5798201,"kind":"runtime native dependency","sha256":"321b279033e2c37316511de74fd25a3525fbc5705793d8f50d72e36a226bd5e2"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/resource.cpython-312-darwin.so":{"bytes":70560,"device":16777232,"inode":5798234,"kind":"runtime native dependency","sha256":"5ead8d0bd513326906c217ebef38b33aa57936c19f81a8e386914d5b5d99a6a4"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/select.cpython-312-darwin.so":{"bytes":92944,"device":16777232,"inode":5798198,"kind":"runtime native dependency","sha256":"ec19dcc94817f0261ec4af51d85b509a6bcd9a65dd2676b466e3d51c55b6426e"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/syslog.cpython-312-darwin.so":{"bytes":70624,"device":16777232,"inode":5798197,"kind":"runtime native dependency","sha256":"7ccafd2aa3bd053ede95f0c632fd23f31a2904ad258290bc5330a53931fc9b8d"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/termios.cpython-312-darwin.so":{"bytes":71808,"device":16777232,"inode":5798184,"kind":"runtime native dependency","sha256":"dd7f18c041f00c51d03c9e99f31e0d73b3f49bcc04633b365bb2294c619a8f29"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/unicodedata.cpython-312-darwin.so":{"bytes":1181200,"device":16777232,"inode":5798186,"kind":"runtime native dependency","sha256":"f481050f6ae43920240605bd7af4267dc8f6f44f33f6b2328e894dd854dce399"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/xxlimited.cpython-312-darwin.so":{"bytes":70784,"device":16777232,"inode":5798178,"kind":"runtime native dependency","sha256":"7bad5cfecfb05b33666bdc35fdce9b23f3dff4f8658fd0f35272a5cdf3d4480c"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/xxlimited_35.cpython-312-darwin.so":{"bytes":70192,"device":16777232,"inode":5798193,"kind":"runtime native dependency","sha256":"4e15fa86847061606a6ab78b6e4b8aae0345f9c6d6a344cf060f673bc94869f6"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/xxsubtype.cpython-312-darwin.so":{"bytes":69472,"device":16777232,"inode":5798196,"kind":"runtime native dependency","sha256":"4061f5e5aa25947893b8b2281ee922e34da6826b9de409477ac472733b7ed6f8"},"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/zlib.cpython-312-darwin.so":{"bytes":94112,"device":16777232,"inode":5798185,"kind":"runtime native dependency","sha256":"94382a4afd7c5f92c30b7561cf02606ee7906427aa5739e8bda71a6fadab04de"},"/opt/homebrew/Cellar/sqlite/3.53.3/lib/libsqlite3.3.53.3.dylib":{"bytes":1286928,"device":16777232,"inode":18678560,"kind":"runtime native dependency","sha256":"b64abb706584a8952e67a3efd872e3fe6e6852c6a2a0d09adec2e94c4ab08ca5"},"/opt/homebrew/Cellar/xz/5.8.3/lib/liblzma.5.dylib":{"bytes":184512,"device":16777232,"inode":18678574,"kind":"runtime native dependency","sha256":"3d5bfa2f097c31463642b1daab5e662b44368bb4da368f85e412e7f9adcbaa10"}},"interpreter":{"framework_realpath":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/Python","realpath":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/bin/python3.12","requested":"/Users/j/dev/TrueMemory/.venv/bin/python","version":"3.12.12"},"linkage_packet_path":".work/reports/mac-native-interpreter-linkage-v1.json","linkage_packet_sha256":"762d535cdb9f021729fe101c654966e52a9d8da285d5fe77bc0b1db815d43843","live_service_queried":false,"modules_imported":false,"native_GO":false,"native_or_models_executed":false,"original_source_config_data_read":false,"runtime_closure_complete":false,"schema":1,"stdlib_complete_dependency_bytes_hashed":false,"total_pinned_native_bytes":22436608,"trust_policy":"Qualified isolated source/dependency behavior; Python audit hooks do not form a native security sandbox","trusted_OS_sharedcache_bytes_hashed":false,"trusted_stdlib":{"builtins_and_frozen":"Interpreter-provided runtime tied to executable/framework pins","canonical_root":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12","excluded_subtrees":["site-packages","test","tests","idlelib","turtledemo","ensurepip","venv"],"lib_dynload_root":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload","python_sources_byte_hashed":false},"trusted_system_loader_names":["/System/Library/Frameworks/Accelerate.framework/Versions/A/Accelerate","/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation","/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit","/System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration","/usr/lib/libSystem.B.dylib","/usr/lib/libbz2.1.0.dylib","/usr/lib/libc++.1.dylib","/usr/lib/libedit.3.dylib","/usr/lib/libexpat.1.dylib","/usr/lib/libffi.dylib","/usr/lib/libncurses.5.4.dylib","/usr/lib/libpanel.5.4.dylib","/usr/lib/libz.1.dylib"]}\n' +LINKAGE_RAW = b'{"modules_imported":false,"observed_linkage":{"/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_psutil_osx.abi3.so":["/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation","/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/mpdecimal/4.0.1/lib/libmpdec.4.0.1.dylib":["/opt/homebrew/opt/mpdecimal/lib/libmpdec.4.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/openssl@3/3.6.2/lib/libcrypto.3.dylib":["/opt/homebrew/opt/openssl@3/lib/libcrypto.3.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/openssl@3/3.6.2/lib/libssl.3.dylib":["/opt/homebrew/opt/openssl@3/lib/libssl.3.dylib","/opt/homebrew/Cellar/openssl@3/3.6.2/lib/libcrypto.3.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/Python":["/opt/homebrew/opt/python@3.12/Frameworks/Python.framework/Versions/3.12/Python","/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/bin/python3.12":["/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/Python","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_asyncio.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_bisect.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_blake2.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_bz2.cpython-312-darwin.so":["/usr/lib/libbz2.1.0.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_cn.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_hk.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_iso2022.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_jp.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_kr.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_tw.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_contextvars.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_crypt.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_csv.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_ctypes.cpython-312-darwin.so":["/usr/lib/libffi.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_ctypes_test.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_curses.cpython-312-darwin.so":["/usr/lib/libncurses.5.4.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_curses_panel.cpython-312-darwin.so":["/usr/lib/libpanel.5.4.dylib","/usr/lib/libncurses.5.4.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_datetime.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_dbm.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_decimal.cpython-312-darwin.so":["/opt/homebrew/opt/mpdecimal/lib/libmpdec.4.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_elementtree.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_hashlib.cpython-312-darwin.so":["/opt/homebrew/opt/openssl@3/lib/libcrypto.3.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_heapq.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_json.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_lsprof.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_lzma.cpython-312-darwin.so":["/opt/homebrew/opt/xz/lib/liblzma.5.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_md5.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_multibytecodec.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_multiprocessing.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_opcode.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_pickle.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_posixshmem.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_posixsubprocess.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_queue.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_random.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_scproxy.cpython-312-darwin.so":["/System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration","/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sha1.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sha2.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sha3.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_socket.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sqlite3.cpython-312-darwin.so":["/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_ssl.cpython-312-darwin.so":["/opt/homebrew/opt/openssl@3/lib/libssl.3.dylib","/opt/homebrew/opt/openssl@3/lib/libcrypto.3.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_statistics.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_struct.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testbuffer.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testcapi.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testclinic.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testimportmultiple.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testinternalcapi.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testmultiphase.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testsinglephase.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_uuid.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_xxinterpchannels.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_xxsubinterpreters.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_xxtestfuzz.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_zoneinfo.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/array.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/audioop.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/binascii.cpython-312-darwin.so":["/usr/lib/libz.1.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/cmath.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/fcntl.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/grp.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/math.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/mmap.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/nis.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/pyexpat.cpython-312-darwin.so":["/usr/lib/libexpat.1.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/readline.cpython-312-darwin.so":["/usr/lib/libedit.3.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/resource.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/select.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/syslog.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/termios.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/unicodedata.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/xxlimited.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/xxlimited_35.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/xxsubtype.cpython-312-darwin.so":["/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/zlib.cpython-312-darwin.so":["/usr/lib/libz.1.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/sqlite/3.53.3/lib/libsqlite3.3.53.3.dylib":["/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib","/usr/lib/libz.1.dylib","/usr/lib/libSystem.B.dylib"],"/opt/homebrew/Cellar/xz/5.8.3/lib/liblzma.5.dylib":["/opt/homebrew/opt/xz/lib/liblzma.5.dylib","/usr/lib/libSystem.B.dylib"]},"requested_realpath_aliases":{"/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_psutil_osx.abi3.so":"/Users/j/dev/TrueMemory/.venv/lib/python3.12/site-packages/psutil/_psutil_osx.abi3.so","/opt/homebrew/Cellar/openssl@3/3.6.2/lib/libcrypto.3.dylib":"/opt/homebrew/Cellar/openssl@3/3.6.2/lib/libcrypto.3.dylib","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/Python":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/Python","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/bin/python3.12":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/bin/python3.12","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_asyncio.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_asyncio.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_bisect.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_bisect.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_blake2.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_blake2.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_bz2.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_bz2.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_cn.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_cn.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_hk.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_hk.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_iso2022.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_iso2022.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_jp.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_jp.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_kr.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_kr.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_tw.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_codecs_tw.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_contextvars.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_contextvars.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_crypt.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_crypt.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_csv.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_csv.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_ctypes.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_ctypes.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_ctypes_test.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_ctypes_test.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_curses.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_curses.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_curses_panel.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_curses_panel.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_datetime.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_datetime.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_dbm.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_dbm.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_decimal.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_decimal.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_elementtree.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_elementtree.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_hashlib.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_hashlib.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_heapq.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_heapq.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_json.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_json.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_lsprof.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_lsprof.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_lzma.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_lzma.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_md5.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_md5.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_multibytecodec.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_multibytecodec.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_multiprocessing.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_multiprocessing.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_opcode.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_opcode.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_pickle.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_pickle.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_posixshmem.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_posixshmem.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_posixsubprocess.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_posixsubprocess.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_queue.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_queue.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_random.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_random.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_scproxy.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_scproxy.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sha1.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sha1.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sha2.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sha2.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sha3.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sha3.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_socket.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_socket.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sqlite3.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_sqlite3.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_ssl.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_ssl.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_statistics.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_statistics.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_struct.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_struct.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testbuffer.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testbuffer.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testcapi.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testcapi.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testclinic.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testclinic.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testimportmultiple.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testimportmultiple.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testinternalcapi.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testinternalcapi.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testmultiphase.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testmultiphase.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testsinglephase.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_testsinglephase.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_uuid.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_uuid.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_xxinterpchannels.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_xxinterpchannels.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_xxsubinterpreters.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_xxsubinterpreters.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_xxtestfuzz.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_xxtestfuzz.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_zoneinfo.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/_zoneinfo.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/array.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/array.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/audioop.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/audioop.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/binascii.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/binascii.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/cmath.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/cmath.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/fcntl.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/fcntl.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/grp.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/grp.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/math.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/math.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/mmap.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/mmap.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/nis.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/nis.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/pyexpat.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/pyexpat.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/readline.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/readline.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/resource.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/resource.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/select.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/select.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/syslog.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/syslog.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/termios.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/termios.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/unicodedata.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/unicodedata.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/xxlimited.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/xxlimited.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/xxlimited_35.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/xxlimited_35.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/xxsubtype.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/xxsubtype.cpython-312-darwin.so","/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/zlib.cpython-312-darwin.so":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/lib/python3.12/lib-dynload/zlib.cpython-312-darwin.so","/opt/homebrew/opt/mpdecimal/lib/libmpdec.4.dylib":"/opt/homebrew/Cellar/mpdecimal/4.0.1/lib/libmpdec.4.0.1.dylib","/opt/homebrew/opt/openssl@3/lib/libcrypto.3.dylib":"/opt/homebrew/Cellar/openssl@3/3.6.2/lib/libcrypto.3.dylib","/opt/homebrew/opt/openssl@3/lib/libssl.3.dylib":"/opt/homebrew/Cellar/openssl@3/3.6.2/lib/libssl.3.dylib","/opt/homebrew/opt/python@3.12/Frameworks/Python.framework/Versions/3.12/Python":"/opt/homebrew/Cellar/python@3.12/3.12.12/Frameworks/Python.framework/Versions/3.12/Python","/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib":"/opt/homebrew/Cellar/sqlite/3.53.3/lib/libsqlite3.3.53.3.dylib","/opt/homebrew/opt/xz/lib/liblzma.5.dylib":"/opt/homebrew/Cellar/xz/5.8.3/lib/liblzma.5.dylib"},"schema":1,"sharedcache_bytes_hashed":false,"trusted_system_loader_names":["/System/Library/Frameworks/Accelerate.framework/Versions/A/Accelerate","/System/Library/Frameworks/CoreFoundation.framework/Versions/A/CoreFoundation","/System/Library/Frameworks/IOKit.framework/Versions/A/IOKit","/System/Library/Frameworks/SystemConfiguration.framework/Versions/A/SystemConfiguration","/usr/lib/libSystem.B.dylib","/usr/lib/libbz2.1.0.dylib","/usr/lib/libc++.1.dylib","/usr/lib/libedit.3.dylib","/usr/lib/libexpat.1.dylib","/usr/lib/libffi.dylib","/usr/lib/libncurses.5.4.dylib","/usr/lib/libpanel.5.4.dylib","/usr/lib/libz.1.dylib"]}\n' + + +def policy_module() -> ModuleType: + name = "native_package_policy_test_v4" + spec = importlib.util.spec_from_file_location(name, PARENT) + if spec is None or spec.loader is None: + raise RuntimeError("Policy source unavailable") + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module + spec.loader.exec_module(module) + return module + + +def bootstrap_module() -> ModuleType: + name = "native_package_bootstrap_test_v4" + spec = importlib.util.spec_from_file_location(name, BOOTSTRAP) + if spec is None or spec.loader is None: + raise RuntimeError("Bootstrap source unavailable") + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module + spec.loader.exec_module(module) + return module + + +def ctypes_frames(stdlib: Path) -> tuple[ModuleType, SimpleNamespace, object]: + filename = str(stdlib / "ctypes/__init__.py") + module = ModuleType("ctypes") + module.__file__ = filename + source = "class CDLL:\n def __init__(self):\n pass\nclass PyDLL(CDLL):\n pass\n" + exec(compile(source, filename, "exec", dont_inherit=True), module.__dict__) + instance = module.PyDLL.__new__(module.PyDLL) + instance._name = None + initializer = SimpleNamespace( + f_code=SimpleNamespace(co_filename=filename, co_name=""), + f_globals=module.__dict__, + f_back=None, + ) + constructor = SimpleNamespace( + f_code=module.CDLL.__init__.__code__, + f_globals=module.__dict__, + f_locals={"self": instance}, + f_back=initializer, + ) + return module, constructor, instance + + +def manifest_fixture(policy: ModuleType) -> dict[str, object]: + return { + "version": 1, + "stage": "B", + "synthetic": True, + "root_execution_go": True, + "run_root": str(policy.RUN_ROOT), + "home": str(policy.RUN_ROOT / "home"), + "source_root": str(policy.SOURCE_ROOT), + "source_commit": policy.SOURCE_COMMIT, + "sources": dict(policy.SOURCE_HASHES), + "sources_sha256": policy.SOURCE_MAP_SHA, + "data": dict(policy.DATA_HASHES), + "data_sha256": policy.DATA_MAP_SHA, + "runtime_packet_sha256": policy.RUNTIME_PACKET_SHA, + "interpreter_packet_sha256": policy.INTERPRETER_PACKET_SHA, + "supervisor_sha256": "a" * 64, + "bootstrap_sha256": "b" * 64, + "limits": dict(policy.LIMITS), + "cases": [{"id": "package-ping"}], + } + + +def result_fixture( + policy: ModuleType, pid: int = 100, uid: int = 7 +) -> dict[str, object]: + caches = {name: None for name in policy.CACHE_KEYS} + registry = { + "pid": pid, + "metric": "process_rss_sampled", + "effective_limit_bytes": 8589934592, + "owner_count": 1, + "closing_owner_count": 0, + "lease_count": 0, + "reserved_known_bytes": 0, + "high_water_known_bytes": 512, + "unprofiled_stages": list(policy.UNKNOWN_STAGES), + "coverage_truncated": False, + "physical_hard_cap": False, + "os_enforcement": copy.deepcopy(policy.CAPABILITY), + } + details = { + "package_version": "0.7.6.2", + "initializer_executed": True, + "paths_exact": True, + "ping": {"ok": True, "protocol": 1}, + "timeout_ms": 200, + "source_data_loaded": True, + "neural_imports": [], + "caches_before": caches, + "caches_after": dict(caches), + "socket_identity": {"device": 1, "inode": 2}, + "socket_uid": uid, + "socket_mode": 384, + "pid_file": {"pid": pid, "device": 1, "inode": 3, "mode": 384}, + "lock_identity": {"device": 1, "inode": 4}, + "bind_lock_held": True, + "registry": registry, + } + return { + "type": "result", + "version": 1, + "case": "package-ping", + "passed": True, + "proof_scope": policy.PROOF_SCOPE, + "import_mode": "full_source_package", + "source_commit": policy.SOURCE_COMMIT, + "sources_sha256": policy.SOURCE_MAP_SHA, + "source_count": 37, + "data_sha256": policy.DATA_MAP_SHA, + "runtime_packet_sha256": policy.RUNTIME_PACKET_SHA, + "interpreter_packet_sha256": policy.INTERPRETER_PACKET_SHA, + "details": details, + } + + +class PackageAcceptanceContracts(unittest.TestCase): + def listener_fixture(self) -> tuple[SimpleNamespace, Mock, Path, SimpleNamespace]: + endpoint = self.bootstrap.RUN_ROOT / "home/.truememory/model.sock" + listener = Mock(spec=["fileno", "getsockname", "getsockopt", "listen"]) + listener.fileno.return_value = 87 + listener.getsockname.return_value = str(endpoint) + guard = SimpleNamespace( + listener=listener, endpoint=endpoint, policy=self.policy + ) + info = SimpleNamespace(st_mode=stat.S_IFREG | 0o600, st_uid=7, st_nlink=1) + return guard, listener, endpoint.with_name("model_server.pid"), info + + def test_listener_eligibility_admits_only_one_ping_candidate(self) -> None: + guard, listener, pid_path, info = self.listener_fixture() + with ( + patch.object(self.policy, "owned_path", return_value=info) as owned, + patch.object(self.bootstrap.os, "getuid", return_value=7), + ): + self.assertIs( + self.bootstrap.listener_eligible(guard, listener, pid_path), True + ) + owned.assert_called_once_with(pid_path) + listener.fileno.assert_called_once_with() + listener.getsockname.assert_called_once_with() + listener.getsockopt.assert_not_called() + listener.listen.assert_not_called() + + def test_listener_eligibility_missing_or_foreign_object_refuses(self) -> None: + guard, listener, pid_path, info = self.listener_fixture() + for candidate in (None, Mock()): + with ( + self.subTest(candidate=candidate), + patch.object(self.policy, "owned_path", return_value=info) as owned, + ): + self.assertIs( + self.bootstrap.listener_eligible(guard, candidate, pid_path), False + ) + owned.assert_not_called() + listener.fileno.assert_not_called() + listener.getsockname.assert_not_called() + + def test_listener_eligibility_exact_current_descriptor_refuses_invalid( + self, + ) -> None: + for descriptor in (-1, 0, 1, 2, False, True, 3.0, "3", None): + with self.subTest(descriptor=descriptor): + guard, listener, pid_path, info = self.listener_fixture() + listener.fileno.return_value = descriptor + with patch.object( + self.policy, "owned_path", return_value=info + ) as owned: + self.assertIs( + self.bootstrap.listener_eligible(guard, listener, pid_path), + False, + ) + owned.assert_not_called() + listener.getsockname.assert_not_called() + + def test_listener_eligibility_exact_endpoint_refuses_foreign_or_wrong_type( + self, + ) -> None: + guard, listener, pid_path, info = self.listener_fixture() + for endpoint in ( + str(guard.endpoint.with_name("other.sock")), + bytes(str(guard.endpoint), "utf-8"), + guard.endpoint, + (str(guard.endpoint),), + None, + ): + with ( + self.subTest(endpoint=endpoint), + patch.object(self.policy, "owned_path", return_value=info) as owned, + ): + listener.getsockname.return_value = endpoint + self.assertIs( + self.bootstrap.listener_eligible(guard, listener, pid_path), False + ) + owned.assert_not_called() + + def test_listener_eligibility_exact_PID_path_refuses_other_paths(self) -> None: + guard, listener, pid_path, info = self.listener_fixture() + for other in ( + pid_path.with_name("other.pid"), + pid_path.parent.parent / "model_server.pid", + Path("model_server.pid"), + pid_path.parent / "sibling/model_server.pid", + ): + with ( + self.subTest(path=other), + patch.object(self.policy, "owned_path", return_value=info) as owned, + ): + self.assertIs( + self.bootstrap.listener_eligible(guard, listener, other), False + ) + owned.assert_not_called() + listener.fileno.assert_not_called() + + def test_listener_eligibility_private_owned_single_link_regular_evidence( + self, + ) -> None: + for field, value in ( + ("st_uid", 8), + ("st_mode", stat.S_IFREG | 0o644), + ("st_mode", stat.S_IFDIR | 0o700), + ("st_mode", stat.S_IFLNK | 0o600), + ("st_nlink", 0), + ("st_nlink", 2), + ): + with self.subTest(field=field, value=value): + guard, listener, pid_path, info = self.listener_fixture() + setattr(info, field, value) + with ( + patch.object(self.policy, "owned_path", return_value=info), + patch.object(self.bootstrap.os, "getuid", return_value=7), + ): + self.assertIs( + self.bootstrap.listener_eligible(guard, listener, pid_path), + False, + ) + + def test_listener_eligibility_actual_owned_path_canonical_and_link_checks( + self, + ) -> None: + for mode in ( + "valid", + "noncanonical", + "link", + "foreign_UID", + "public_mode", + "directory", + "hardlink", + ): + with self.subTest(mode=mode): + guard, listener, pid_path, info = self.listener_fixture() + resolved = ( + pid_path.with_name("other.pid") + if mode == "noncanonical" + else pid_path + ) + if mode == "foreign_UID": + info.st_uid = 8 + elif mode == "public_mode": + info.st_mode = stat.S_IFREG | 0o644 + elif mode == "directory": + info.st_mode = stat.S_IFDIR | 0o700 + elif mode == "hardlink": + info.st_nlink = 2 + with ( + patch.object(Path, "resolve", return_value=resolved), + patch.object(Path, "is_symlink", return_value=mode == "link"), + patch.object(Path, "lstat", return_value=info), + patch.object(self.bootstrap.os, "getuid", return_value=7), + ): + self.assertIs( + self.bootstrap.listener_eligible(guard, listener, pid_path), + mode == "valid", + ) + + def test_listener_eligibility_specific_source_errors_are_ineligible(self) -> None: + for operation, error in ( + ("fileno", OSError("closed")), + ("fileno", ValueError("closed")), + ("getsockname", OSError(42, "Protocol not available")), + ("owned_path", FileNotFoundError("missing")), + ("owned_path", self.policy.SafetyError("path_not_private_owned")), + ): + with self.subTest(operation=operation, error=type(error)): + guard, listener, pid_path, info = self.listener_fixture() + if operation != "owned_path": + getattr(listener, operation).side_effect = error + with patch.object( + self.policy, + "owned_path", + side_effect=error if operation == "owned_path" else None, + return_value=info, + ): + self.assertIs( + self.bootstrap.listener_eligible(guard, listener, pid_path), + False, + ) + guard, listener, pid_path, info = self.listener_fixture() + listener.fileno.side_effect = TypeError("Unsupported socket interface") + with self.assertRaises(TypeError): + self.bootstrap.listener_eligible(guard, listener, pid_path) + + def test_listener_eligibility_preserves_single_ping_and_original_bounded_loop( + self, + ) -> None: + text = BOOTSTRAP.read_text() + tree = ast.parse(text) + worker = next( + node + for node in tree.body + if isinstance(node, ast.FunctionDef) and node.name == "execute_case" + ) + calls = [ + node + for node in ast.walk(worker) + if isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) + and node.func.attr == "_send_request" + ] + self.assertEqual(1, len(calls)) + self.assertEqual( + ast.dump( + ast.parse('model_client._send_request({"op":"ping"},timeout=0.2)') + .body[0] + .value + ), + ast.dump(calls[0]), + ) + loop = next(node for node in worker.body if isinstance(node, ast.While)) + self.assertEqual( + ast.dump( + ast.parse('require(runner.is_alive(),"server_thread_stopped")').body[0] + ), + ast.dump(loop.body[0]), + ) + self.assertEqual( + ast.dump( + ast.parse("listener_eligible(guard,listener,model_server.PID_PATH)") + .body[0] + .value + ), + ast.dump(loop.body[2].test), + ) + self.assertEqual( + ast.dump( + ast.parse( + 'require(time.monotonic() None: + comparisons: list[object] = [] + + class EqNone: + def __eq__(self, value: object) -> bool: + comparisons.append(value) + return value is None + + worker = self.bootstrap + guard = worker.AuditGuard.__new__(worker.AuditGuard) + guard.stdlib = Path("/synthetic/stdlib") + guard.ctypes_namespace_loads = 0 + module, frame, _ = ctypes_frames(guard.stdlib) + with ( + patch.dict(worker.sys.modules, {"ctypes": module}), + patch.object(worker.sys, "_getframe", return_value=frame) as frame_lookup, + ): + with self.assertRaises(worker.BootstrapError): + guard("ctypes.dlopen", (EqNone(),)) + frame_lookup.assert_not_called() + self.assertEqual([], comparisons) + self.assertEqual(0, guard.ctypes_namespace_loads) + + @classmethod + def setUpClass(cls) -> None: + cls.policy = policy_module() + cls.bootstrap = bootstrap_module() + + def test_ctypes_actual_initializer_one_shot_only(self) -> None: + worker = self.bootstrap + guard = worker.AuditGuard.__new__(worker.AuditGuard) + guard.stdlib = Path("/synthetic/stdlib") + guard.ctypes_namespace_loads = 0 + module, frame, _ = ctypes_frames(guard.stdlib) + with ( + patch.dict(worker.sys.modules, {"ctypes": module}), + patch.object(worker.sys, "_getframe", return_value=frame), + ): + guard("ctypes.dlopen", (None,)) + self.assertEqual(1, guard.ctypes_namespace_loads) + with self.assertRaises(worker.BootstrapError): + guard("ctypes.dlopen", (None,)) + self.assertEqual(1, guard.ctypes_namespace_loads) + + def test_ctypes_process_namespace_argument_count_and_library_negatives( + self, + ) -> None: + worker = self.bootstrap + for args in ( + [None], + (), + (None, 0), + ("/usr/lib/libSystem.B.dylib",), + (b"library",), + (False,), + (0,), + ): + guard = worker.AuditGuard.__new__(worker.AuditGuard) + guard.stdlib = Path("/synthetic/stdlib") + guard.ctypes_namespace_loads = 0 + with self.subTest(args=args), self.assertRaises(worker.BootstrapError): + guard("ctypes.dlopen", args) + self.assertEqual(0, guard.ctypes_namespace_loads) + for count in (False, True, 1, -1, 1.0): + guard = worker.AuditGuard.__new__(worker.AuditGuard) + guard.stdlib = Path("/synthetic/stdlib") + guard.ctypes_namespace_loads = count + with self.subTest(count=count), self.assertRaises(worker.BootstrapError): + guard("ctypes.dlopen", (None,)) + + def test_ctypes_foreign_module_globals_and_constructor_frames_refuse(self) -> None: + worker = self.bootstrap + for mode in ( + "module_missing", + "foreign_module", + "constructor_globals", + "initializer_globals", + "constructor_file", + "constructor_name", + "foreign_code", + "initializer_file", + "initializer_name", + "initializer_missing", + ): + with self.subTest(mode=mode): + guard = worker.AuditGuard.__new__(worker.AuditGuard) + guard.stdlib = Path("/synthetic/stdlib") + guard.ctypes_namespace_loads = 0 + module, frame, _ = ctypes_frames(guard.stdlib) + current = module + if mode == "module_missing": + current = None + elif mode == "foreign_module": + current = ModuleType("ctypes") + elif mode == "constructor_globals": + frame.f_globals = dict(module.__dict__) + elif mode == "initializer_globals": + frame.f_back.f_globals = dict(module.__dict__) + elif mode in ("constructor_file", "constructor_name", "foreign_code"): + source = ( + "def __init__(self):\n pass\n" + if mode != "constructor_name" + else "def foreign(self):\n pass\n" + ) + namespace = {} + filename = ( + "/synthetic/foreign.py" + if mode == "constructor_file" + else str(guard.stdlib / "ctypes/__init__.py") + ) + exec( + compile(source, filename, "exec", dont_inherit=True), namespace + ) + frame.f_code = namespace[ + "foreign" if mode == "constructor_name" else "__init__" + ].__code__ + elif mode == "initializer_file": + frame.f_back.f_code.co_filename = "/synthetic/foreign.py" + elif mode == "initializer_name": + frame.f_back.f_code.co_name = "foreign" + else: + frame.f_back = None + with ( + patch.dict(worker.sys.modules, {"ctypes": current}), + patch.object(worker.sys, "_getframe", return_value=frame), + ): + with self.assertRaises(worker.BootstrapError): + guard("ctypes.dlopen", (None,)) + self.assertEqual(0, guard.ctypes_namespace_loads) + + def test_ctypes_namespace_type_name_and_pythonapi_negatives(self) -> None: + worker = self.bootstrap + for mode in ( + "CDLL_not_class", + "CDLL_builtin_init", + "PyDLL_not_class", + "CDLL_self", + "PyDLL_subclass", + "named_library", + "name_missing", + "pythonapi_present", + ): + with self.subTest(mode=mode): + guard = worker.AuditGuard.__new__(worker.AuditGuard) + guard.stdlib = Path("/synthetic/stdlib") + guard.ctypes_namespace_loads = 0 + module, frame, instance = ctypes_frames(guard.stdlib) + if mode == "CDLL_not_class": + module.CDLL = None + elif mode == "CDLL_builtin_init": + module.CDLL = type("CDLL", (), {}) + elif mode == "PyDLL_not_class": + module.PyDLL = None + elif mode == "CDLL_self": + frame.f_locals["self"] = module.CDLL.__new__(module.CDLL) + frame.f_locals["self"]._name = None + elif mode == "PyDLL_subclass": + child = type("ForeignPyDLL", (module.PyDLL,), {}) + frame.f_locals["self"] = child.__new__(child) + frame.f_locals["self"]._name = None + elif mode == "named_library": + instance._name = "library" + elif mode == "name_missing": + del instance._name + else: + module.pythonapi = None + with ( + patch.dict(worker.sys.modules, {"ctypes": module}), + patch.object(worker.sys, "_getframe", return_value=frame), + ): + with self.assertRaises(worker.BootstrapError): + guard("ctypes.dlopen", (None,)) + self.assertEqual(0, guard.ctypes_namespace_loads) + + def test_manifest_actual_frozen_positive(self) -> None: + value = manifest_fixture(self.policy) + self.assertIs(self.policy.validate_manifest(value), value) + self.assertEqual(37, len(value["sources"])) + self.assertEqual(18, len(value)) + self.assertEqual(1, value["limits"]["threads"]) + self.assertEqual(4, self.policy.B_OS_THREAD_MAX) + + def test_manifest_bool_path_source_limit_and_extra_refuse(self) -> None: + for field, value in ( + ("version", True), + ("root_execution_go", 1), + ("run_root", "/synthetic/other"), + ("source_commit", "c" * 40), + ("sources_sha256", "c" * 64), + ("data_sha256", "c" * 64), + ("runtime_packet_sha256", "c" * 64), + ): + with self.subTest(field=field): + row = manifest_fixture(self.policy) + row[field] = value + with self.assertRaises(self.policy.SafetyError): + self.policy.validate_manifest(row) + for mode in ("extra", "source_missing", "limit_bool", "limit_raise"): + with self.subTest(mode=mode): + row = manifest_fixture(self.policy) + if mode == "extra": + row["unexpected"] = True + elif mode == "source_missing": + row["sources"].pop("truememory/__init__.py") + elif mode == "limit_bool": + row["limits"]["threads"] = True + else: + row["limits"]["rss_abort_bytes"] += 1 + with self.assertRaises(self.policy.SafetyError): + self.policy.validate_manifest(row) + + def test_case_one_exact_operation(self) -> None: + self.assertEqual( + {"id": "package-ping"}, self.policy.case_contract({"id": "package-ping"}) + ) + for value in ( + {"id": "package-ping", "timeout": 120}, + {"id": "embed"}, + [], + {"id": True}, + ): + with self.assertRaises(self.policy.SafetyError): + self.policy.case_contract(value) + + def test_runtime_actual_raw_hashes_and_qualified_flags(self) -> None: + result = self.policy.runtime_contract(PACKAGE_RAW, INTERPRETER_RAW, LINKAGE_RAW) + self.assertEqual(37, len(result["package"]["sources"])) + self.assertEqual(193, len(result["package"]["numpy_files"])) + self.assertEqual(84, len(result["interpreter"]["exact_content_files"])) + self.assertIs(result["package"]["runtime_closure_complete"], False) + self.assertIs( + result["interpreter"]["stdlib_complete_dependency_bytes_hashed"], False + ) + self.assertIs(result["linkage"]["sharedcache_bytes_hashed"], False) + for index, raw in enumerate((PACKAGE_RAW, INTERPRETER_RAW, LINKAGE_RAW)): + args = [PACKAGE_RAW, INTERPRETER_RAW, LINKAGE_RAW] + args[index] = raw + b" " + with self.assertRaises(self.policy.SafetyError): + self.policy.runtime_contract(*args) + + def test_runtime_source_maps_data_and_metadata_current(self) -> None: + runtime = self.policy.runtime_contract( + PACKAGE_RAW, INTERPRETER_RAW, LINKAGE_RAW + ) + self.assertEqual( + "aeecf25f78279629789d98a29bbd8bf817233391", + runtime["package"]["source_commit"], + ) + self.assertEqual( + { + "truememory/data/l3_weights.json": self.policy.DATA_HASHES[ + "truememory/data/l3_weights.json" + ] + }, + runtime["package"]["data"], + ) + self.assertEqual( + "f723b880e455fea1545c31a9b7fe01def1902af3a36609cfa084fc9548296c51", + runtime["package"]["staged_wheel_sha256"], + ) + self.assertTrue( + all( + "memory-installed-wheel-v2/metadata/" in name + for name in runtime["package"][ + "staged_truememory_distribution_metadata" + ] + ) + ) + files = self.policy.runtime_files(runtime) + self.assertGreater(len(files), 193) + + def test_complete_result_actual_field_shape_and_intact_copy(self) -> None: + row = result_fixture(self.policy) + original = copy.deepcopy(row) + self.assertIs( + self.policy.result_contract(row, manifest_fixture(self.policy), 100, 7), row + ) + self.assertEqual(original, row) + self.assertEqual( + (13, 16, 12, 7), + ( + len(row), + len(row["details"]), + len(row["details"]["registry"]), + len(row["details"]["registry"]["os_enforcement"]), + ), + ) + self.assertLessEqual(len(self.policy.encoded(row)) + 1, 2048) + + def test_result_bool_false_scope_cache_and_unknowns_refuse(self) -> None: + mutations = [ + ("source_count", True), + ("passed", 1), + ("import_mode", "isolated_source_module"), + ("source_commit", "a" * 40), + ] + for field, value in mutations: + row = result_fixture(self.policy) + row[field] = value + with self.assertRaises(self.policy.SafetyError): + self.policy.result_contract(row, manifest_fixture(self.policy), 100, 7) + for mode in ( + "cache", + "unknown_empty", + "unknown_omitted", + "cap_reason", + "hard_cap", + "active_lease", + "wrong_pid", + "protocol_bool", + ): + with self.subTest(mode=mode): + row = result_fixture(self.policy) + d = row["details"] + r = d["registry"] + if mode == "cache": + d["caches_after"]["vector_model"] = "fake" + elif mode == "unknown_empty": + r["unprofiled_stages"] = [] + elif mode == "unknown_omitted": + r["unprofiled_stages"].pop() + elif mode == "cap_reason": + r["os_enforcement"]["reason"] = "" + elif mode == "hard_cap": + r["physical_hard_cap"] = True + elif mode == "active_lease": + r["lease_count"] = 1 + elif mode == "wrong_pid": + d["pid_file"]["pid"] = 101 + else: + d["ping"]["protocol"] = True + with self.assertRaises(self.policy.SafetyError): + self.policy.result_contract( + row, manifest_fixture(self.policy), 100, 7 + ) + + def test_result_large_valid_numbers_refuse_without_mutation(self) -> None: + row = result_fixture(self.policy, pid=(1 << 63) - 1, uid=(1 << 63) - 1) + for key in ("socket_identity", "lock_identity"): + row["details"][key] = {"device": (1 << 63) - 1, "inode": (1 << 63) - 1} + row["details"]["pid_file"].update(device=(1 << 63) - 1, inode=(1 << 63) - 1) + row["details"]["registry"]["high_water_known_bytes"] = 8589934592 + original = copy.deepcopy(row) + self.assertGreater(len(self.policy.encoded(row)) + 1, 2048) + with self.assertRaises(self.policy.SafetyError): + self.policy.result_contract( + row, manifest_fixture(self.policy), (1 << 63) - 1, (1 << 63) - 1 + ) + self.assertEqual(original, row) + + def test_mutation_actual_producer_flags_and_socket_count(self) -> None: + policy = self.policy + control = policy.MutationPolicy({"id": "package-ping"}) + events = [ + ("os.mkdir", control.model_dir, 0o777), + ("os.chmod", control.model_dir, 0o700), + ("open", control.lock, os.O_CREAT | os.O_RDWR), + ("socket.__new__", policy.ENDPOINT, 0), + ("socket.bind", policy.ENDPOINT, 0), + ("os.chmod", policy.ENDPOINT, 0o600), + ("open", control.pid, os.O_CREAT | os.O_WRONLY | os.O_TRUNC), + ("socket.__new__", policy.ENDPOINT, 0), + ("socket.connect", policy.ENDPOINT, 0), + ("socket.__new__", policy.ENDPOINT, 0), + ] + for seq, (event, path, flags) in enumerate(events, 1): + result = control.decide( + { + "type": "mutation", + "version": 1, + "seq": seq, + "event": event, + "path": str(path), + "source": None, + "flags": flags, + } + ) + self.assertEqual(seq, result["seq"]) + self.assertEqual(3, control.socket_objects) + with self.assertRaises(policy.SafetyError): + control.decide( + { + "type": "mutation", + "version": 1, + "seq": 11, + "event": "socket.__new__", + "path": str(policy.ENDPOINT), + "source": None, + "flags": 0, + } + ) + + def test_mutation_extra_flags_wrong_paths_unlink_sequence_refuse(self) -> None: + policy = self.policy + for event, path, flags in ( + ("os.mkdir", policy.ENDPOINT.parent, 0o700), + ("open", policy.ENDPOINT.with_name("other"), os.O_CREAT | os.O_WRONLY), + ( + "open", + policy.ENDPOINT.with_name("model_server.pid"), + os.O_CREAT | os.O_RDWR | os.O_TRUNC, + ), + ("os.remove", policy.ENDPOINT, 0), + ("socket.connect", policy.ENDPOINT, 0), + ): + control = policy.MutationPolicy({"id": "package-ping"}) + with self.assertRaises(policy.SafetyError): + control.decide( + { + "type": "mutation", + "version": 1, + "seq": 1, + "event": event, + "path": str(path), + "source": None, + "flags": flags, + } + ) + control = policy.MutationPolicy({"id": "package-ping"}) + with self.assertRaises(policy.SafetyError): + control.decide( + { + "type": "mutation", + "version": 1, + "seq": 2, + "event": "os.mkdir", + "path": str(control.model_dir), + "source": None, + "flags": 0o777, + } + ) + + def test_native_hash_streaming_closes_on_success_and_failure(self) -> None: + policy = self.policy + data = b"x" * 65537 + info = SimpleNamespace( + st_mode=stat.S_IFREG | 0o444, + st_size=len(data), + st_dev=1, + st_ino=2, + st_mtime_ns=3, + st_ctime_ns=4, + ) + path = Path("/synthetic/runtime.so") + for checksum in (hashlib.sha256(data).hexdigest(), "f" * 64): + with ( + patch.object(Path, "resolve", lambda self: self), + patch.object(Path, "is_symlink", return_value=False), + patch.object(policy.os, "open", return_value=87) as opened, + patch.object(policy.os, "fstat", return_value=info), + patch.object( + policy.os, "read", side_effect=[data[:65536], data[65536:], b""] + ) as read, + patch.object(policy.os, "close") as close, + ): + if checksum.startswith("f" * 64): + with self.assertRaises(policy.SafetyError): + policy.native_file_sha256( + path, {"bytes": len(data), "sha256": checksum} + ) + else: + self.assertEqual( + checksum, + policy.native_file_sha256( + path, {"bytes": len(data), "sha256": checksum} + ), + ) + close.assert_called_once_with(87) + self.assertTrue(opened.call_args.args[1] & os.O_NONBLOCK) + self.assertTrue(opened.call_args.args[1] & os.O_NOFOLLOW) + self.assertTrue( + all(call.args[1] <= 65536 for call in read.call_args_list) + ) + + def test_thread_samples_strict_four_ceiling(self) -> None: + for value in (1, 2, 3, 4): + self.assertEqual(value, self.policy.thread_sample_contract(value)) + for value in (0, 5, True, 4.0, None): + with self.assertRaises(self.policy.SafetyError): + self.policy.thread_sample_contract(value) + self.assertEqual(1, self.policy.LIMITS["threads"]) + + def test_terminal_ACK_whole_object(self) -> None: + self.assertEqual( + self.policy.TERMINAL_ACK, + self.policy.terminal_ACK_contract(dict(self.policy.TERMINAL_ACK)), + ) + for value in ( + {"type": "result_ack", "version": True, "case": "package-ping"}, + {"type": "result_ack", "version": 1, "case": "package-ping", "extra": 1}, + {"type": "result_ack", "version": 1, "case": "other"}, + ): + with self.assertRaises(self.policy.SafetyError): + self.policy.terminal_ACK_contract(value) + + def test_actual_bootstrap_accepted_descriptor_requires_exact_frame(self) -> None: + worker = self.bootstrap + endpoint = worker.RUN_ROOT / "home/.truememory/model.sock" + stdlib = Path("/synthetic/stdlib") + for mode in ( + "accepted", + "foreign_frame", + "foreign_listener", + "foreign_fd", + "regular_fd", + "wrong_uid", + "extra_wrapper", + ): + with self.subTest(mode=mode): + guard = worker.AuditGuard.__new__(worker.AuditGuard) + guard.stdlib, guard.endpoint = stdlib, endpoint + guard.listener = Mock() + guard.listener.getsockname.return_value = str(endpoint) + guard.client = None + guard.accepted = None + guard.accepted_checked = False + guard.socket_count = 3 if mode == "extra_wrapper" else 2 + guard.grant = Mock() + caller = SimpleNamespace( + f_code=SimpleNamespace( + co_filename=str(stdlib / "socket.py"), co_name="accept" + ), + f_locals={"self": guard.listener, "fd": 87}, + f_back=None, + ) + if mode == "foreign_frame": + caller.f_code.co_filename = "/synthetic/foreign.py" + elif mode == "foreign_listener": + caller.f_locals["self"] = Mock() + elif mode == "foreign_fd": + caller.f_locals["fd"] = 88 + initializer = SimpleNamespace( + f_code=SimpleNamespace( + co_filename=str(stdlib / "socket.py"), co_name="__init__" + ), + f_locals={"fileno": 87}, + f_back=caller, + ) + info = SimpleNamespace( + st_mode=stat.S_IFREG if mode == "regular_fd" else stat.S_IFSOCK, + st_uid=8 if mode == "wrong_uid" else 7, + st_dev=1, + st_ino=2, + ) + obj = Mock() + with ( + patch.object(worker.sys, "_getframe", return_value=initializer), + patch.object(worker.os, "fstat", return_value=info), + patch.object(worker.os, "getuid", return_value=7), + ): + args = ( + obj, + int(worker.socket.AF_UNIX), + int(worker.socket.SOCK_STREAM), + 0, + ) + if mode == "accepted": + guard.socket_new(args) + self.assertIs(guard.accepted, obj) + self.assertTrue(guard.accepted_checked) + self.assertEqual((1, 2), guard.accepted_identity) + self.assertEqual(3, guard.socket_count) + guard.grant.assert_called_once_with( + "socket.__new__", endpoint, flags=0 + ) + else: + with self.assertRaises(worker.BootstrapError): + guard.socket_new(args) + guard.grant.assert_not_called() + + def test_actual_bootstrap_grant_lock_and_whole_ACK(self) -> None: + worker = self.bootstrap + path = worker.RUN_ROOT / "home/.truememory/model_server.lock" + for mode in ( + "valid", + "lock_refused", + "wrong_sequence", + "extra_field", + "terminal", + ): + with self.subTest(mode=mode): + guard = worker.AuditGuard.__new__(worker.AuditGuard) + guard.terminal = mode == "terminal" + guard.sequence = 0 + guard.approved = {} + guard.exchange_lock = Mock() + guard.exchange_lock.acquire.return_value = mode != "lock_refused" + answer = { + "type": "grant", + "version": 1, + "seq": 1, + "path": str(path), + "purpose": "Synthetic retained lock", + } + if mode == "wrong_sequence": + answer["seq"] = 2 + elif mode == "extra_field": + answer["extra"] = True + guard.protocol = Mock() + guard.protocol.receive.return_value = answer + if mode == "valid": + guard.grant("open", path, flags=os.O_CREAT | os.O_RDWR) + self.assertEqual({path: os.O_RDWR}, guard.approved) + else: + with self.assertRaises(worker.BootstrapError): + guard.grant("open", path, flags=os.O_CREAT | os.O_RDWR) + self.assertEqual({}, guard.approved) + if mode == "terminal": + guard.exchange_lock.acquire.assert_not_called() + guard.protocol.emit.assert_not_called() + else: + guard.exchange_lock.acquire.assert_called_once_with(timeout=0.1) + if mode in ("terminal", "lock_refused"): + guard.exchange_lock.release.assert_not_called() + guard.protocol.emit.assert_not_called() + else: + guard.exchange_lock.release.assert_called_once_with() + guard.protocol.receive.assert_called_once_with(2048, 0.1) + self.assertEqual(1, guard.protocol.emit.call_args.args[0]["seq"]) + + def test_actual_bootstrap_terminal_requires_ACK_before_hard_exit(self) -> None: + worker = self.bootstrap + for mode in ("valid", "wrong_case", "lock_refused"): + with self.subTest(mode=mode): + guard = worker.AuditGuard.__new__(worker.AuditGuard) + guard.terminal = False + guard.policy = self.policy + guard.exchange_lock = Mock() + guard.exchange_lock.acquire.return_value = mode != "lock_refused" + guard.protocol = Mock() + guard.protocol.receive.return_value = dict(self.policy.TERMINAL_ACK) + if mode == "wrong_case": + guard.protocol.receive.return_value["case"] = "other" + with patch.object( + worker.os, "_exit", side_effect=RuntimeError("Inert hard exit") + ) as hard_exit: + if mode == "valid": + with self.assertRaisesRegex(RuntimeError, "Inert hard exit"): + guard.terminal_result({"type": "result"}) + hard_exit.assert_called_once_with(73) + elif mode == "wrong_case": + with self.assertRaises(self.policy.SafetyError): + guard.terminal_result({"type": "result"}) + hard_exit.assert_not_called() + else: + with self.assertRaises(worker.BootstrapError): + guard.terminal_result({"type": "result"}) + hard_exit.assert_not_called() + if mode == "lock_refused": + guard.protocol.emit.assert_not_called() + else: + guard.protocol.receive.assert_called_once_with(2048, 0.1) + guard.exchange_lock.release.assert_called_once_with() + + def test_original_admission_and_attempt_guards(self) -> None: + policy = self.policy + now = 4000000000 + rows = [ + policy.Sensor(stamp, 1, 268435456, 5) + for stamp in (1000000000, 2000000000, 3500000000) + ] + policy.admission(rows, now) + for mode in ("free", "warning", "swap", "stale"): + changed = list(rows) + if mode == "free": + changed[-1] = policy.Sensor(3500000000, 1, 268435455, 5) + elif mode == "warning": + changed[-1] = policy.Sensor(3500000000, 2, 268435456, 5) + elif mode == "swap": + changed[-1] = policy.Sensor(3500000000, 1, 268435456, 6) + else: + changed[-1] = policy.Sensor(3000000000, 1, 268435456, 5) + with self.assertRaises(policy.SafetyError): + policy.admission(changed, now) + for value in ("v1", "v2", "v99"): + self.assertEqual(value, policy.validate_attempt_version(value)) + for value in ("v0", "v01", "v100", True): + with self.assertRaises(policy.SafetyError): + policy.validate_attempt_version(value) + + def test_static_worker_real_source_and_bounded_grant_terminal(self) -> None: + tree = ast.parse(BOOTSTRAP.read_text()) + text = BOOTSTRAP.read_text() + self.assertIn('importlib.import_module("truememory")', text) + self.assertIn("ModelServer(automatic_rss_budget=True)", text) + self.assertIn('_send_request({"op":"ping"},timeout=0.2)', text) + worker = next( + node + for node in tree.body + if isinstance(node, ast.FunctionDef) and node.name == "execute_case" + ) + worker_body = "\n".join( + ast.get_source_segment(text, node) for node in worker.body + ) + self.assertNotIn("types.ModuleType", worker_body) + self.assertIn("registry = dict(snapshot)", text) + self.assertIn('asdict(snapshot["os_enforcement"])', text) + self.assertIn('str(os.getpid()).encode("ascii")', text) + self.assertIn("exchange_lock.acquire(timeout=0.1)", text) + self.assertIn("self.policy.terminal_ACK_contract(answer)", text) + self.assertIn("os._exit(73)", text) + self.assertTrue( + any( + isinstance(node, ast.FunctionDef) and node.name == "socket_new" + for node in ast.walk(tree) + ) + ) + parent = PARENT.read_text() + self.assertIn("thread_sample_contract(process.num_threads())", parent) + self.assertIn('result.get("terminal_ACK_sent") is not True', parent) + self.assertNotIn("((signal.SIGTERM, 0.25)", parent) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-memory-enforcement-native-297.py b/tests/test-memory-enforcement-native-297.py new file mode 100644 index 00000000..4ccb5c69 --- /dev/null +++ b/tests/test-memory-enforcement-native-297.py @@ -0,0 +1,131 @@ +"""Synthetic bootstrap readback controls; real enforcement remains root-gated.""" +from __future__ import annotations + +import ast +import importlib.util +import json +import os +from pathlib import Path +import stat +import sys +from types import SimpleNamespace +import unittest +from unittest.mock import patch + + +_SOURCE = Path(__file__).resolve().parents[1] / "truememory" / "memory_bootstrap.py" +_SPEC = importlib.util.spec_from_file_location("_memory_bootstrap_author_297", _SOURCE) +assert _SPEC is not None and _SPEC.loader is not None +bootstrap = importlib.util.module_from_spec(_SPEC) +sys.modules[_SPEC.name] = bootstrap +_SPEC.loader.exec_module(bootstrap) + + +def expectation() -> object: + return bootstrap.LinuxScopeExpectation("/synthetic/owned", 123, 1000, 10, 20, 67108864, 0) + + +_HAS_SCOPE_FLAGS = all(hasattr(os, name) for name in ("O_DIRECTORY", "O_NOFOLLOW", "O_NONBLOCK")) + + +class SyntheticReadbackControls(unittest.TestCase): + def test_bootstrap_imports_only_stdlib_and_does_not_activate(self) -> None: + tree = ast.parse(_SOURCE.read_bytes()) + imports = {node.module.split(".")[0] for node in ast.walk(tree) + if isinstance(node, ast.ImportFrom) and node.module is not None} + imports |= {alias.name.split(".")[0] for node in ast.walk(tree) + if isinstance(node, ast.Import) for alias in node.names} + self.assertLessEqual(imports, {"__future__", "dataclasses", "os", "pathlib", "stat"}) + self.assertNotIn("truememory", imports) + self.assertNotIn("torch", imports) + with self.assertRaises(SystemExit): + bootstrap.main() + + def test_finite_typed_expectations(self) -> None: + baseline = expectation().__dict__ + for field, value in (("pid", True), ("uid", -1), ("memory_max", 0), + ("swap_max", "max"), ("inode", 2**63), ("scope", "relative")): + with self.subTest(field=field), self.assertRaises(ValueError): + bootstrap.LinuxScopeExpectation(**(baseline | {field: value})) + + @unittest.skipUnless(_HAS_SCOPE_FLAGS, "Descriptor readback needs POSIX no-follow primitives") + def test_readback_never_promotes_physical_or_lifetime_claim(self) -> None: + files = {"memory.max": "67108864", "memory.swap.max": "0", "cgroup.procs": "123", + "memory.current": "1048576", "memory.events": "low 0\nhigh 0\nmax 0\noom 0\noom_kill 0"} + fake_stat = SimpleNamespace(st_uid=1000, st_dev=10, st_ino=20) + with patch.object(bootstrap.os, "open", return_value=17), \ + patch.object(bootstrap.os, "fstat", return_value=fake_stat), \ + patch.object(bootstrap.os, "close") as close, \ + patch.object(bootstrap, "_read_owned_file", side_effect=lambda fd, name: files[name]): + result = bootstrap.inspect_owned_linux_scope(expectation()) + self.assertEqual(result.members, (123,)) + self.assertEqual(result.memory_current, 1048576) + self.assertEqual(result.metric, "controller_file_readback_unverified") + self.assertFalse(result.physical_hard_cap) + self.assertFalse(result.lifetime_enforcement_proven) + close.assert_called_once_with(17) + + @unittest.skipUnless(_HAS_SCOPE_FLAGS, "Descriptor readback needs POSIX no-follow primitives") + def test_unrelated_scope_or_member_is_rejected_and_descriptor_closes(self) -> None: + base = {"memory.max": "67108864", "memory.swap.max": "0", "cgroup.procs": "123", + "memory.current": "0", "memory.events": "oom_kill 0"} + for changed in ({"memory.max": "max"}, {"memory.max": "67108863"}, + {"memory.swap.max": "1"}, {"cgroup.procs": "123\n456"}, + {"cgroup.procs": ""}, {"memory.events": "bad"}): + files = base | changed + with self.subTest(changed=changed), \ + patch.object(bootstrap.os, "open", return_value=17), \ + patch.object(bootstrap.os, "fstat", return_value=SimpleNamespace(st_uid=1000, st_dev=10, st_ino=20)), \ + patch.object(bootstrap.os, "close") as close, \ + patch.object(bootstrap, "_read_owned_file", side_effect=lambda fd, name: files[name]): + with self.assertRaises(bootstrap.EnforcementUnavailable): + bootstrap.inspect_owned_linux_scope(expectation()) + close.assert_called_once_with(17) + with patch.object(bootstrap.os, "open", return_value=17), \ + patch.object(bootstrap.os, "fstat", return_value=SimpleNamespace(st_uid=0, st_dev=10, st_ino=20)), \ + patch.object(bootstrap.os, "close") as close: + with self.assertRaises(bootstrap.EnforcementUnavailable): + bootstrap.inspect_owned_linux_scope(expectation()) + close.assert_called_once_with(17) + + @unittest.skipUnless(_HAS_SCOPE_FLAGS, "Descriptor readback needs POSIX no-follow primitives") + def test_nonregular_controller_file_uses_nonblocking_open_and_closes(self) -> None: + with patch.object(bootstrap.os, "open", return_value=19) as opened, \ + patch.object(bootstrap.os, "fstat", return_value=SimpleNamespace(st_mode=stat.S_IFIFO)), \ + patch.object(bootstrap.os, "close") as closed, \ + patch.object(bootstrap.os, "read") as read: + with self.assertRaises(bootstrap.EnforcementUnavailable): + bootstrap._read_owned_file(17, "memory.max") + self.assertTrue(opened.call_args.args[1] & os.O_NONBLOCK) + self.assertTrue(opened.call_args.args[1] & os.O_NOFOLLOW) + read.assert_not_called() + closed.assert_called_once_with(19) + + @unittest.skipUnless(_HAS_SCOPE_FLAGS, "Descriptor readback needs POSIX no-follow primitives") + def test_readback_is_bounded_and_closes_on_decode_failure(self) -> None: + for chunks in ([b"\xff", b""], [b"x" * 4096] * 17): + with self.subTest(kind=len(chunks)), \ + patch.object(bootstrap.os, "open", return_value=19), \ + patch.object(bootstrap.os, "fstat", return_value=SimpleNamespace(st_mode=stat.S_IFREG)), \ + patch.object(bootstrap.os, "close") as close, \ + patch.object(bootstrap.os, "read", side_effect=chunks): + with self.assertRaises((UnicodeError, bootstrap.EnforcementUnavailable)): + bootstrap._read_owned_file(17, "memory.max") + close.assert_called_once_with(19) + + +@unittest.skipUnless(sys.platform == "linux" and os.environ.get("TRUEMEMORY_GPU_MEMORY_READBACK") == "1", + "Actual owned Linux readback requires a separate root-approved scope receipt") +class OwnedLinuxReadbackWitness(unittest.TestCase): + def test_explicit_owned_scope_readback_only(self) -> None: + receipt_path = os.environ.get("TRUEMEMORY_GPU_MEMORY_READBACK_RECEIPT") + self.assertIsNotNone(receipt_path, "Root must supply an explicit scope expectation receipt") + raw = json.loads(Path(receipt_path).read_text()) + self.assertEqual(set(raw), {"scope", "pid", "uid", "device", "inode", "memory_max", "swap_max"}) + result = bootstrap.inspect_owned_linux_scope(bootstrap.LinuxScopeExpectation(**raw)) + self.assertFalse(result.physical_hard_cap) + self.assertFalse(result.lifetime_enforcement_proven) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-source-capture-admission-native-v1.py b/tests/test-source-capture-admission-native-v1.py new file mode 100644 index 00000000..266734e2 --- /dev/null +++ b/tests/test-source-capture-admission-native-v1.py @@ -0,0 +1,292 @@ +"""Tiny retained synthetic GPU controls for actual source capture admission. + +Linux only. This file does not provide Mac, installed-host or model proof. +""" +from __future__ import annotations + +import contextlib +import errno +import io +import os +import signal +import stat +import sys +import types +import unittest +from collections.abc import Callable, Iterator +from pathlib import Path +from typing import TYPE_CHECKING +from unittest.mock import patch + +if TYPE_CHECKING: + from truememory.ingest.completion import Capture + +ROOT = Path(__file__).resolve().parents[1] +_CASE_NUMBER = 0 +_ANNOUNCEMENT_BYTES = 0 +MAX_ANNOUNCEMENT_BYTES = 65536 +MAX_REGULAR_FIXTURE_BYTES_PER_CASE = 65536 +EVIDENCE_ROOT = Path("/results/source-capture-admission-native-v1") + + +def announce(path: Path, purpose: str) -> None: + global _ANNOUNCEMENT_BYTES + line = f"Create {path}: {purpose}\n" + _ANNOUNCEMENT_BYTES += len(line.encode("utf-8")) + if _ANNOUNCEMENT_BYTES > MAX_ANNOUNCEMENT_BYTES: + raise AssertionError("Synthetic fixture announcements exceeded 65536 bytes") + print(line, end="", flush=True) + + +def directory(path: Path) -> None: + announce(path, "Retained synthetic GPU source-capture fixture directory") + path.mkdir(mode=0o700) + + +@contextlib.contextmanager +def actual_completion(home: Path) -> Iterator[types.ModuleType]: + previous = {name: value for name, value in sys.modules.items() + if name == "truememory" or name.startswith("truememory.")} + for name in previous: + del sys.modules[name] + try: + with patch.dict(os.environ, {"HOME": str(home)}, clear=False): + for name, relative in (("truememory", "truememory"), + ("truememory.ingest", "truememory/ingest"), + ("truememory.ingest.hooks", "truememory/ingest/hooks")): + module = types.ModuleType(name) + module.__path__ = [str(ROOT / relative)] + sys.modules[name] = module + for name, relative in (("truememory._platform", "truememory/_platform.py"), + ("truememory.ingest.hooks._shared", "truememory/ingest/hooks/_shared.py"), + ("truememory.ingest.completion", "truememory/ingest/completion.py")): + module = types.ModuleType(name) + module.__file__ = str(ROOT / relative) + module.__package__ = name.rpartition(".")[0] + sys.modules[name] = module + exec(compile((ROOT / relative).read_bytes(), str(ROOT / relative), "exec", dont_inherit=True), module.__dict__) + yield sys.modules["truememory.ingest.completion"] + finally: + for name in tuple(sys.modules): + if name == "truememory" or name.startswith("truememory."): + del sys.modules[name] + sys.modules.update(previous) + + +@contextlib.contextmanager +def bounded_call() -> Iterator[None]: + previous = signal.getsignal(signal.SIGALRM) + timer = signal.getitimer(signal.ITIMER_REAL) + + def expired(signum: int, frame: object) -> None: + raise TimeoutError("Synthetic GPU capture call blocked") + + signal.signal(signal.SIGALRM, expired) + signal.setitimer(signal.ITIMER_REAL, 1) + try: + yield + finally: + signal.setitimer(signal.ITIMER_REAL, 0) + signal.signal(signal.SIGALRM, previous) + signal.setitimer(signal.ITIMER_REAL, *timer) + + +@unittest.skipUnless(sys.platform.startswith("linux"), "Bounded synthetic GPU descriptor controls are Linux only") +class SourceCaptureAdmission(unittest.TestCase): + def setUp(self) -> None: + global _CASE_NUMBER + _CASE_NUMBER += 1 + if not EVIDENCE_ROOT.exists(): + directory(EVIDENCE_ROOT) + self.base = EVIDENCE_ROOT / f"{os.getpid()}-{_CASE_NUMBER}" + directory(self.base) + self.home = self.base / "home" + directory(self.home) + directory(self.home / ".claude") + directory(self.home / ".claude/projects") + self.source = self.home / ".claude/projects/source-v1.jsonl" + self.raw = b'{"type":"user","timestamp":"2020-01-01T00:00:00Z","message":{"role":"user","content":"Synthetic scope only."}}\n' + self.context = actual_completion(self.home) + self.module = self.context.__enter__() + + def tearDown(self) -> None: + self.context.__exit__(None, None, None) + regular_bytes = sum(info.st_size for path in self.base.rglob("*") + if stat.S_ISREG((info := path.lstat()).st_mode)) + self.assertLessEqual(regular_bytes, MAX_REGULAR_FIXTURE_BYTES_PER_CASE) + # All evidence remains in the announced private synthetic directory. + + def regular_source(self) -> None: + announce(self.source, "Immutable tiny synthetic source version") + fd = os.open(self.source, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600) + try: + os.write(fd, self.raw) + finally: + os.close(fd) + + def capture(self) -> Capture: + original_open, original_mkdir, original_replace = os.open, os.mkdir, os.replace + + def opened(path: str | Path, flags: int, *args: object, **kwargs: object) -> int: + if flags & os.O_CREAT: + announce(Path(path), "Retained synthetic GPU production capture artifact") + return original_open(path, flags, *args, **kwargs) + + def made(path: str | Path, *args: object, **kwargs: object) -> None: + announce(Path(path), "Private synthetic GPU production capture directory") + return original_mkdir(path, *args, **kwargs) + + def replaced(source: str | Path, destination: str | Path, *args: object, **kwargs: object) -> None: + announce(Path(destination), "Publish synthetic GPU state from its retained temporary") + return original_replace(source, destination, *args, **kwargs) + + with bounded_call(), patch.object(self.module.os, "open", opened), patch.object(self.module.os, "mkdir", made), patch.object(self.module.os, "replace", replaced): + return self.module.admit_capture(str(self.source), "synthetic-session-v1", "synthetic-account-v1", + str(self.base / "locator-only-v1.db"), root=self.base / "completion-v1") + + @contextlib.contextmanager + def source_descriptors(self, before_open: Callable[[int], None] | None = None) -> Iterator[None]: + original = os.open + self.opened: list[tuple[int, int, int]] = [] + self.flags: list[int] = [] + + def observed(path: str | Path, flags: int, *args: object, **kwargs: object) -> int: + if Path(path) == self.source: + if before_open is not None: + before_open(len(self.flags)) + self.flags.append(flags) + fd = original(path, flags, *args, **kwargs) + info = os.fstat(fd) + self.opened.append((fd, info.st_dev, info.st_ino)) + return fd + return original(path, flags, *args, **kwargs) + + with patch.object(self.module.os, "open", observed): + yield + for fd, device, inode in self.opened: + try: + info = os.fstat(fd) + except OSError as error: + self.assertEqual(error.errno, errno.EBADF) + else: + self.assertNotEqual((info.st_dev, info.st_ino), (device, inode), "Source descriptor leaked") + + def test_regular_capture_bytes_identity_and_idempotence(self) -> None: + self.regular_source() + with self.source_descriptors(): + first = self.capture() + second = self.capture() + self.assertEqual(first.key, second.key) + self.assertEqual(first.path.read_bytes(), self.raw) + self.assertEqual(first.metadata["byte_count"], len(self.raw)) + self.module.verify_capture(first) + self.assertEqual(len(self.flags), 4) + self.assertTrue(all(flags & os.O_NONBLOCK and flags & os.O_NOFOLLOW for flags in self.flags)) + self.assertFalse((self.base / "locator-only-v1.db").exists()) + + def test_first_fifo_refused_before_wrapper(self) -> None: + announce(self.source, "Synthetic GPU FIFO negative; never a Mac fixture") + os.mkfifo(self.source, 0o600) + with self.source_descriptors(), patch.object(self.module.os, "fdopen", side_effect=AssertionError("FIFO reached wrapper")): + with self.assertRaisesRegex(self.module.CompletionError, "source_not_regular"): + self.capture() + self.assertTrue(self.flags[0] & os.O_NONBLOCK) + + def replace_source(self, kind: str) -> None: + backup = self.source.with_name("preserved-source-v1.jsonl") + announce(backup, "Preserved original synthetic source before pathname replacement") + os.rename(self.source, backup) + announce(self.source, "Synthetic GPU replacement " + kind) + if kind == "FIFO": + os.mkfifo(self.source, 0o600) + else: + self.source.mkdir(mode=0o700) + + def test_second_fifo_refused_without_blocking(self) -> None: + self.regular_source() + + def replace_on_second(ordinal: int) -> None: + if ordinal == 1: + self.replace_source("FIFO") + + with self.source_descriptors(replace_on_second): + with self.assertRaisesRegex(self.module.CompletionError, "source_not_regular"): + self.capture() + self.assertEqual(len(self.flags), 2) + self.assertTrue(all(flags & os.O_NONBLOCK for flags in self.flags)) + + def test_first_directory_refused_before_wrapper(self) -> None: + directory(self.source) + with self.source_descriptors(), patch.object(self.module.os, "fdopen", side_effect=AssertionError("Directory reached wrapper")): + with self.assertRaisesRegex(self.module.CompletionError, "source_not_regular"): + self.capture() + + def test_second_directory_refused_before_wrapper(self) -> None: + self.regular_source() + + def replace_on_second(ordinal: int) -> None: + if ordinal == 1: + self.replace_source("directory") + + with self.source_descriptors(replace_on_second): + with self.assertRaisesRegex(self.module.CompletionError, "source_not_regular"): + self.capture() + + def test_first_and_second_wrapper_failure_closes_raw_descriptor(self) -> None: + self.regular_source() + original = os.fdopen + for target in (1, 2): + with self.subTest(source_open=target): + count = 0 + + def failed(fd: int, mode: str) -> io.IOBase: + nonlocal count + count += 1 + if count == target: + raise OSError(errno.EIO, "Synthetic wrapper failure") + return original(fd, mode) + + with self.source_descriptors(), patch.object(self.module.os, "fdopen", failed): + with self.assertRaisesRegex(self.module.CompletionError, "source_capture_failed"): + self.capture() + + def test_valueerror_wrapper_failure_also_closes_raw_descriptor(self) -> None: + self.regular_source() + with self.source_descriptors(), patch.object(self.module.os, "fdopen", side_effect=ValueError("Synthetic wrapper failure")): + with self.assertRaisesRegex(ValueError, "Synthetic wrapper failure"): + self.capture() + + def test_first_and_second_fstat_failure_closes_raw_descriptor(self) -> None: + self.regular_source() + original = os.fstat + original_open = os.open + for target in (1, 3): + with self.subTest(fstat_ordinal=target): + calls = 0 + fds: list[int] = [] + + def observed(path: str | Path, flags: int, *args: object, **kwargs: object) -> int: + fd = original_open(path, flags, *args, **kwargs) + if Path(path) == self.source: + fds.append(fd) + return fd + + def failed(fd: int) -> os.stat_result: + nonlocal calls + if fd in fds: + calls += 1 + if calls == target: + raise OSError(errno.EIO, "Synthetic fstat failure") + return original(fd) + + with patch.object(self.module.os, "open", observed), patch.object(self.module.os, "fstat", failed): + with self.assertRaisesRegex(self.module.CompletionError, "source_capture_failed"): + self.capture() + for fd in fds: + with self.assertRaises(OSError) as error: + original(fd) + self.assertEqual(error.exception.errno, errno.EBADF) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test-tier-streamed-worker-756.py b/tests/test-tier-streamed-worker-756.py index 88a32e9c..a453618a 100644 --- a/tests/test-tier-streamed-worker-756.py +++ b/tests/test-tier-streamed-worker-756.py @@ -178,6 +178,7 @@ def load(name): self.modules[name] = module return module + load("memory_budget") target_module = load("embedding_target") self.target = target_module.EmbeddingTarget("custom", "synthetic/worker", 2, "custom") self.vector = types.ModuleType("synthetic_streamed_vector") diff --git a/tests/test-whole-process-memory-budget-297.py b/tests/test-whole-process-memory-budget-297.py new file mode 100644 index 00000000..512990d6 --- /dev/null +++ b/tests/test-whole-process-memory-budget-297.py @@ -0,0 +1,812 @@ +"""Synthetic controls for sampled RSS plus declared future allocation admission. + +Run only in root's isolated test environment. No model or package initializer loads. +""" +from __future__ import annotations + +import ast +import __future__ +import gc +import importlib.util +import json +from pathlib import Path +import sys +import struct +import threading +from types import ModuleType, SimpleNamespace +import unittest +import weakref +from unittest.mock import patch + + +_PATH = Path(__file__).resolve().parents[1] / "truememory" / "memory_budget.py" +_SPEC = importlib.util.spec_from_file_location("_memory_budget_author_297", _PATH) +assert _SPEC is not None and _SPEC.loader is not None +budget = importlib.util.module_from_spec(_SPEC) +sys.modules[_SPEC.name] = budget +_SPEC.loader.exec_module(budget) + + +class Buffer: + def __init__(self, nbytes: int, base: object = None) -> None: + self.nbytes = nbytes + self.base = base + + +class FalseSensor: + def __bool__(self) -> bool: + return False + + def __call__(self) -> int: + return 100 + + +def key(stage: str = "synthetic stage") -> object: + return budget.ProfileKey(stage, "synthetic") + + +def plan(amount: int, stage: str = "synthetic stage") -> object: + return budget.AllocationPlan(key(stage), amount) + + +def native_key() -> object: + return budget.ProfileKey( + "native synthetic", "synthetic", runtime_fingerprint="1" * 64, + source_fingerprint="2" * 64, model_identity=("synthetic/model", 32, "synthetic"), + resolved_revision="3" * 40, asset_manifest_sha256="4" * 64, + device="cpu", backend="synthetic", dtype="float32", attention_impl="eager", + token_shape=((1, 8), (1, 8)), native_tensor_shape=(1, 8, 32), + batch_size=1, max_seq_length=8, padding_policy="longest", truncation_policy="max_length", + thread_config_fingerprint="5" * 64, + ) + + +class RegistryControls(unittest.TestCase): + def test_sum_boundary_and_draining_minimum(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + high = registry.register_owner(1000, kind="high") + low = registry.register_owner(500, kind="low") + disabled = registry.register_owner(0, kind="disabled") + ticket = low.reserve(plan(300)) + self.assertEqual(registry.snapshot()["reserved_known_bytes"], 300) + with self.assertRaises(budget.MemoryAdmissionRefused): + high.reserve(plan(101)) + low.close() + self.assertEqual(registry.snapshot()["effective_limit_bytes"], 500) + disabled.close() + self.assertEqual(registry.snapshot()["effective_limit_bytes"], 500) + ticket.close() + self.assertEqual(registry.snapshot()["effective_limit_bytes"], 1000) + high.close() + self.assertEqual(registry.snapshot()["owner_count"], 0) + + def test_current_rss_equal_limit_refuses_even_zero_demand(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + owner = registry.register_owner(100, kind="equal") + with self.assertRaises(budget.MemoryAdmissionRefused): + owner.reserve(plan(0)) + self.assertEqual(registry.snapshot()["lease_count"], 0) + owner.close() + + def test_sum_equal_limit_accepts(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + owner = registry.register_owner(500, kind="equal") + ticket = owner.reserve(plan(400)) + self.assertEqual(registry.snapshot()["reserved_known_bytes"], 400) + ticket.close() + owner.close() + + def test_disabled_owners_do_not_read_sensor(self) -> None: + def unavailable() -> object: + raise AssertionError("disabled sensor was queried") + registry = budget.ProcessRegistry(usage_reader=unavailable) + owner = registry.register_owner(0, kind="disabled") + ticket = owner.reserve(plan(500)) + self.assertEqual(registry.snapshot()["reserved_known_bytes"], 500) + ticket.close() + owner.close() + + def test_sensor_boundaries(self) -> None: + for value in (None, False, True, 0, -1, 1.0, 2**63): + with self.subTest(value=value): + registry = budget.ProcessRegistry(usage_reader=lambda: value) + owner = registry.register_owner(1000, kind="sensor") + with self.assertRaises(budget.MemoryAdmissionRefused): + owner.reserve(plan(1)) + owner.close() + + def test_false_callable_sensor_is_preserved(self) -> None: + registry = budget.ProcessRegistry(usage_reader=FalseSensor()) + owner = registry.register_owner(1000, kind="sensor", usage_reader=FalseSensor()) + ticket = owner.reserve(plan(900)) + self.assertEqual(registry.snapshot()["reserved_known_bytes"], 900) + ticket.close() + owner.close() + for invalid in (0, False, "sensor"): + with self.assertRaises(ValueError): + budget.ProcessRegistry(usage_reader=invalid) + + def test_atomic_concurrent_admission(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + owner = registry.register_owner(1000, kind="concurrent") + barrier = threading.Barrier(3) + successes = [] + failures = [] + def worker() -> None: + barrier.wait(timeout=5) + try: + successes.append(owner.reserve(plan(600))) + except budget.MemoryAdmissionRefused as error: + failures.append(error) + workers = [threading.Thread(target=worker) for _ in range(2)] + for worker_thread in workers: + worker_thread.start() + barrier.wait(timeout=5) + for worker_thread in workers: + worker_thread.join(timeout=5) + self.assertFalse(worker_thread.is_alive()) + self.assertEqual(len(successes), 1) + self.assertEqual(len(failures), 1) + self.assertEqual(registry.snapshot()["reserved_known_bytes"], 600) + successes[0].close() + failures.clear() + owner.close() + + def test_overflow_and_bool_are_not_estimates(self) -> None: + for value in (True, False, -1, 2**63): + with self.assertRaises(ValueError): + plan(value) + registry = budget.ProcessRegistry(usage_reader=lambda: 1) + owner = registry.register_owner(0, kind="overflow") + ticket = owner.reserve(plan(2**63 - 1)) + with self.assertRaises(budget.MemoryAdmissionRefused): + owner.reserve(plan(1)) + ticket.close() + owner.close() + + def test_unknown_coverage_is_none_not_native_zero(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + unknown = registry.resolve_profile(key("unknown")) + self.assertIsNone(unknown.future_bytes) + self.assertIsNone(unknown.retained_bytes) + self.assertEqual(unknown.basis, "unprofiled") + owner = registry.register_owner(1000, kind="unknown") + ticket = owner.reserve(unknown) + self.assertIn("unknown", registry.snapshot()["unprofiled_stages"]) + self.assertIsNone(ticket.plan.future_bytes) + ticket.close() + owner.close() + + def test_retained_split_preserves_empirical_provenance(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + empirical = budget.AllocationPlan( + native_key(), 300, 50, "empirical_native_profile", + profile_id="6" * 64, evidence_sha256=("7" * 64,), reason="Synthetic measured fixture", + ) + owner = registry.register_owner(1000, kind="retention") + original = owner.reserve(empirical) + retained = original.split_retained(50) + self.assertEqual(retained.plan.basis, empirical.basis) + self.assertEqual(retained.plan.profile_id, empirical.profile_id) + self.assertEqual(retained.plan.evidence_sha256, empirical.evidence_sha256) + self.assertEqual(retained.plan.reason, empirical.reason) + self.assertEqual(registry.snapshot()["reserved_known_bytes"], 300) + original.close() + self.assertEqual(registry.snapshot()["reserved_known_bytes"], 50) + owner.close() + self.assertEqual(registry.snapshot()["effective_limit_bytes"], 1000) + retained.close() + self.assertEqual(registry.snapshot()["owner_count"], 0) + + def test_repeated_buffer_and_views_share_one_retained_ticket(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + owner = registry.register_owner(1000, kind="result") + result = Buffer(40) + view = Buffer(20, result) + with budget.allocation_scope(owner) as session: + session.charge(key("result assembly"), known_bytes=40) + session.retain_buffer(result) + session.retain_buffer(result) + session.retain_buffer(view) + self.assertEqual(registry.snapshot()["reserved_known_bytes"], 40) + owner.close() + del result + gc.collect() + self.assertEqual(registry.snapshot()["reserved_known_bytes"], 40) + del view + gc.collect() + self.assertEqual(registry.snapshot()["reserved_known_bytes"], 0) + self.assertEqual(registry.snapshot()["owner_count"], 0) + + def test_exception_lifetime_keeps_declared_reservation(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + owner = registry.register_owner(1000, kind="failure") + _failure = None + try: + with budget.allocation_scope(owner) as session: + session.charge(key("failed stage"), known_bytes=200) + raise RuntimeError("synthetic retained failure") + except RuntimeError as error: + _failure = error + self.assertEqual(registry.snapshot()["reserved_known_bytes"], 200) + owner.close() + self.assertEqual(registry.snapshot()["effective_limit_bytes"], 1000) + session = None + _failure = None + gc.collect() + self.assertEqual(registry.snapshot()["reserved_known_bytes"], 0) + + def test_exact_profiles_bind_only_before_first_owner(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + expected = plan(200) + registry.bind_profiles((expected,)) + self.assertEqual(registry.resolve_profile(expected.key), expected) + owner = registry.register_owner(1000, kind="bound") + with self.assertRaises(budget.InvalidAllocationPlan): + registry.bind_profiles((expected,)) + owner.close() + + def test_identity_binding_fills_only_static_exact_fields(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + runtime = budget.ProfileKey("runtime context", "context", runtime_fingerprint="1" * 64, + source_fingerprint="2" * 64, thread_config_fingerprint="3" * 64) + model = native_key() + registry.bind_identity_context(runtime, (model,)) + actual = registry.enrich_key(budget.ProfileKey( + "construction", "synthetic", model_identity=model.model_identity, device="cpu", + )) + self.assertEqual(actual.asset_manifest_sha256, model.asset_manifest_sha256) + self.assertEqual(actual.attention_impl, "eager") + self.assertIsNone(actual.token_shape) + self.assertIsNone(actual.native_tensor_shape) + self.assertEqual(actual.runtime_fingerprint, runtime.runtime_fingerprint) + + def test_no_physical_enforcement_claim(self) -> None: + registry = budget.ProcessRegistry() + self.assertFalse(registry.snapshot()["physical_hard_cap"]) + self.assertFalse(registry.capability.physical_hard_cap) + with self.assertRaises(ValueError): + budget.EnforcementCapability(physical_hard_cap=True) + + +class SchemaControls(unittest.TestCase): + def test_custom_strings_rejected_before_hashing(self) -> None: + class CustomString(str): + pass + for changes in ({"stage": CustomString("stage")}, {"owner_kind": CustomString("owner")}, + {"device": CustomString("cpu")}, + {"model_identity": (CustomString("model"), 32, "tier")}, + {"live_residency_signature": (("m", "cpu", CustomString("float32"), "r"),)}): + with self.subTest(changes=changes), self.assertRaises(ValueError): + budget.ProfileKey(**({"stage": "stage", "owner_kind": "owner"} | changes)) + for reason in (1, False, CustomString("reason"), ""): + with self.assertRaises(budget.InvalidAllocationPlan): + budget.AllocationPlan(key(), None, None, "unprofiled", + unknown_components=("workspace",), reason=reason) + + def test_subclasses_cannot_skip_boundary_validation(self) -> None: + class InvalidKey(budget.ProfileKey): + def __post_init__(self) -> None: + pass + class InvalidPlan(budget.AllocationPlan): + def __post_init__(self) -> None: + pass + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + owner = registry.register_owner(1000, kind="schema") + with self.assertRaises(budget.InvalidAllocationPlan): + budget.AllocationPlan(InvalidKey("stage", "owner"), 1) + invalid = InvalidPlan(key(), True) + with self.assertRaises(budget.InvalidAllocationPlan): + owner.reserve(invalid) + with self.assertRaises(budget.InvalidAllocationPlan): + registry.bind_profiles((invalid,)) + owner.close() + + def test_mutated_exact_instance_is_revalidated(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + owner = registry.register_owner(1000, kind="schema") + invalid = plan(1) + object.__setattr__(invalid, "future_bytes", True) + with self.assertRaises(ValueError): + owner.reserve(invalid) + owner.close() + + def test_complete_native_identity_and_canonical_evidence_required(self) -> None: + complete = native_key() + for field in ("attention_impl", "max_seq_length", "padding_policy", "truncation_policy"): + with self.subTest(field=field): + fields = complete.__dict__ | {field: None} + with self.assertRaises(budget.InvalidAllocationPlan): + budget.AllocationPlan(budget.ProfileKey(**fields), 100, 0, + "empirical_native_profile", "6" * 64, ("7" * 64,)) + for evidence in (("bad",), ("A" * 64,), (True,), ["7" * 64]): + with self.assertRaises((ValueError, budget.InvalidAllocationPlan)): + budget.AllocationPlan(complete, 100, 0, "empirical_native_profile", + "6" * 64, evidence) + with self.assertRaises(ValueError): + budget.ProfileKey("stage", "owner", token_shape=(None,)) + self.assertEqual(budget.ProfileKey("stage", "owner", token_shape=((0,),)).token_shape, ((0,),)) + + def test_resolver_mismatch_is_rejected_before_reservation(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + owner = registry.register_owner(1000, kind="resolver") + with patch.object(budget, "resolve_allocation_plan", return_value=plan(10, "other")): + with budget.allocation_scope(owner) as session: + with self.assertRaises(budget.InvalidAllocationPlan): + session.charge(key("requested")) + self.assertEqual(registry.snapshot()["reserved_known_bytes"], 0) + owner.close() + + +class ReentrantLifetimeControls(unittest.TestCase): + def test_finalizers_reenter_each_lock_without_disabling_gc(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + owner = registry.register_owner(1000, kind="finalizer") + for lock in (budget._registry_lock, registry._lock, budget._model_owners_lock, + budget._retained_buffers_lock): + called = [] + failures = [] + def action() -> None: + with lock: + called.append(True) + def worker() -> None: + try: + witness = Buffer(0) + finalize = weakref.finalize(witness, action) + with lock: + finalize() + except Exception as error: + failures.append(error) + child = threading.Thread(target=worker, daemon=True) + child.start() + child.join(timeout=5) + self.assertFalse(child.is_alive(), "Finalizer reentry deadlocked") + self.assertEqual(failures, []) + self.assertEqual(called, [True]) + self.assertTrue(gc.isenabled()) + owner.close() + + def test_reader_lowered_ceiling_is_recomputed_before_commit(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + owner = registry.register_owner(1000, kind="reader") + def sensor() -> int: + owner.set_limit(200) + return 100 + owner._usage_reader = sensor + with self.assertRaises(budget.MemoryAdmissionRefused): + owner.reserve(plan(101)) + self.assertEqual(registry.snapshot()["lease_count"], 0) + owner.close() + + def test_reader_nested_reservation_is_added_to_actual_total(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + owner = registry.register_owner(1000, kind="reader") + held = [] + entered = False + def sensor() -> int: + nonlocal entered + if not entered: + entered = True + held.append(owner.reserve(plan(600, "nested"))) + return 100 + owner._usage_reader = sensor + with self.assertRaises(budget.MemoryAdmissionRefused): + owner.reserve(plan(301, "outer")) + self.assertEqual(registry.snapshot()["reserved_known_bytes"], 600) + held[0].close() + owner.close() + + def test_reader_closed_owner_cannot_reserve_new_ticket(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + owner = registry.register_owner(1000, kind="reader") + def sensor() -> int: + owner.close() + return 100 + owner._usage_reader = sensor + with self.assertRaises(budget.StaleBudgetOwner): + owner.reserve(plan(1)) + self.assertEqual(registry.snapshot()["reserved_known_bytes"], 0) + + def test_reader_retired_grow_ticket_is_revalidated(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + owner = registry.register_owner(1000, kind="reader") + ticket = owner.reserve(plan(100)) + def sensor() -> int: + ticket.close() + return 100 + owner._usage_reader = sensor + with self.assertRaises(budget.StaleBudgetOwner): + ticket.grow(plan(200)) + self.assertEqual(registry.snapshot()["reserved_known_bytes"], 0) + owner.close() + + def test_nested_growth_recomputes_increment_from_live_ticket(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + owner = registry.register_owner(1000, kind="reader") + ticket = owner.reserve(plan(100)) + entered = False + def sensor() -> int: + nonlocal entered + if not entered: + entered = True + ticket.grow(plan(500)) + return 100 + owner._usage_reader = sensor + ticket.grow(plan(300)) + self.assertEqual(ticket.reserved_known_bytes, 500) + self.assertEqual(registry.snapshot()["reserved_known_bytes"], 500) + ticket.close() + owner.close() + + def test_closing_owner_growth_drains_under_retained_minimum(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + owner = registry.register_owner(500, kind="draining") + ticket = owner.reserve(plan(100)) + owner.close() + ticket.grow(plan(300)) + self.assertEqual(registry.snapshot()["effective_limit_bytes"], 500) + with self.assertRaises(budget.MemoryAdmissionRefused): + ticket.grow(plan(401)) + with self.assertRaises(budget.StaleBudgetOwner): + owner.reserve(plan(0)) + ticket.close() + self.assertEqual(registry.snapshot()["owner_count"], 0) + + def test_fallback_reader_is_followed_by_deadline_validation(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + owner = registry.register_owner(0, kind="disabled") + held = [] + calls = [] + def deadline() -> None: + calls.append("deadline") + if len(calls) == 2: + held.append(registry.register_owner(1000, kind="late finite")) + def sensor() -> int: + calls.append("sensor") + return 100 + owner._usage_reader = sensor + ticket = owner.reserve(plan(1), deadline=deadline) + self.assertEqual(calls, ["deadline", "deadline", "sensor", "deadline"]) + ticket.close() + owner.close() + held[0].close() + + def test_duplicate_model_owner_closes_outside_global_map_lock(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + original = registry.register_owner(1000, kind="model") + duplicate = registry.register_owner(1000, kind="duplicate") + model = Buffer(0) + budget.bind_model_owner(model, original) + observed = [] + close = duplicate.close + def checked_close() -> None: + acquired = [] + def worker() -> None: + got_lock = budget._model_owners_lock.acquire(timeout=1) + acquired.append(got_lock) + if got_lock: + budget._model_owners_lock.release() + child = threading.Thread(target=worker, daemon=True) + child.start() + child.join(timeout=2) + observed.append(acquired) + close() + duplicate.close = checked_close + with self.assertRaises(budget.InvalidAllocationPlan): + budget.bind_model_owner(model, duplicate) + self.assertEqual(observed, [[True]]) + self.assertIs(budget.model_owner(model), original) + del model + gc.collect() + + +class CooperativeBorrowControls(unittest.TestCase): + def test_active_owner_is_sensor_free_existing_positive_minimum(self) -> None: + def forbidden() -> int: + self.fail("Borrow selection must not sample RSS") + registry = budget.ProcessRegistry(usage_reader=forbidden) + large = registry.register_owner(1000, kind="large") + disabled = registry.register_owner(0, kind="disabled") + small = registry.register_owner(500, kind="small") + self.assertIs(registry.active_owner(), small) + self.assertEqual(registry.snapshot()["owner_count"], 3) + small.close() + self.assertIs(registry.active_owner(), large) + disabled.close() + self.assertIs(registry.active_owner(), large) + large.close() + self.assertIsNone(registry.active_owner()) + + def test_closing_owner_is_not_borrowed_but_keeps_draining_ceiling(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + large = registry.register_owner(1000, kind="large") + small = registry.register_owner(500, kind="small") + held = small.reserve(plan(1)) + small.close() + self.assertIs(registry.active_owner(), large) + self.assertEqual(registry.snapshot()["effective_limit_bytes"], 500) + with self.assertRaises(budget.MemoryAdmissionRefused): + large.reserve(plan(400)) + held.close() + self.assertEqual(registry.snapshot()["effective_limit_bytes"], 1000) + large.close() + + def test_cooperative_scope_reuses_exact_current_session(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + owner = registry.register_owner(1000, kind="current") + with budget.allocation_scope(owner) as current: + with patch.object(budget, "get_process_registry", side_effect=AssertionError("No registry lookup")): + with budget.cooperative_scope() as borrowed: + self.assertIs(borrowed, current) + self.assertIs(budget.current_session(), current) + owner.close() + + def test_cooperative_scope_enrolls_no_owner_and_keeps_legacy_none(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + with patch.object(budget, "get_process_registry", return_value=registry): + with budget.cooperative_scope() as legacy: + self.assertIsNone(legacy) + owner = registry.register_owner(1000, kind="existing") + with budget.cooperative_scope() as borrowed: + self.assertIs(borrowed.owner, owner) + self.assertEqual(registry.snapshot()["owner_count"], 1) + borrowed.charge(key("borrowed"), known_bytes=1) + self.assertEqual(registry.snapshot()["reserved_known_bytes"], 0) + owner.close() + + def test_closed_borrow_refuses_before_deadline_or_reader(self) -> None: + def forbidden() -> int: + self.fail("Retired owner must be checked before external callbacks") + registry = budget.ProcessRegistry(usage_reader=forbidden) + owner = registry.register_owner(1000, kind="closed") + selected = registry.active_owner() + owner.close() + with self.assertRaises(budget.StaleBudgetOwner): + selected.reserve(plan(1), deadline=forbidden) + self.assertEqual(registry.snapshot()["lease_count"], 0) + + def test_actual_client_transport_borrows_through_socket_close(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + owner = registry.register_owner(1000, kind="client") + observed = [] + class SyntheticSocket: + def settimeout(self, timeout: float) -> None: + self.assert_scope() + def sendall(self, data: bytes) -> None: + self.assert_scope() + def close(self) -> None: + self.assert_scope() + observed.append("closed") + def assert_scope(self) -> None: + observed.append(budget.current_session().owner) + socket = SyntheticSocket() + source = _PATH.with_name("model_client.py") + tree = ast.parse(source.read_bytes()) + function = next(node for node in tree.body if isinstance(node, ast.FunctionDef) + and node.name == "_send_request") + namespace = {"json": json, "struct": struct, "_HEADER_FMT": "!I", "_HEADER_SIZE": 4, + "_REQUEST_TIMEOUT": 120, "_remaining": lambda deadline: 120, + "_connect": lambda deadline: socket, + "_receive_response": lambda socket, deadline: {"ok": True}} + code = compile(ast.Module(body=[function], type_ignores=[]), str(source), "exec", + flags=__future__.annotations.compiler_flag) + exec(code, namespace) + package = ModuleType("truememory") + package.memory_budget = budget + with patch.dict(sys.modules, {"truememory": package, "truememory.memory_budget": budget}), \ + patch.object(budget, "get_process_registry", return_value=registry): + result = namespace["_send_request"]({"synthetic": 1}) + self.assertEqual(result, {"ok": True}) + self.assertEqual(observed, [owner, owner, owner, "closed"]) + self.assertEqual(registry.snapshot()["owner_count"], 1) + self.assertEqual(registry.snapshot()["reserved_known_bytes"], 0) + self.assertIsNone(budget.current_session()) + owner.close() + + +class ActualResultMaterializationControls(unittest.TestCase): + def functions(self, events: list[str]) -> dict: + class Array: + ndim = 2 + shape = (2, 3) + + def asarray(value: object, dtype: object) -> object: + events.append("asarray") + return value if isinstance(value, Array) else Array() + + source = _PATH.with_name("model_server.py") + tree = ast.parse(source.read_bytes()) + functions = [node for node in tree.body if isinstance(node, ast.FunctionDef) + and node.name == "_checked_batch"] + namespace = {"np": SimpleNamespace(ndarray=Array, float32="float32", asarray=asarray), + "_check_result_size": lambda shape, field: events.append("size check")} + code = compile(ast.Module(body=functions, type_ignores=[]), str(source), "exec", + flags=__future__.annotations.compiler_flag) + exec(code, namespace) + return namespace + + def test_actual_nonarray_materialization_is_preceded_by_profile_hook(self) -> None: + events = [] + functions = self.functions(events) + functions["_checked_batch"]([[0.0] * 3] * 2, 2, 2, "vectors", + admit=lambda shape, cast: events.append("result admission"), + before_materialize=lambda: events.append("materialization admission")) + self.assertEqual(events, ["materialization admission", "asarray", "size check", + "result admission", "asarray"]) + + def test_actual_refusal_prevents_nonarray_allocation(self) -> None: + events = [] + functions = self.functions(events) + def refused() -> None: + raise budget.MemoryAdmissionRefused("synthetic materialization demand") + with self.assertRaises(budget.MemoryAdmissionRefused): + functions["_checked_batch"]([[0.0] * 3] * 2, 2, 2, "vectors", + before_materialize=refused) + self.assertEqual(events, []) + + def test_actual_legacy_array_path_does_not_require_new_dtype_probe(self) -> None: + events = [] + functions = self.functions(events) + array = functions["np"].ndarray() + result = functions["_checked_batch"](array, 2, 2, "vectors") + self.assertIs(result, array) + self.assertEqual(events, ["size check", "asarray"]) + + +class LocalRoutingControls(unittest.TestCase): + def test_inherited_policy_constructor_and_reflective_mutation_refuse(self) -> None: + class InheritedPolicy(budget.LocalMemoryPolicy): + pass + with self.assertRaises(budget.InvalidAllocationPlan): + InheritedPolicy() + self.assertIs(budget.validate_local_policy(budget.DEFAULT_LOCAL_MEMORY_POLICY), + budget.DEFAULT_LOCAL_MEMORY_POLICY) + for field, value in (("version", True), ("mode", "disabled")): + policy = budget.LocalMemoryPolicy() + object.__setattr__(policy, field, value) + with self.subTest(field=field), self.assertRaises(budget.InvalidAllocationPlan): + budget.validate_local_policy(policy) + + def test_policy_requires_exact_fields_type_and_version(self) -> None: + policy = budget.LocalMemoryPolicy() + self.assertIs(budget.validate_local_policy(policy), policy) + for fields in ({"version": True}, {"version": 2}, {"mode": None}, {"mode": "disabled"}): + with self.subTest(fields=fields), self.assertRaises(budget.InvalidAllocationPlan): + budget.LocalMemoryPolicy(**fields) + class SubPolicy(budget.LocalMemoryPolicy): + def __post_init__(self) -> None: + pass + for invalid in (None, {}, SubPolicy(version=True)): + with self.subTest(invalid=invalid), self.assertRaises(budget.InvalidAllocationPlan): + budget.validate_local_policy(invalid) + object.__setattr__(policy, "mode", "disabled") + with self.assertRaises(budget.InvalidAllocationPlan): + budget.validate_local_policy(policy) + + def test_configured_factory_validates_before_sensor_and_preserves_kind(self) -> None: + with patch.object(budget, "new_local_owner") as factory: + for policy, kind in ((None, "local"), (budget.LocalMemoryPolicy(), ""), + (budget.LocalMemoryPolicy(), None)): + with self.subTest(kind=kind), self.assertRaises(budget.InvalidAllocationPlan): + budget.configured_local_owner(policy, kind=kind) + factory.assert_not_called() + result = budget.configured_local_owner(budget.LocalMemoryPolicy(), kind="local_embedding") + self.assertIs(result, factory.return_value) + factory.assert_called_once_with("local_embedding") + + def test_unbound_operation_preserves_exact_current_session(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + owner = registry.register_owner(1000, kind="current") + with patch.object(budget, "get_process_registry", return_value=registry): + with budget.allocation_scope(owner) as current: + with budget.model_operation_scope(object(), key("unbound")) as operation: + self.assertIs(operation, current) + self.assertIs(budget.current_session(), current) + self.assertEqual(registry.snapshot()["owner_count"], 1) + owner.close() + + def test_unbound_operation_borrows_existing_positive_without_enrollment(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + owner = registry.register_owner(1000, kind="existing") + with patch.object(budget, "get_process_registry", return_value=registry): + with budget.model_operation_scope(object(), key("borrowed")) as operation: + self.assertIs(operation.owner, owner) + self.assertEqual(registry.snapshot()["owner_count"], 1) + owner.close() + with budget.model_operation_scope(object(), key("uncovered")) as operation: + self.assertIsNone(operation) + + def test_bound_closed_owner_never_falls_back_to_eligible_owner(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + bound = registry.register_owner(500, kind="bound") + eligible = registry.register_owner(1000, kind="eligible") + model = Buffer(0) + with patch.object(budget, "get_process_registry", return_value=registry): + budget.bind_model_owner(model, bound) + bound.close() + with self.assertRaises(budget.StaleBudgetOwner): + with budget.model_operation_scope(model, key("closed")): + self.fail("Closed bound owner must not enter native operation") + eligible.close() + + def test_operation_validates_key_and_deadline_before_context_lookup(self) -> None: + with patch.object(budget, "model_owner", side_effect=AssertionError("No context lookup")): + for supplied_key, deadline in ((None, None), (key(), 1)): + with self.subTest(deadline=deadline), self.assertRaises(budget.InvalidAllocationPlan): + with budget.model_operation_scope(object(), supplied_key, deadline): + self.fail("Invalid operation cannot enter") + + def test_both_wrappers_preserve_raw_arguments_and_retain_backing(self) -> None: + for name in ("budgeted_encode_with_model_ownership", "budgeted_encode_with_mps_fallback"): + with self.subTest(wrapper=name): + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + owner = registry.register_owner(1000, kind="existing") + model, texts = object(), ["synthetic text"] + result = Buffer(24) + events = [] + def encode(actual_model: object, actual_texts: object, **kwargs: object) -> object: + self.assertIs(actual_model, model) + self.assertIs(actual_texts, texts) + self.assertEqual(kwargs, {"show_progress_bar": False, "batch_size": 7}) + self.assertIsNotNone(budget.current_session()) + events.append("native") + return result + package = ModuleType("truememory") + package.__path__ = [] + mps = ModuleType("truememory.mps_utils") + mps.encode_with_model_ownership = encode + mps.encode_with_mps_fallback = encode + with patch.dict(sys.modules, {"truememory": package, "truememory.mps_utils": mps}), \ + patch.object(budget, "get_process_registry", return_value=registry): + returned = getattr(budget, name)( + model, texts, key("opaque operation"), + native_kwargs={"show_progress_bar": False, "batch_size": 7}, + ) + self.assertIs(returned, result) + self.assertEqual(events, ["native"]) + self.assertEqual(registry.snapshot()["reserved_known_bytes"], 24) + owner.close() + self.assertEqual(registry.snapshot()["effective_limit_bytes"], 1000) + del returned + result = None + gc.collect() + self.assertEqual(registry.snapshot()["owner_count"], 0) + + def test_wrapper_refusal_happens_before_native_call(self) -> None: + registry = budget.ProcessRegistry(usage_reader=lambda: 100) + owner = registry.register_owner(100, kind="full") + package = ModuleType("truememory") + package.__path__ = [] + mps = ModuleType("truememory.mps_utils") + mps.encode_with_model_ownership = lambda *args, **kwargs: self.fail("Native call after refusal") + with patch.dict(sys.modules, {"truememory": package, "truememory.mps_utils": mps}), \ + patch.object(budget, "get_process_registry", return_value=registry): + with self.assertRaises(budget.MemoryAdmissionRefused): + budget.budgeted_encode_with_model_ownership(object(), ["synthetic"], key("refused")) + owner.close() + + def test_prepared_result_payload_admission_precedes_both_actual_encodes(self) -> None: + source = _PATH.with_name("vector_search.py") + module = ast.parse(source.read_text()) + frozen = next(node for node in module.body if isinstance(node, ast.FunctionDef) + and node.name == "_load_frozen_embedding_target") + lease = next(node for node in module.body if isinstance(node, ast.ClassDef) + and node.name == "_PreparedEmbeddingLease") + encode = next(node for node in lease.body if isinstance(node, ast.FunctionDef) and node.name == "encode") + for method in (frozen, encode): + with self.subTest(method=method.name): + charges = [node for node in ast.walk(method) if isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) and node.func.attr == "charge" + and any(keyword.arg == "group" and isinstance(keyword.value, ast.Constant) + and keyword.value.value == "result assembly" for keyword in node.keywords)] + encodes = [node for node in ast.walk(method) if isinstance(node, ast.Call) + and isinstance(node.func, ast.Attribute) and node.func.attr == "encode" + and isinstance(node.func.value, ast.Name) and node.func.value.id == "encoder"] + self.assertEqual(len(charges), 1) + self.assertTrue(encodes) + self.assertLess(charges[0].lineno, max(node.lineno for node in encodes)) + self.assertTrue(any(keyword.arg == "retained_bytes" for keyword in charges[0].keywords)) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_issue_577_model_server_oom.py b/tests/test_issue_577_model_server_oom.py index a441dc65..677cb6db 100644 --- a/tests/test_issue_577_model_server_oom.py +++ b/tests/test_issue_577_model_server_oom.py @@ -807,6 +807,10 @@ def test_issue_577_hooks_pass_short_deadline(self, monkeypatch): ) +class _SyntheticLocalEmbeddingModel: + __slots__ = ("__weakref__",) + + class TestIssue739LocalEmbeddingDevice(unittest.TestCase): """Exercise real local routing and device resolution with synthetic factories.""" @@ -819,7 +823,7 @@ def setUp(self) -> None: self.static_calls: list[tuple[str, dict]] = [] self.availability_calls: list[str] = [] self.available = {"cuda": True, "mps": True} - self.local_model = object() + self.local_model = _SyntheticLocalEmbeddingModel() self.proxy = object() def transformer(name: str, **kwargs: object) -> object: @@ -869,6 +873,7 @@ def available(kind: str) -> bool: def load(self, model_id: str, override: str | None) -> object: self.vector_search._model = None + self.local_model = _SyntheticLocalEmbeddingModel() self.vector_search.EMBEDDING_MODEL = model_id self.vector_search._embedding_dim = -1 self.transformer_calls.clear() diff --git a/truememory/__init__.py b/truememory/__init__.py index 85d107ab..afa56795 100644 --- a/truememory/__init__.py +++ b/truememory/__init__.py @@ -126,6 +126,7 @@ "predictive", "query_classifier", "reranker", "hyde", "clustering", "tier_config", "embedding_target", "maintenance", "rebuild_source", "tier_switch", + "memory_budget", ] diff --git a/truememory/clustering.py b/truememory/clustering.py index bffe7b82..40a32aa4 100644 --- a/truememory/clustering.py +++ b/truememory/clustering.py @@ -38,6 +38,10 @@ import numpy as np +from truememory.memory_budget import LocalMemoryPolicy + +_LOCAL_MEMORY_POLICY = LocalMemoryPolicy() + logger = logging.getLogger(__name__) @@ -396,8 +400,20 @@ def search_clustered( from truememory.vector_search import get_model from truememory.mps_utils import encode_with_model_ownership + configured_memory = "_LOCAL_MEMORY_POLICY" in globals() + if configured_memory: + from truememory.memory_budget import ( + ProfileKey, budgeted_encode_with_model_ownership, validate_local_policy, + ) + validate_local_policy(globals()["_LOCAL_MEMORY_POLICY"]) model = get_model() - query_vec = encode_with_model_ownership(model, [query])[0].astype(np.float32) + if configured_memory: + values = budgeted_encode_with_model_ownership( + model, [query], ProfileKey("cluster query embedding", "local_embedding", input_item_count=1), + ) + else: + values = encode_with_model_ownership(model, [query]) + query_vec = values[0].astype(np.float32) # Keep centroids, memberships, vectors and winner content in one snapshot. # Encoding completes before we open an owned read transaction. diff --git a/truememory/hybrid.py b/truememory/hybrid.py index ea3ac220..20768c4b 100644 --- a/truememory/hybrid.py +++ b/truememory/hybrid.py @@ -41,8 +41,11 @@ import sqlite3 from collections import defaultdict +from truememory.memory_budget import LocalMemoryPolicy from truememory.tier_switch.runtime import database_operation +_LOCAL_MEMORY_POLICY = LocalMemoryPolicy() + log = logging.getLogger(__name__) @@ -191,8 +194,20 @@ def search_hybrid( # ------------------------------------------------------------------ from truememory.vector_search import get_model, serialize_f32 from truememory.mps_utils import encode_with_mps_fallback + configured_memory = "_LOCAL_MEMORY_POLICY" in globals() + if configured_memory: + from truememory.memory_budget import ( + ProfileKey, budgeted_encode_with_mps_fallback, validate_local_policy, + ) + validate_local_policy(globals()["_LOCAL_MEMORY_POLICY"]) _q_model = get_model() - _q_emb = encode_with_mps_fallback(_q_model, [query])[0] + if configured_memory: + _q_values = budgeted_encode_with_mps_fallback( + _q_model, [query], ProfileKey("hybrid query embedding", "local_embedding", input_item_count=1), + ) + else: + _q_values = encode_with_mps_fallback(_q_model, [query]) + _q_emb = _q_values[0] _q_blob = serialize_f32(_q_emb) fts_results = search_fts(conn, query, limit=_CANDIDATE_POOL, include_directives=include_directives) diff --git a/truememory/ingest/completion.py b/truememory/ingest/completion.py index afc4d7f8..2c44593b 100644 --- a/truememory/ingest/completion.py +++ b/truememory/ingest/completion.py @@ -405,22 +405,36 @@ def admit_capture(source: str, session_id: str, user_id: str = "", db_path: str if original.is_symlink(): raise CompletionError("source_link") try: - fd = os.open(original, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)) - with os.fdopen(fd, "rb") as handle: - before_stat = os.fstat(handle.fileno()) - _source_owned(before_stat, handle.fileno()) - before = _file_identity(before_stat) - if before[2] > MAX_CAPTURE_BYTES: - raise CompletionError("capture_limit") - data = handle.read(MAX_CAPTURE_BYTES + 1) - after_stat = os.fstat(handle.fileno()) - _source_owned(after_stat, handle.fileno()) - after = _file_identity(after_stat) - fd = os.open(original, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0)) - with os.fdopen(fd, "rb") as handle: - path_stat = os.fstat(handle.fileno()) - _source_owned(path_stat, handle.fileno()) - path_after = _file_identity(path_stat) + fd = os.open(original, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) + | getattr(os, "O_NONBLOCK", 0)) + try: + before_stat = os.fstat(fd) + _source_owned(before_stat, fd) + handle = os.fdopen(fd, "rb") + fd = None + with handle: + before = _file_identity(before_stat) + if before[2] > MAX_CAPTURE_BYTES: + raise CompletionError("capture_limit") + data = handle.read(MAX_CAPTURE_BYTES + 1) + after_stat = os.fstat(handle.fileno()) + _source_owned(after_stat, handle.fileno()) + after = _file_identity(after_stat) + finally: + if fd is not None: + os.close(fd) + fd = os.open(original, os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) + | getattr(os, "O_NONBLOCK", 0)) + try: + path_stat = os.fstat(fd) + _source_owned(path_stat, fd) + handle = os.fdopen(fd, "rb") + fd = None + with handle: + path_after = _file_identity(path_stat) + finally: + if fd is not None: + os.close(fd) except CompletionError: raise except OSError as error: diff --git a/truememory/ingest/encoding_gate.py b/truememory/ingest/encoding_gate.py index 14e17a30..426222ec 100644 --- a/truememory/ingest/encoding_gate.py +++ b/truememory/ingest/encoding_gate.py @@ -61,6 +61,8 @@ import numpy as np +from truememory.memory_budget import LocalMemoryPolicy + # Shared correction/update marker vocabulary (issue #649) — single source # of truth used by BOTH this gate and the dedup stage so the two never # disagree about what counts as a correction. @@ -69,6 +71,8 @@ has_update_markers as _has_update_markers, ) +_LOCAL_MEMORY_POLICY = LocalMemoryPolicy() + log = logging.getLogger(__name__) @@ -501,6 +505,13 @@ def _compute_prediction_error(self, fact: str) -> float: Independent of novelty (r=0.30) and salience (r=0.23). """ + configured_memory = "_LOCAL_MEMORY_POLICY" in globals() + if configured_memory: + from truememory.memory_budget import ( + InvalidAllocationPlan, MemoryAdmissionRefused, ProfileKey, StaleBudgetOwner, + budgeted_encode_with_model_ownership, validate_local_policy, + ) + validate_local_policy(globals()["_LOCAL_MEMORY_POLICY"]) EncodingGate._refresh_serving_generation(self) if not fact or fact.lower().strip().rstrip("!?.… ") in _PE_NOISE: return 0.0 @@ -520,6 +531,8 @@ def _compute_prediction_error(self, fact: str) -> float: from truememory.vector_search import get_model self._embed_model = get_model() except Exception as e: + if configured_memory and isinstance(e, (MemoryAdmissionRefused, StaleBudgetOwner, InvalidAllocationPlan)): + raise from truememory.tier_switch.runtime import raise_if_serving_rejection raise_if_serving_rejection(e) # PE model failed to load — degrade open (#585) @@ -535,10 +548,16 @@ def _compute_prediction_error(self, fact: str) -> float: try: from truememory.mps_utils import encode_with_model_ownership - embeddings = encode_with_model_ownership( - model, [fact, mem_content, fact + " [SEP] " + mem_content, - mem_content + " [SEP] " + mem_content], - ) + texts = [fact, mem_content, fact + " [SEP] " + mem_content, + mem_content + " [SEP] " + mem_content] + if configured_memory: + validate_local_policy(globals()["_LOCAL_MEMORY_POLICY"]) + embeddings = budgeted_encode_with_model_ownership( + model, texts, ProfileKey("encoding gate prediction embedding", "local_embedding", + input_item_count=4), + ) + else: + embeddings = encode_with_model_ownership(model, texts) emb_fact = embeddings[0] emb_mem = embeddings[1] @@ -563,6 +582,8 @@ def _compute_prediction_error(self, fact: str) -> float: return pe except Exception as e: + if configured_memory and isinstance(e, (MemoryAdmissionRefused, StaleBudgetOwner, InvalidAllocationPlan)): + raise from truememory.tier_switch.runtime import raise_if_serving_rejection raise_if_serving_rejection(e) log.debug("PE embedding scorer failed: %s", e) diff --git a/truememory/memory_bootstrap.py b/truememory/memory_bootstrap.py new file mode 100644 index 00000000..04c3ef1d --- /dev/null +++ b/truememory/memory_bootstrap.py @@ -0,0 +1,133 @@ +"""Minimal stdlib-only owned Linux cgroup readback. + +This file is intended to be addressed by absolute path. Phase B launch activation +is held; importing it neither enrolls a process nor changes an OS limit. +""" +from __future__ import annotations + +from dataclasses import dataclass +import os +from pathlib import Path +import stat + + +_MAX_BYTES = 2**63 - 1 +_READ_LIMIT = 65536 + + +class EnforcementUnavailable(RuntimeError): + """A requested owned controller scope cannot be verified.""" + + +@dataclass(frozen=True) +class LinuxScopeExpectation: + scope: str + pid: int + uid: int + device: int + inode: int + memory_max: int + swap_max: int + + def __post_init__(self) -> None: + if type(self.scope) is not str or not self.scope or not Path(self.scope).is_absolute(): + raise ValueError("scope must be an explicit absolute path") + for name in ("pid", "uid", "device", "inode", "memory_max", "swap_max"): + value = getattr(self, name) + if type(value) is not int or not 0 <= value <= _MAX_BYTES: + raise ValueError(f"{name} must be a nonnegative signed64 integer") + if self.pid <= 0 or self.memory_max <= 0: + raise ValueError("pid and memory_max must be positive") + + +@dataclass(frozen=True) +class LinuxScopeReadback: + expectation: LinuxScopeExpectation + members: tuple[int, ...] + memory_current: int + memory_events: tuple[tuple[str, int], ...] + metric: str = "controller_file_readback_unverified" + physical_hard_cap: bool = False + lifetime_enforcement_proven: bool = False + + +def _read_owned_file(directory: int, name: str) -> str: + flags = (os.O_RDONLY | getattr(os, "O_NOFOLLOW", 0) | getattr(os, "O_CLOEXEC", 0) + | getattr(os, "O_NONBLOCK", 0)) + descriptor = os.open(name, flags, dir_fd=directory) + try: + metadata = os.fstat(descriptor) + if not stat.S_ISREG(metadata.st_mode): + raise EnforcementUnavailable(f"{name} is not a regular controller file") + chunks = bytearray() + while len(chunks) <= _READ_LIMIT: + chunk = os.read(descriptor, min(4096, _READ_LIMIT + 1 - len(chunks))) + if not chunk: + break + chunks.extend(chunk) + if len(chunks) > _READ_LIMIT: + raise EnforcementUnavailable(f"{name} exceeds the readback bound") + return bytes(chunks).decode("ascii").strip() + finally: + os.close(descriptor) + + +def _integer(text: str, name: str) -> int: + if not text.isascii() or not text.isdecimal(): + raise EnforcementUnavailable(f"{name} is not a finite integer") + value = int(text) + if value > _MAX_BYTES: + raise EnforcementUnavailable(f"{name} exceeds signed64") + return value + + +def inspect_owned_linux_scope(expectation: LinuxScopeExpectation) -> LinuxScopeReadback: + """Read an already prepared owned scope, without creating or modifying it. + + This is a point-in-time ownership/readback check, not an installed launch or + kernel allocation witness. Filesystem/controller authenticity must be proved + by a separate root-owned witness. The separate swap domain is not a unified + physical process footprint. + """ + if type(expectation) is not LinuxScopeExpectation: + raise TypeError("Expected an exact immutable LinuxScopeExpectation") + LinuxScopeExpectation.__post_init__(expectation) + if not hasattr(os, "O_NOFOLLOW") or not hasattr(os, "O_DIRECTORY"): + raise EnforcementUnavailable("No no-follow directory ownership primitive") + directory = os.open(expectation.scope, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW + | getattr(os, "O_CLOEXEC", 0)) + try: + metadata = os.fstat(directory) + if (metadata.st_uid != expectation.uid or metadata.st_dev != expectation.device + or metadata.st_ino != expectation.inode): + raise EnforcementUnavailable("Owned controller directory identity changed") + maximum = _integer(_read_owned_file(directory, "memory.max"), "memory.max") + swap = _integer(_read_owned_file(directory, "memory.swap.max"), "memory.swap.max") + if maximum != expectation.memory_max or swap != expectation.swap_max: + raise EnforcementUnavailable("Finite controller limits differ from the owned receipt") + raw_members = _read_owned_file(directory, "cgroup.procs").splitlines() + members = tuple(_integer(value, "cgroup.procs") for value in raw_members) + if members != (expectation.pid,): + raise EnforcementUnavailable("Owned scope contains a missing or unrelated process") + current = _integer(_read_owned_file(directory, "memory.current"), "memory.current") + events = [] + for line in _read_owned_file(directory, "memory.events").splitlines(): + parts = line.split() + if len(parts) != 2 or not parts[0].isascii(): + raise EnforcementUnavailable("Malformed controller event readback") + events.append((parts[0], _integer(parts[1], "memory.events"))) + return LinuxScopeReadback(expectation, members, current, tuple(events)) + except (OSError, UnicodeError, ValueError) as error: + raise EnforcementUnavailable("Owned Linux scope readback is unavailable") from error + finally: + os.close(directory) + + +def main() -> None: + raise SystemExit( + "Phase B launch activation is held pending a root-approved owned kernel witness" + ) + + +if __name__ == "__main__": + main() diff --git a/truememory/memory_budget.py b/truememory/memory_budget.py new file mode 100644 index 00000000..48516ad3 --- /dev/null +++ b/truememory/memory_budget.py @@ -0,0 +1,983 @@ +"""Process-local future allocation admission; this is not an OS physical cap.""" + +from __future__ import annotations + +from collections.abc import Callable, Iterator +from contextlib import contextmanager +from dataclasses import dataclass, replace +import os +import threading +from typing import Literal +import weakref + +_MAX_BYTES = 2**63 - 1 +Basis = Literal["exact_owned_payload", "empirical_runtime_profile", + "empirical_native_profile", "unprofiled"] + + +class MemoryAdmissionRefused(RuntimeError): + """An enabled process ceiling cannot admit an allocation.""" + + +class InvalidAllocationPlan(ValueError): + """A resolver returned an invalid or mismatched allocation plan.""" + + +class StaleBudgetOwner(RuntimeError): + """An owner belongs to another PID or retired registry epoch.""" + + +@dataclass(frozen=True) +class LocalMemoryPolicy: + version: int = 1 + mode: str = "automatic" + + def __post_init__(self) -> None: + if type(self) is not LocalMemoryPolicy: + raise InvalidAllocationPlan("Local memory policy requires the exact policy type") + if type(self.version) is not int or self.version != 1: + raise InvalidAllocationPlan("Local memory policy requires version 1") + if type(self.mode) is not str or self.mode != "automatic": + raise InvalidAllocationPlan("Local memory policy requires automatic mode") + + +def validate_local_policy(policy: object) -> LocalMemoryPolicy: + if type(policy) is not LocalMemoryPolicy: + raise InvalidAllocationPlan("Local memory policy requires the exact policy type") + LocalMemoryPolicy.__post_init__(policy) + return policy + + +DEFAULT_LOCAL_MEMORY_POLICY = LocalMemoryPolicy() + + +def _bytes(value: object, name: str) -> int: + if type(value) is not int or not 0 <= value <= _MAX_BYTES: + raise ValueError(f"{name} must be a nonnegative signed64 integer") + return value + + +def _sum_bytes(left: int, right: int) -> int: + if left > _MAX_BYTES - right: + raise MemoryAdmissionRefused("Future allocation accounting overflow") + return left + right + + +def _shape(value: object, name: str) -> None: + if value is None: + return + if type(value) is not tuple: + raise ValueError(f"{name} must be an immutable tuple") + product = 1 + for dimension in value: + dimension = _bytes(dimension, name) + if dimension and product > _MAX_BYTES // dimension: + raise ValueError(f"{name} product exceeds signed64") + product *= dimension + + +def _pin(value: object, name: str) -> None: + if value is not None and (type(value) is not str or len(value) != 64 + or any(char not in "0123456789abcdef" for char in value)): + raise ValueError(f"{name} must be a canonical SHA256 or None") + + +@dataclass(frozen=True) +class ProfileKey: + stage: str + owner_kind: str + runtime_fingerprint: str | None = None + source_fingerprint: str | None = None + model_identity: tuple[str, int, str] | None = None + resolved_revision: str | None = None + asset_manifest_sha256: str | None = None + device: str | None = None + backend: str | None = None + dtype: str | None = None + attention_impl: str | None = None + token_shape: tuple[tuple[int, ...], ...] | None = None + native_tensor_shape: tuple[int, ...] | None = None + batch_size: int | None = None + max_seq_length: int | None = None + padding_policy: str | None = None + truncation_policy: str | None = None + live_residency_signature: tuple[tuple[str, str, str, str], ...] = () + thread_config_fingerprint: str | None = None + input_item_count: int | None = None + wire_signature: str | None = None + + def __post_init__(self) -> None: + if type(self.stage) is not str or not self.stage: + raise ValueError("stage must be a nonempty string") + if type(self.owner_kind) is not str or not self.owner_kind: + raise ValueError("owner_kind must be a nonempty string") + for name in ("runtime_fingerprint", "source_fingerprint", "resolved_revision", + "asset_manifest_sha256", "device", "backend", "dtype", "attention_impl", + "padding_policy", "truncation_policy", "thread_config_fingerprint"): + value = getattr(self, name) + if value is not None and (type(value) is not str or not value): + raise ValueError(f"{name} must be a nonempty string or None") + for name in ("runtime_fingerprint", "source_fingerprint", "asset_manifest_sha256", + "thread_config_fingerprint", "wire_signature"): + _pin(getattr(self, name), name) + if self.model_identity is not None: + identity = self.model_identity + if (type(identity) is not tuple or len(identity) != 3 + or type(identity[0]) is not str or not identity[0] + or type(identity[2]) is not str or not identity[2]): + raise ValueError("model_identity must be an immutable identity triple") + _bytes(identity[1], "model dimension") + _shape(self.native_tensor_shape, "native_tensor_shape") + if self.token_shape is not None: + if type(self.token_shape) is not tuple: + raise ValueError("token_shape must be immutable") + for shape in self.token_shape: + if shape is None: + raise ValueError("Inner token shapes cannot be None") + _shape(shape, "token_shape") + for name in ("batch_size", "max_seq_length", "input_item_count"): + value = getattr(self, name) + if value is not None: + _bytes(value, name) + if type(self.live_residency_signature) is not tuple: + raise ValueError("live_residency_signature must be immutable") + for identity in self.live_residency_signature: + if (type(identity) is not tuple or len(identity) != 4 + or not all(type(part) is str for part in identity)): + raise ValueError("residency entries must be immutable string quadruples") + + +@dataclass(frozen=True) +class AllocationPlan: + key: ProfileKey + future_bytes: int | None + retained_bytes: int | None = 0 + basis: Basis = "exact_owned_payload" + profile_id: str | None = None + evidence_sha256: tuple[str, ...] = () + unknown_components: tuple[str, ...] = () + reason: str | None = None + + def __post_init__(self) -> None: + if type(self.key) is not ProfileKey: + raise InvalidAllocationPlan("key must be a ProfileKey") + ProfileKey.__post_init__(self.key) + if type(self.basis) is not str or self.basis not in ("exact_owned_payload", "empirical_runtime_profile", + "empirical_native_profile", "unprofiled"): + raise InvalidAllocationPlan("Unsupported allocation basis") + for name in ("evidence_sha256", "unknown_components"): + value = getattr(self, name) + if type(value) is not tuple or not all(type(item) is str and item for item in value): + raise InvalidAllocationPlan(f"{name} must be immutable strings") + _pin(self.profile_id, "profile_id") + for evidence in self.evidence_sha256: + _pin(evidence, "evidence SHA256") + if self.reason is not None and (type(self.reason) is not str or not self.reason): + raise InvalidAllocationPlan("reason must be a nonempty exact string or None") + if self.future_bytes is None: + if (self.basis != "unprofiled" or self.retained_bytes is not None + or not self.reason or not self.unknown_components): + raise InvalidAllocationPlan("Unknown demand requires None bytes and explicit coverage") + else: + _bytes(self.future_bytes, "future_bytes") + if self.retained_bytes is None: + raise InvalidAllocationPlan("Known demand needs a retained portion") + _bytes(self.retained_bytes, "retained_bytes") + if self.retained_bytes > self.future_bytes or self.basis == "unprofiled": + raise InvalidAllocationPlan("Invalid retained portion or known-demand basis") + if self.basis.startswith("empirical"): + if not self.profile_id or not self.evidence_sha256: + raise InvalidAllocationPlan("Empirical demand needs immutable profile evidence") + if self.key.runtime_fingerprint is None or self.key.source_fingerprint is None: + raise InvalidAllocationPlan("Empirical demand needs exact runtime and source pins") + if self.basis == "empirical_native_profile" and any(value is None for value in ( + self.key.model_identity, self.key.resolved_revision, self.key.asset_manifest_sha256, + self.key.device, self.key.backend, self.key.dtype, self.key.token_shape, + self.key.native_tensor_shape, self.key.batch_size, self.key.thread_config_fingerprint, + self.key.attention_impl, self.key.max_seq_length, + self.key.padding_policy, self.key.truncation_policy, + )): + raise InvalidAllocationPlan("Native profile coverage is incomplete") + + +def _validate_plan(plan: object) -> AllocationPlan: + if type(plan) is not AllocationPlan: + raise InvalidAllocationPlan("AllocationPlan subclasses are not accepted") + AllocationPlan.__post_init__(plan) + return plan + + +def _validate_key(key: object) -> ProfileKey: + if type(key) is not ProfileKey: + raise InvalidAllocationPlan("ProfileKey subclasses are not accepted") + ProfileKey.__post_init__(key) + return key + + +@dataclass(frozen=True) +class EnforcementCapability: + status: Literal["unsupported", "unverified", "verified"] = "unsupported" + metric: str = "none" + limit_bytes: int | None = None + scope: str | None = None + evidence_sha256: tuple[str, ...] = () + physical_hard_cap: bool = False + reason: str = "No native enforcement witness has been accepted" + + def __post_init__(self) -> None: + if self.status not in ("unsupported", "unverified", "verified"): + raise ValueError("Invalid enforcement status") + if self.limit_bytes is not None: + _bytes(self.limit_bytes, "limit_bytes") + if self.physical_hard_cap: + raise ValueError("Phase A cannot certify a unified physical hard cap") + if self.status == "verified" and (not self.scope or not self.evidence_sha256): + raise ValueError("Verified capability requires owned-scope evidence") + + +@dataclass +class _OwnerRecord: + limit: int + kind: str + handle: weakref.ReferenceType[BudgetOwner] + leases: set[int] + closing: bool = False + + +@dataclass +class _LeaseRecord: + owner: int + reserved: int + + +def _default_usage() -> object: + try: + import psutil + return psutil.Process().memory_info().rss + except (ImportError, AttributeError, OSError, RuntimeError, ValueError, TypeError): + return None + + +class ProcessRegistry: + def __init__(self, *, usage_reader: Callable[[], object] | None = None) -> None: + if usage_reader is not None and not callable(usage_reader): + raise ValueError("usage_reader must be callable or None") + self.pid = os.getpid() + self.epoch = object() + self._lock = threading.RLock() + self._usage_reader = _default_usage if usage_reader is None else usage_reader + self._owners: dict[int, _OwnerRecord] = {} + self._leases: dict[int, _LeaseRecord] = {} + self._next_owner = 0 + self._owner_generation = 0 + self._next_lease = 0 + self._future = 0 + self._high_water = 0 + self._profiles: dict[ProfileKey, AllocationPlan] = {} + self._runtime_context: ProfileKey | None = None + self._model_context: dict[tuple[tuple[str, int, str], str], ProfileKey] = {} + self._unknown_stages: set[str] = set() + self._coverage_truncated = False + self._enrolled = False + self.capability = EnforcementCapability() + + def bind_identity_context( + self, runtime: ProfileKey, models: tuple[ProfileKey, ...] = (), + ) -> None: + self._identity() + runtime = _validate_key(runtime) + if any(value is None for value in (runtime.runtime_fingerprint, + runtime.source_fingerprint, + runtime.thread_config_fingerprint)): + raise InvalidAllocationPlan("Runtime context needs verified exact pins") + if type(models) is not tuple: + raise InvalidAllocationPlan("Model contexts must be immutable") + table: dict[tuple[tuple[str, int, str], str], ProfileKey] = {} + for model in models: + model = _validate_key(model) + if model.model_identity is None or model.device is None: + raise InvalidAllocationPlan("Model context needs captured identity and device") + identity = (model.model_identity, model.device) + if identity in table: + raise InvalidAllocationPlan("Duplicate exact model context") + table[identity] = replace(model) + with self._lock: + if self._enrolled: + raise InvalidAllocationPlan("Identity context binds before enrollment only") + self._runtime_context = replace(runtime) + self._model_context = table + + def enrich_key(self, key: ProfileKey) -> ProfileKey: + self._identity() + key = _validate_key(key) + with self._lock: + runtime = self._runtime_context + model = self._model_context.get((key.model_identity, key.device)) + updates: dict[str, object] = {} + if runtime is not None: + for name in ("runtime_fingerprint", "source_fingerprint", "thread_config_fingerprint"): + supplied, actual = getattr(runtime, name), getattr(key, name) + if actual is not None and supplied != actual: + raise InvalidAllocationPlan("Runtime identity changed before allocation") + updates[name] = supplied + if model is not None: + for name in ("resolved_revision", "asset_manifest_sha256", "backend", "dtype", + "attention_impl", "max_seq_length", "padding_policy", "truncation_policy"): + supplied, actual = getattr(model, name), getattr(key, name) + if actual is not None and supplied != actual: + raise InvalidAllocationPlan("Model identity changed before allocation") + updates[name] = supplied + # Input tokens/padded native shapes are never supplied by a static identity manifest. + return replace(key, **updates) if updates else key + + def _identity(self) -> None: + if self.pid != os.getpid(): + raise StaleBudgetOwner("Inherited registry is not the child's registry") + + def bind_profiles(self, profiles: tuple[AllocationPlan, ...]) -> None: + self._identity() + if type(profiles) is not tuple: + raise InvalidAllocationPlan("Profiles must be an immutable tuple") + table: dict[ProfileKey, AllocationPlan] = {} + for plan in profiles: + plan = _validate_plan(plan) + if plan.future_bytes is None: + raise InvalidAllocationPlan("Only validated known plans can be bound") + if plan.key in table: + raise InvalidAllocationPlan("Duplicate exact profile key") + table[plan.key] = replace(plan, key=replace(plan.key)) + with self._lock: + if self._enrolled: + raise InvalidAllocationPlan("Profiles bind only before first owner enrollment") + self._profiles = table + + def resolve_profile(self, key: ProfileKey) -> AllocationPlan: + self._identity() + _validate_key(key) + with self._lock: + plan = self._profiles.get(key) + if plan is not None: + return _validate_plan(plan) + return AllocationPlan( + key, None, None, "unprofiled", unknown_components=(key.stage,), + reason="No exact calibrated profile covers this runtime/model/input stage", + ) + + def register_owner( + self, limit_bytes: int, *, kind: str, + usage_reader: Callable[[], object] | None = None, + ) -> BudgetOwner: + self._identity() + limit = _bytes(limit_bytes, "limit_bytes") + if type(kind) is not str or not kind: + raise ValueError("kind must be a nonempty string") + if usage_reader is not None and not callable(usage_reader): + raise ValueError("usage_reader must be callable") + with self._lock: + self._next_owner += 1 + token = self._next_owner + owner = BudgetOwner(self, token, self._usage_reader if usage_reader is None else usage_reader) + self._owners[token] = _OwnerRecord(limit, kind, weakref.ref(owner), set()) + self._owner_generation += 1 + self._enrolled = True + return owner + + def active_owner(self) -> BudgetOwner | None: + self._identity() + with self._lock: + while True: + generation = self._owner_generation + records = self._owners.copy() + selected = None + limit = 0 + for token, record in records.items(): + if record.closing or record.limit <= 0: + continue + handle = record.handle() + if (handle is None or handle.registry is not self + or handle.pid != self.pid or handle.epoch is not self.epoch + or self._owners.get(token) is not record): + continue + if not limit or record.limit < limit: + selected, limit = handle, record.limit + # Snapshot metadata or releasing temporary handles can run a + # finalizer. Selection must reflect the current owner generation. + if generation != self._owner_generation: + continue + self._identity() + return selected + + def _record(self, token: int) -> _OwnerRecord: + record = self._owners.get(token) + if record is None: + raise StaleBudgetOwner("Owner has retired") + return record + + def _ceiling(self) -> int: + ceiling = 0 + for record in self._owners.values(): + if record.limit > 0 and (ceiling == 0 or record.limit < ceiling): + ceiling = record.limit + return ceiling + + def _increment(self, owner: BudgetOwner, requested: int | None, lease_token: int | None) -> int: + owner._identity() + if owner.registry is not self or owner.epoch is not self.epoch: + raise StaleBudgetOwner("Allocation belongs to another registry epoch") + record = self._record(owner.token) + if record.handle() is not owner: + raise StaleBudgetOwner("Owner generation has retired") + if lease_token is None: + if record.closing: + raise StaleBudgetOwner("Owner is closing") + return 0 if requested is None else requested + lease = self._leases.get(lease_token) + if lease is None or lease.owner != owner.token or lease_token not in record.leases: + raise StaleBudgetOwner("Lease has retired") + return 0 if requested is None else max(0, requested - lease.reserved) + + def _admit( + self, owner: BudgetOwner, requested: int | None, + deadline: Callable[[], None] | None, lease_token: int | None = None, + ) -> int: + self._increment(owner, requested, lease_token) + if deadline is not None: + deadline() + ceiling = self._ceiling() + usage = None + if ceiling: + try: + usage = owner._usage_reader() + except (AttributeError, OSError, RuntimeError, ValueError, TypeError, OverflowError): + usage = None + if deadline is not None: + deadline() + if deadline is not None: + deadline() + # Readers/deadlines may collect an owner, retire a ticket, or lower a ceiling. + # No cached owner/lease/accounting state crosses those callbacks. + if not ceiling and self._ceiling(): + try: + usage = owner._usage_reader() + except (AttributeError, OSError, RuntimeError, ValueError, TypeError, OverflowError): + usage = None + if deadline is not None: + deadline() + ceiling = self._ceiling() + increment = self._increment(owner, requested, lease_token) + total = _sum_bytes(self._future, increment) + if ceiling: + if type(usage) is not int or not 0 < usage <= _MAX_BYTES: + raise MemoryAdmissionRefused("Current process RSS is unavailable") + if usage >= ceiling or _sum_bytes(usage, total) > ceiling: + raise MemoryAdmissionRefused("Process RSS plus future allocation budget exhausted") + return increment + + def _note_unknown(self, plan: AllocationPlan) -> None: + if plan.future_bytes is None or plan.unknown_components: + if len(self._unknown_stages) < 128: + self._unknown_stages.add(plan.key.stage) + elif plan.key.stage not in self._unknown_stages: + self._coverage_truncated = True + + def _reserve( + self, owner: BudgetOwner, plan: AllocationPlan, deadline: Callable[[], None] | None, + ) -> BudgetLease: + self._identity() + plan = _validate_plan(plan) + with self._lock: + # Unknown native demand remains explicit; only named known bytes enter the sum. + reserved = 0 if plan.future_bytes is None else plan.future_bytes + self._next_lease += 1 + token = self._next_lease + ticket = BudgetLease(owner, token, plan) + allocation = _LeaseRecord(owner.token, reserved) + self._admit(owner, reserved, deadline) + record = self._record(owner.token) + self._leases[token] = allocation + record.leases.add(token) + self._future = _sum_bytes(self._future, reserved) + self._high_water = max(self._high_water, self._future) + self._note_unknown(plan) + return ticket + + def _grow( + self, lease: BudgetLease, plan: AllocationPlan, deadline: Callable[[], None] | None, + ) -> None: + self._identity() + plan = _validate_plan(plan) + with self._lock: + # Hold the previous immutable plan through replacement, so retiring + # its metadata cannot run a finalizer halfway through accounting. + _previous = lease.plan + increment = self._admit(lease.owner, plan.future_bytes, deadline, lease.token) + record = self._leases[lease.token] + self._future = _sum_bytes(self._future, increment) + record.reserved += increment + self._high_water = max(self._high_water, self._future) + self._note_unknown(plan) + lease.plan = plan + + def _split(self, lease: BudgetLease, byte_count: int) -> BudgetLease: + self._identity() + amount = _bytes(byte_count, "retained bytes") + with self._lock: + self._next_lease += 1 + token = self._next_lease + original = _validate_plan(lease.plan) + if original.future_bytes is None: + plan = replace(original) + else: + plan = replace(original, future_bytes=amount, retained_bytes=amount) + ticket = BudgetLease(lease.owner, token, plan) + allocation = _LeaseRecord(lease.owner.token, amount) + # Dataclass/finalizer creation may reenter; transfer only a still-live + # ticket with unchanged provenance and enough currently owned bytes. + self._increment(lease.owner, None, lease.token) + record = self._leases[lease.token] + if lease.plan is not original or amount > record.reserved: + raise InvalidAllocationPlan("Retained portion or provenance changed during transfer") + record.reserved -= amount + self._leases[token] = allocation + self._record(record.owner).leases.add(token) + return ticket + + def _release(self, token: int, pid: int, epoch: object) -> None: + if pid != os.getpid() or epoch is not self.epoch: + return + with self._lock: + record = self._leases.pop(token, None) + if record is None: + return + self._future -= record.reserved + if self._future < 0: + raise RuntimeError("Negative future allocation accounting") + owner = self._record(record.owner) + owner.leases.discard(token) + if owner.closing and not owner.leases: + self._owners.pop(record.owner) + self._owner_generation += 1 + + def _close_owner(self, token: int, pid: int, epoch: object) -> None: + if pid != os.getpid() or epoch is not self.epoch: + return + with self._lock: + record = self._owners.get(token) + if record is None: + return + if not record.closing: + record.closing = True + self._owner_generation += 1 + if not record.leases: + self._owners.pop(token) + + def _set_limit(self, token: int, limit_bytes: int) -> None: + self._identity() + limit = _bytes(limit_bytes, "limit_bytes") + with self._lock: + record = self._record(token) + if record.closing: + raise StaleBudgetOwner("Closing owner cannot change its ceiling") + if record.limit != limit: + record.limit = limit + self._owner_generation += 1 + + def snapshot(self) -> dict[str, object]: + self._identity() + with self._lock: + return { + "pid": self.pid, "metric": "process_rss_sampled", + "effective_limit_bytes": self._ceiling(), + "owner_count": len(self._owners), + "closing_owner_count": sum(record.closing for record in self._owners.values()), + "lease_count": len(self._leases), "reserved_known_bytes": self._future, + "high_water_known_bytes": self._high_water, + "unprofiled_stages": tuple(sorted(self._unknown_stages)), + "coverage_truncated": self._coverage_truncated, + "physical_hard_cap": False, "os_enforcement": self.capability, + } + + +class BudgetOwner: + def __init__( + self, registry: ProcessRegistry, token: int, usage_reader: Callable[[], object], + ) -> None: + self.registry = registry + self.token = token + self.pid = registry.pid + self.epoch = registry.epoch + self._usage_reader = usage_reader + self._finalizer = weakref.finalize( + self, registry._close_owner, token, self.pid, self.epoch, + ) + + def _identity(self) -> None: + if self.pid != os.getpid() or self.epoch is not self.registry.epoch: + raise StaleBudgetOwner("Inherited owner cannot allocate in a new process") + + def reserve( + self, plan: AllocationPlan, *, deadline: Callable[[], None] | None = None, + ) -> BudgetLease: + self._identity() + return self.registry._reserve(self, plan, deadline) + + def set_limit(self, limit_bytes: int) -> None: + self._identity() + self.registry._set_limit(self.token, limit_bytes) + + def close(self) -> None: + self._finalizer() + + +class BudgetLease: + def __init__(self, owner: BudgetOwner, token: int, plan: AllocationPlan) -> None: + self.owner = owner + self.token = token + self.plan = plan + self._finalizer = weakref.finalize( + self, owner.registry._release, token, owner.pid, owner.epoch, + ) + + @property + def reserved_known_bytes(self) -> int: + self.owner._identity() + with self.owner.registry._lock: + record = self.owner.registry._leases.get(self.token) + return 0 if record is None else record.reserved + + def grow( + self, plan: AllocationPlan, *, deadline: Callable[[], None] | None = None, + ) -> None: + self.owner._identity() + self.owner.registry._grow(self, plan, deadline) + + def split_retained(self, byte_count: int) -> BudgetLease: + self.owner._identity() + return self.owner.registry._split(self, byte_count) + + def close(self) -> None: + self._finalizer() + + +_registry_lock = threading.RLock() +_registry_pid = os.getpid() +_registry: ProcessRegistry | None = None +_registry_initializing = False +_sessions = threading.local() + + +def _after_fork_child() -> None: + global _registry_lock, _registry_pid, _registry, _registry_initializing, _sessions + global _model_owners_lock, _model_owners, _retained_buffers_lock, _retained_buffers + _registry_lock = threading.RLock() + _registry_pid = os.getpid() + _registry = None + _registry_initializing = False + _sessions = threading.local() + _model_owners_lock = threading.RLock() + _model_owners = {} + _retained_buffers_lock = threading.RLock() + _retained_buffers = {} + + +if hasattr(os, "register_at_fork"): + os.register_at_fork(after_in_child=_after_fork_child) + + +def get_process_registry() -> ProcessRegistry: + global _registry_lock, _registry_pid, _registry, _registry_initializing + pid = os.getpid() + # Never acquire a lock inherited from a vanished parent thread. + if pid != _registry_pid: + _after_fork_child() + with _registry_lock: + if _registry is None: + if _registry_initializing: + raise MemoryAdmissionRefused("Process registry initialization is reentered") + _registry_initializing = True + try: + _registry = ProcessRegistry() + finally: + _registry_initializing = False + return _registry + + +def resolve_allocation_plan(key: ProfileKey) -> AllocationPlan: + registry = getattr(_sessions, "resolution_registry", None) + return (registry or get_process_registry()).resolve_profile(key) + + +def automatic_budget_bytes() -> int: + try: + import psutil + physical = psutil.virtual_memory().total + except (ImportError, AttributeError, OSError, RuntimeError, ValueError, TypeError) as exc: + raise MemoryAdmissionRefused("Local automatic physical-memory sensor unavailable") from exc + if type(physical) is not int or not 0 < physical <= _MAX_BYTES: + raise MemoryAdmissionRefused("Local physical-memory sensor invalid") + budget = min(8 * 1024**3, physical // 4) + if not budget: + raise MemoryAdmissionRefused("Local automatic budget is not positive") + return budget + + +def new_local_owner(kind: str) -> BudgetOwner: + return get_process_registry().register_owner(automatic_budget_bytes(), kind=kind) + + +def configured_local_owner(policy: object, *, kind: str) -> BudgetOwner: + validate_local_policy(policy) + if type(kind) is not str or not kind: + raise InvalidAllocationPlan("Local owner kind must be a nonempty exact string") + return new_local_owner(kind) + + +def _resolved_plan(key: ProfileKey, known_bytes: int, retained_bytes: int) -> AllocationPlan: + known = _bytes(known_bytes, "known bytes") + retained = _bytes(retained_bytes, "retained bytes") + _validate_key(key) + plan = _validate_plan(resolve_allocation_plan(key)) + if plan.key != key: + raise InvalidAllocationPlan("Resolver returned a mismatched allocation plan") + if plan.future_bytes is None: + if not known: + return plan + return AllocationPlan( + key, known, retained, unknown_components=plan.unknown_components, + reason=plan.reason, + ) + if plan.future_bytes < known: + raise InvalidAllocationPlan("Profile demand is below the enumerated named copies") + return replace(plan, retained_bytes=max(retained, plan.retained_bytes or 0)) + + +class AllocationSession: + def __init__(self, owner: BudgetOwner) -> None: + self.owner = owner + self._leases: dict[str, BudgetLease] = {} + self._closed = False + + def charge( + self, key: ProfileKey, *, known_bytes: int = 0, retained_bytes: int = 0, + deadline: Callable[[], None] | None = None, group: str | None = None, + ) -> BudgetLease: + if self._closed: + raise StaleBudgetOwner("Allocation session is closed") + key = self.owner.registry.enrich_key(key) + previous = getattr(_sessions, "resolution_registry", None) + _sessions.resolution_registry = self.owner.registry + try: + plan = _resolved_plan(key, known_bytes, retained_bytes) + finally: + _sessions.resolution_registry = previous + label = group or key.stage + lease = self._leases.get(label) + if lease is None: + lease = self.owner.reserve(plan, deadline=deadline) + self._leases[label] = lease + else: + lease.grow(plan, deadline=deadline) + return lease + + def retain_buffer(self, value: object, *, deadline: Callable[[], None] | None = None) -> None: + candidate = value + seen: set[int] = set() + while id(candidate) not in seen: + seen.add(id(candidate)) + base = getattr(candidate, "base", None) + if base is None: + break + try: + weakref.ref(base) + except TypeError: + break + candidate = base + amount = getattr(candidate, "nbytes", None) + if type(amount) is not int or amount < 0: + # A native buffer outside ndarray-owned bytes needs explicit profile coverage. + self.charge(ProfileKey("retained external buffer", "retained_result"), + deadline=deadline) + return + token = id(candidate) + get_process_registry() # Reset every inherited process-local lock before use. + with _retained_buffers_lock: + entry = _retained_buffers.get(token) + if entry is not None and entry[0]() is candidate: + if any(lease.owner is self.owner for lease in entry[1]): + return + existing = True + else: + existing = False + key = ProfileKey("retained result", "retained_result", native_tensor_shape=(amount,)) + lease = self._leases.get("result assembly") + if lease is None or lease.reserved_known_bytes < amount: + lease = self.charge(key, known_bytes=amount, retained_bytes=amount, + deadline=deadline, group="retained result") + portion = lease.split_retained(0 if existing else amount) + + def retired(reference: weakref.ReferenceType[object]) -> None: + with _retained_buffers_lock: + found = _retained_buffers.get(token) + leases = _retained_buffers.pop(token)[1] if found is not None and found[0] is reference else () + for ticket in leases: + ticket.close() + + try: + reference = weakref.ref(candidate, retired) + except TypeError: + portion.close() + raise InvalidAllocationPlan("Result owner cannot retain its byte lease") + spare = portion.split_retained(0) + prepared_entry = (reference, [portion]) + with _retained_buffers_lock: + found = _retained_buffers.get(token) + if found is not None and found[0]() is candidate: + found[1].append(spare) + unused = portion + else: + _retained_buffers[token] = prepared_entry + unused = spare + unused.close() + + def defer_to_exception(self, error: BaseException) -> None: + # The exception already owns its traceback; the ticket must follow that lifetime. + error.__dict__.setdefault("_truememory_allocation_sessions", []).append(self) + + def close(self) -> None: + if self._closed: + return + self._closed = True + leases, self._leases = self._leases, {} + for lease in leases.values(): + lease.close() + + +def current_session() -> AllocationSession | None: + stack = getattr(_sessions, "stack", ()) + return stack[-1] if stack else None + + +@contextmanager +def cooperative_scope() -> Iterator[AllocationSession | None]: + session = current_session() + if session is not None: + yield session + return + with allocation_scope(get_process_registry().active_owner()) as session: + yield session + + +@contextmanager +def allocation_scope(owner: BudgetOwner | None) -> Iterator[AllocationSession | None]: + if owner is None: + yield None + return + stack = getattr(_sessions, "stack", None) + if stack is None: + stack = _sessions.stack = [] + if stack and stack[-1].owner is owner: + yield stack[-1] + return + session = AllocationSession(owner) + stack.append(session) + failed = False + try: + yield session + except BaseException as error: + failed = True + session.defer_to_exception(error) + raise + finally: + stack.pop() + if not failed: + session.close() + + +_model_owners_lock = threading.RLock() +_model_owners: dict[int, tuple[weakref.ReferenceType[object], BudgetOwner]] = {} +_retained_buffers_lock = threading.RLock() +_retained_buffers: dict[int, tuple[weakref.ReferenceType[object], list[BudgetLease]]] = {} + + +def bind_model_owner(model: object, owner: BudgetOwner) -> None: + get_process_registry() + token = id(model) + + def retired(reference: weakref.ReferenceType[object]) -> None: + with _model_owners_lock: + found = _model_owners.get(token) + if found is not None and found[0] is reference: + _model_owners.pop(token) + owner.close() + + try: + reference = weakref.ref(model, retired) + except TypeError: + owner.close() + raise InvalidAllocationPlan("Local model cannot retain an automatic owner") + prepared_entry = (reference, owner) + with _model_owners_lock: + duplicate = token in _model_owners + if not duplicate: + _model_owners[token] = prepared_entry + if duplicate: + owner.close() + raise InvalidAllocationPlan("Local model already has an owner") + + +def model_owner(model: object) -> BudgetOwner | None: + get_process_registry() + with _model_owners_lock: + found = _model_owners.get(id(model)) + return found[1] if found is not None and found[0]() is model else None + + +@contextmanager +def model_operation_scope( + model: object, key: ProfileKey, deadline: Callable[[], object] | None = None, +) -> Iterator[AllocationSession | None]: + _validate_key(key) + if deadline is not None and not callable(deadline): + raise InvalidAllocationPlan("Operation deadline must be callable or None") + owner = model_owner(model) + scope = allocation_scope(owner) if owner is not None else cooperative_scope() + with scope as memory: + if memory is not None: + memory.charge(key, deadline=deadline) + yield memory + + +def _native_arguments(native_kwargs: dict[str, object] | None) -> dict[str, object]: + if native_kwargs is None: + return {} + if type(native_kwargs) is not dict or any(type(key) is not str for key in native_kwargs): + raise InvalidAllocationPlan("Native arguments must be an exact dictionary with exact string keys") + return native_kwargs + + +def budgeted_encode_with_model_ownership( + model: object, texts: object, key: ProfileKey, *, + native_kwargs: dict[str, object] | None = None, + deadline: Callable[[], object] | None = None, +) -> object: + arguments = _native_arguments(native_kwargs) + with model_operation_scope(model, key, deadline) as memory: + from truememory.mps_utils import encode_with_model_ownership + values = encode_with_model_ownership(model, texts, **arguments) + if memory is not None: + memory.retain_buffer(values, deadline=deadline) + return values + + +def budgeted_encode_with_mps_fallback( + model: object, texts: object, key: ProfileKey, *, + native_kwargs: dict[str, object] | None = None, + deadline: Callable[[], object] | None = None, +) -> object: + arguments = _native_arguments(native_kwargs) + with model_operation_scope(model, key, deadline) as memory: + from truememory.mps_utils import encode_with_mps_fallback + values = encode_with_mps_fallback(model, texts, **arguments) + if memory is not None: + memory.retain_buffer(values, deadline=deadline) + return values diff --git a/truememory/model_client.py b/truememory/model_client.py index 77cb5063..540448e8 100644 --- a/truememory/model_client.py +++ b/truememory/model_client.py @@ -12,6 +12,7 @@ import base64 import json +import itertools import logging import os import platform @@ -84,6 +85,7 @@ class ModelServerBusyError(RuntimeError): # Code hooks) set a short deadline via set_request_timeout() so a contended # server fast-fails and the caller's FTS-only fallback can trigger. _default_request_timeout: float | None = None +_decoded_array_groups = itertools.count() def set_request_timeout(timeout: float | None) -> None: @@ -101,8 +103,20 @@ def set_request_timeout(timeout: float | None) -> None: def _json_object_hook(obj): """Decode base64-encoded numpy arrays from JSON.""" if "__ndarray__" in obj: + from truememory.memory_budget import ProfileKey, current_session + memory = current_session() + encoded = obj["__ndarray__"] + if memory is not None and type(encoded) is str: + decoded_bound = 3 * ((len(encoded) + 3) // 4) + memory.charge(ProfileKey("client ndarray decode", "model_client", + native_tensor_shape=(len(encoded),)), + known_bytes=len(encoded) + decoded_bound, + group=f"client decoded array:{next(_decoded_array_groups)}") data = base64.b64decode(obj["__ndarray__"]) - return np.frombuffer(data, dtype=np.dtype(obj["dtype"])).reshape(obj["shape"]) + values = np.frombuffer(data, dtype=np.dtype(obj["dtype"])).reshape(obj["shape"]) + if memory is not None: + memory.retain_buffer(values) + return values return obj _APP_BUNDLE_PATH = _TRUEMEMORY_DIR / "TrueMemory.app" @@ -237,8 +251,11 @@ def _server_ready() -> bool: def _probe_server(deadline: float) -> bool: """Validate transport/auth/protocol without renewing the caller's budget.""" + policy = _required_capped_policy() if "TRUEMEMORY_REQUIRE_PROCESS_MEMORY_CAP" in os.environ else None if not _server_ready(): return False + if policy is not None: + _verify_retained_capped_server(policy, deadline) try: response = _send_request({"op": "ping"}, timeout=min(0.2, _remaining(deadline))) except ModelServerBusyError: @@ -253,6 +270,12 @@ def _probe_server(deadline: float) -> bool: def _write_start_claim(claim: dict) -> None: + if "TRUEMEMORY_REQUIRE_PROCESS_MEMORY_CAP" in os.environ: + from truememory_memory_bootstrap import _validate_claim, _write_console_claim + _required_capped_policy() + _validate_claim(claim, pending=True) + _write_console_claim(START_STATE_PATH, claim) + return temporary = START_STATE_PATH.with_name(START_STATE_PATH.name + "." + secrets.token_hex(8) + ".tmp") fd = os.open(str(temporary), os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600) try: @@ -272,6 +295,18 @@ def _launch_is_pending(claim: dict | None) -> bool: def _spawn_server(generation: str, deadline: float) -> subprocess.Popen: _remaining(deadline) + if "TRUEMEMORY_REQUIRE_PROCESS_MEMORY_CAP" in os.environ: + from truememory_memory_bootstrap import ( + EnforcementUnavailable, _physical_total, launch_owned_linux, + plan_required_linux_launch, required_cap_selected, + ) + required_cap_selected(os.environ) + if sys.platform != "linux": + raise EnforcementUnavailable("Required pre-exec memory cap is unsupported on this platform") + total = None if "TRUEMEMORY_MODEL_SERVER_MAX_RSS_MB" in os.environ else _physical_total() + policy = plan_required_linux_launch(os.environ, physical_total=total) + return launch_owned_linux(policy, generation=generation, + python_executable=os.path.abspath(sys.executable), deadline=deadline) app_exe = _ensure_app_bundle(deadline) env = os.environ.copy() env[_MODEL_SERVER_GENERATION_ENV] = generation @@ -299,12 +334,33 @@ def _start_server(wait_timeout: float | None = None) -> bool: separate lifetime bind lock. A timed-out caller leaves its live child claimed, so concurrent or later clients wait instead of spawning copies. """ + required_started = time.monotonic() if "TRUEMEMORY_REQUIRE_PROCESS_MEMORY_CAP" in os.environ else None + required_cap = False + required_policy = None + if "TRUEMEMORY_REQUIRE_PROCESS_MEMORY_CAP" in os.environ: + from truememory_memory_bootstrap import ( + EnforcementUnavailable, _physical_total, plan_required_linux_launch, + required_cap_selected, + ) + required_cap = required_cap_selected(os.environ) + if sys.platform != "linux": + raise EnforcementUnavailable("Required pre-exec memory cap is unsupported on this platform") + total = None if "TRUEMEMORY_MODEL_SERVER_MAX_RSS_MB" in os.environ else _physical_total() + required_policy = plan_required_linux_launch(os.environ, physical_total=total) + if not use_model_server(): + raise EnforcementUnavailable("Required capped daemon cannot use local-model mode") if not use_model_server(): return False wait = _SERVER_START_TIMEOUT if wait_timeout is None else min(_SERVER_START_TIMEOUT, max(wait_timeout, 0.0)) if wait <= 0: return False - deadline = time.monotonic() + wait + deadline = (required_started if required_started is not None else time.monotonic()) + wait + if required_cap: + from truememory_memory_bootstrap import _required_linux_support + _required_linux_support(required_policy, deadline) + if _probe_server(deadline): + _verify_retained_capped_server(required_policy, deadline) + return True launched = False try: _TRUEMEMORY_DIR.mkdir(parents=True, exist_ok=True) @@ -314,12 +370,16 @@ def _start_server(wait_timeout: float | None = None) -> bool: pass while time.monotonic() < deadline: if _probe_server(deadline): + if required_cap: + _verify_retained_capped_server(required_policy, deadline) return True _remaining(deadline) gate = try_file_lock(START_LOCK_PATH) if gate is not None: try: if _probe_server(deadline): + if required_cap: + _verify_retained_capped_server(required_policy, deadline) return True _remaining(deadline) bind = try_file_lock(LOCK_PATH) @@ -348,12 +408,20 @@ def _start_server(wait_timeout: float | None = None) -> bool: child_born = process_birth(child.pid) if child_born is None: raise OSError("Model server exited before startup") + if required_cap and (type(child_born) is not float or child_born != child.record.identity.birth_time): + raise OSError("Actual model-server process birth differs from capped launch identity") + if required_cap and child.record.identity.cap_bytes != required_policy.cap_bytes: + raise OSError("Capped launch ceiling changed during startup") claim["child"] = {"pid": child.pid, "born": child_born} _write_start_claim(claim) + if required_cap: + child.release_after_claim(claim, deadline=deadline) + _retain_owned_capped_launch(child) except (OSError, TimeoutError): # Invalidate this generation; a late child must # not publish after its launch failed. - START_STATE_PATH.unlink(missing_ok=True) + if not required_cap: + START_STATE_PATH.unlink(missing_ok=True) raise finally: os.close(gate) @@ -368,6 +436,59 @@ def _start_server(wait_timeout: float | None = None) -> bool: return False +_owned_capped_launches: list[object] = [] + + +def _required_capped_policy() -> object | None: + if "TRUEMEMORY_REQUIRE_PROCESS_MEMORY_CAP" not in os.environ: + return None + from truememory_memory_bootstrap import ( + EnforcementUnavailable, _physical_total, plan_required_linux_launch, required_cap_selected, + ) + required_cap_selected(os.environ) + if sys.platform != "linux": + raise EnforcementUnavailable("Required pre-exec memory cap is unsupported on this platform") + total = None if "TRUEMEMORY_MODEL_SERVER_MAX_RSS_MB" in os.environ else _physical_total() + policy = plan_required_linux_launch(os.environ, physical_total=total) + if not use_model_server(): + raise EnforcementUnavailable("Required capped daemon cannot use local-model mode") + return policy + + +def _verify_retained_capped_server(policy: object, deadline: float) -> None: + """Current readonly proof; joining never grants launch or stop authority.""" + from truememory_memory_bootstrap import ( + EnforcementUnavailable, _validate_claim, inspect_existing_owned_linux, + ) + claim = read_start_claim(START_STATE_PATH) + _validate_claim(claim) + child = claim["child"] + observed_birth = process_birth(child["pid"]) + if type(observed_birth) is not float or observed_birth != child["born"]: + raise EnforcementUnavailable("Required daemon current birth differs from its claim") + inspect_existing_owned_linux(policy, pid=child["pid"], birth_time=observed_birth, + generation=claim["generation"], claim=claim, deadline=deadline) + _remaining(deadline) + + +def _retain_owned_capped_launch(handle: object) -> None: + """Retain native resources; an uncertain lifetime never triggers a live stop.""" + import select + import threading + from truememory_memory_bootstrap import stop_owned_exact + _owned_capped_launches.append(handle) + + def observe_exit() -> None: + try: + while not select.select([handle.record.pidfd], [], [], 1.0)[0]: + pass + stop_owned_exact(handle, deadline=time.monotonic() + 30.0) + except (OSError, RuntimeError, ValueError, TypeError, TimeoutError) as error: + log.warning("Capped daemon terminal evidence unresolved (%s)", type(error).__name__) + + threading.Thread(target=observe_exit, name="truememory-capped-owner", daemon=True).start() + + def _remaining(deadline: float | None) -> float | None: """Seconds left until *deadline* (a time.monotonic() value), or None. @@ -436,33 +557,43 @@ def _send_request(request: dict, timeout: float | None = None) -> dict: lock and a full encode. """ deadline = None if timeout is None else time.monotonic() + timeout - sock = _connect(deadline) - try: - payload = request - if timeout is not None and "deadline" not in payload: - # Server checks this wall-clock epoch deadline before encode (M-44). - payload = {**request, "deadline": time.time() + max(_remaining(deadline), 0.0)} - data = json.dumps(payload).encode("utf-8") - header = struct.pack(_HEADER_FMT, len(data)) - sock.settimeout(_remaining(deadline) if deadline is not None else _REQUEST_TIMEOUT) + from truememory.memory_budget import ProfileKey, cooperative_scope + with cooperative_scope() as memory: + sock = _connect(deadline) try: - sock.sendall(header + data) - except OSError: - # A bounded server may reject the header before our full send ends. - # Preserve that explicit error without mistaking overload for death. - recovery_deadline = time.monotonic() + 0.25 - if deadline is not None: - recovery_deadline = min(recovery_deadline, deadline) + payload = request + if timeout is not None and "deadline" not in payload: + # Server checks this wall-clock epoch deadline before encode (M-44). + payload = {**request, "deadline": time.time() + max(_remaining(deadline), 0.0)} + if memory is not None: + memory.charge(ProfileKey("client request measure", "model_client"), + deadline=lambda: _remaining(deadline)) + wire_bytes = sum(len(chunk.encode("utf-8")) for chunk in json.JSONEncoder().iterencode(payload)) + memory.charge(ProfileKey("client request serialize", "model_client", + native_tensor_shape=(wire_bytes,)), + known_bytes=3 * wire_bytes + _HEADER_SIZE, + deadline=lambda: _remaining(deadline)) + data = json.dumps(payload).encode("utf-8") + header = struct.pack(_HEADER_FMT, len(data)) + sock.settimeout(_remaining(deadline) if deadline is not None else _REQUEST_TIMEOUT) try: - _receive_response(sock, recovery_deadline) - except ModelServerBusyError: + sock.sendall(header + data) + except OSError: + # A bounded server may reject the header before our full send ends. + # Preserve that explicit error without mistaking overload for death. + recovery_deadline = time.monotonic() + 0.25 + if deadline is not None: + recovery_deadline = min(recovery_deadline, deadline) + try: + _receive_response(sock, recovery_deadline) + except ModelServerBusyError: + raise + except (OSError, ValueError): + pass raise - except (OSError, ValueError): - pass - raise - return _receive_response(sock, deadline) - finally: - sock.close() + return _receive_response(sock, deadline) + finally: + sock.close() def _receive_response(sock: socket.socket, deadline: float | None) -> dict: @@ -488,6 +619,13 @@ def _decode_response(resp_data: bytes) -> dict: well-formed JSON response carrying an incompatible ``protocol`` version is rejected the same way. """ + from truememory.memory_budget import ProfileKey, current_session + memory = current_session() + if memory is not None: + import hashlib + memory.charge(ProfileKey("client response materialize", "model_client", + native_tensor_shape=(len(resp_data),), + wire_signature=hashlib.sha256(resp_data).hexdigest())) try: resp = json.loads(resp_data, object_hook=_json_object_hook) except (UnicodeDecodeError, json.JSONDecodeError, ValueError) as e: @@ -509,14 +647,21 @@ def _decode_response(resp_data: bytes) -> dict: def _recv_exact(sock: socket.socket, n: int, deadline: float | None = None) -> bytes | None: - buf = bytearray() - while len(buf) < n: + from truememory.memory_budget import ProfileKey, current_session + memory = current_session() + if memory is not None: + memory.charge(ProfileKey("client frame receive", "model_client", native_tensor_shape=(n,)), + known_bytes=2 * n + min(n, 65536), deadline=lambda: _remaining(deadline)) + buf = bytearray(n) + offset = 0 + while offset < n: if deadline is not None: sock.settimeout(_remaining(deadline)) - chunk = sock.recv(n - len(buf)) + chunk = sock.recv(min(n - offset, 65536)) if not chunk: return None - buf.extend(chunk) + buf[offset:offset + len(chunk)] = chunk + offset += len(chunk) return bytes(buf) @@ -536,12 +681,29 @@ def _request_with_autostart(request: dict, timeout: float | None = None) -> dict instead of silently absorbing the autostart + full-timeout retry cycle — deadline-bound callers (hook recall) have their own FTS-only fallback. """ + required_started = time.monotonic() if "TRUEMEMORY_REQUIRE_PROCESS_MEMORY_CAP" in os.environ else None + required_policy = _required_capped_policy() if "TRUEMEMORY_REQUIRE_PROCESS_MEMORY_CAP" in os.environ else None if timeout is None: timeout = _default_request_timeout has_deadline = timeout is not None - started = time.monotonic() + started = required_started if required_started is not None else time.monotonic() + if required_policy is not None and timeout is not None and (type(timeout) not in (int, float) or timeout <= 0): + raise ValueError("Required request timeout must be a positive finite number") + required_deadline = started + (_SERVER_START_TIMEOUT if timeout is None else timeout) if required_policy is not None else None + if required_deadline is not None: + from truememory_memory_bootstrap import _deadline + _deadline(required_deadline) + required_existing = required_policy is not None and _server_ready() + if required_existing: + _verify_retained_capped_server(required_policy, required_deadline) try: + if required_policy is not None and not required_existing: + raise FileNotFoundError("Required daemon endpoint is absent") + if required_policy is not None and has_deadline: + response = _send_request(request, timeout=_remaining(required_deadline)) + _remaining(required_deadline) + return response return _send_request(request, timeout=timeout) except ProtocolMismatchError: # A stale/foreign server is bound (M-53). Restarting won't help until @@ -561,6 +723,8 @@ def _request_with_autostart(request: dict, timeout: float | None = None) -> dict remaining = timeout - (time.monotonic() - started) if remaining <= 0: raise _deadline_error(timeout) + elif required_deadline is not None: + remaining = _remaining(required_deadline) if not _start_server(wait_timeout=remaining): if has_deadline and time.monotonic() - started >= timeout: @@ -575,10 +739,15 @@ def _request_with_autostart(request: dict, timeout: float | None = None) -> dict if remaining <= 0: raise _deadline_error(timeout) try: + if required_policy is not None: + _verify_retained_capped_server(required_policy, required_deadline) + remaining = _remaining(required_deadline) return _send_request(request, timeout=remaining) except TimeoutError: raise _deadline_error(timeout) from None + if required_policy is not None: + _verify_retained_capped_server(required_policy, required_deadline) return _send_request(request) diff --git a/truememory/model_server.py b/truememory/model_server.py index 37ceb224..cb56a353 100644 --- a/truememory/model_server.py +++ b/truememory/model_server.py @@ -29,6 +29,7 @@ def _set_mps_memory_cap(): import base64 # noqa: E402 import gc # noqa: E402 import hmac # noqa: E402 +import itertools # noqa: E402 import json # noqa: E402 import logging # noqa: E402 import math # noqa: E402 @@ -46,7 +47,7 @@ def _set_mps_memory_cap(): from contextlib import contextmanager # noqa: E402 from dataclasses import dataclass # noqa: E402 from pathlib import Path # noqa: E402 -from typing import TYPE_CHECKING # noqa: E402 +from typing import TYPE_CHECKING, Callable # noqa: E402 import numpy as np # noqa: E402 @@ -172,20 +173,37 @@ def _batch_limit(value: object, maximum: int) -> int: return min(value, maximum) -def _checked_batch(values: object, count: int, total: int, field: str) -> np.ndarray: +def _checked_batch( + values: object, count: int, total: int, field: str, + *, admit: Callable[[tuple, int], None] | None = None, + before_materialize: Callable[[], None] | None = None, +) -> np.ndarray: + if admit is not None and isinstance(values, np.ndarray) and values.ndim: + cast_bytes = 0 if values.dtype == np.dtype(np.float32) else math.prod(values.shape) * 4 + admit((total, *values.shape[1:]), cast_bytes) + if not isinstance(values, np.ndarray) and before_materialize is not None: + before_materialize() batch = values if isinstance(values, np.ndarray) else np.asarray(values, dtype=np.float32) if batch.ndim == 0 or batch.shape[0] != count: raise ValueError("Model output count does not match the input microbatch") _check_result_size((total, *batch.shape[1:]), field) + if admit is not None: + admit((total, *batch.shape[1:]), 0) return np.asarray(batch, dtype=np.float32) def _store_batch_result( result: np.ndarray | None, values: object, offset: int, count: int, total: int, field: str = "vectors", *, indices: np.ndarray | None = None, + admit: Callable[[tuple, int], None] | None = None, + before_materialize: Callable[[], None] | None = None, ) -> np.ndarray: """Check the complete output before allocating it; keep every float32 row.""" - batch = _checked_batch(values, count, total, field) + if admit is None and before_materialize is None: + batch = _checked_batch(values, count, total, field) + else: + batch = _checked_batch(values, count, total, field, admit=admit, + before_materialize=before_materialize) if result is None: result = np.empty((total, *batch.shape[1:]), dtype=np.float32) elif result.shape[1:] != batch.shape[1:]: @@ -229,6 +247,7 @@ def _json_default(obj): _ALLOWED_DTYPES = frozenset({"float32", "float64", "float16", "int32", "int64"}) +_decoded_array_groups = itertools.count() def _json_object_hook(obj): @@ -237,6 +256,14 @@ def _json_object_hook(obj): dtype_str = obj["dtype"] if dtype_str not in _ALLOWED_DTYPES: raise ValueError(f"Disallowed dtype: {dtype_str}") + from truememory.memory_budget import ProfileKey, current_session + memory = current_session() + encoded = obj["__ndarray__"] + if memory is not None and type(encoded) is str: + memory.charge(ProfileKey("request ndarray decode", "model_server", + native_tensor_shape=(len(encoded),)), + known_bytes=len(encoded) + 3 * ((len(encoded) + 3) // 4), + group=f"request decoded array:{next(_decoded_array_groups)}") data = base64.b64decode(obj["__ndarray__"]) return np.frombuffer(data, dtype=np.dtype(dtype_str)).reshape(obj["shape"]) return obj @@ -381,9 +408,21 @@ def __init__(self, *, automatic_rss_budget: bool = False) -> None: self._busy_rejections = 0 self._stopped = threading.Event() self._transport_context = threading.local() + self._memory_owner = None + if automatic_rss_budget: + from truememory.memory_budget import get_process_registry + self._memory_owner = get_process_registry().register_owner( + self._max_rss_bytes, kind="model_server", + usage_reader=lambda: psutil.Process().memory_info().rss if psutil is not None else None, + ) # Submission requires a slot+bytes lease; at most max_handlers futures # can be running or waiting in this executor. - self._workers = ThreadPoolExecutor(max_workers=self._max_handlers, thread_name_prefix="model-client") + try: + self._workers = ThreadPoolExecutor(max_workers=self._max_handlers, thread_name_prefix="model-client") + except BaseException: + if self._memory_owner is not None: + self._memory_owner.close() + raise # Loaded embedding model + identity as ONE immutable snapshot, # assigned only under self._lock; the fast lane reads the single # reference lock-free (issue #577, panel round 2). @@ -435,6 +474,8 @@ def _check_process_memory( deadline.check() budget = self._max_rss_bytes if budget == 0: + if getattr(self, "_memory_owner", None) is not None: + self._memory_stage(stage, deadline) return try: usage = psutil.Process().memory_info().rss if psutil is not None else None @@ -452,6 +493,80 @@ def _check_process_memory( raise _ProcessMemoryRefused( f"Model server busy: process RSS budget exhausted before {stage}" ) + if getattr(self, "_memory_owner", None) is not None and not stage.endswith(" completion"): + self._memory_stage(stage, deadline) + + def _memory_scope(self): + from contextlib import nullcontext + owner = getattr(self, "_memory_owner", None) + if owner is None: + return nullcontext(None) + from truememory.memory_budget import InvalidAllocationPlan, StaleBudgetOwner, allocation_scope + try: + owner.set_limit(self._max_rss_bytes) + except (InvalidAllocationPlan, StaleBudgetOwner, ValueError) as error: + raise _ProcessMemoryRefused(f"Model server busy: {error}") from error + return allocation_scope(owner) + + def _memory_stage( + self, stage: str, deadline: _RequestDeadline | None = None, *, + known_bytes: int = 0, shape: tuple[int, ...] | None = None, + wire_signature: str | None = None, model_identity: tuple[str, int, str] | None = None, + device: str | None = None, input_count: int | None = None, + ) -> None: + owner = getattr(self, "_memory_owner", None) + if owner is None: + return + from truememory.memory_budget import ( + InvalidAllocationPlan, MemoryAdmissionRefused, ProfileKey, StaleBudgetOwner, current_session, + ) + if current_session() is None: + # A standalone sampled guard performs no allocation itself. + return + deadline = deadline or getattr(self._transport_context, "memory_deadline", None) + try: + owner.set_limit(self._max_rss_bytes) + key = ProfileKey(stage, "model_server", native_tensor_shape=shape, + wire_signature=wire_signature, model_identity=model_identity, + device=device, input_item_count=input_count) + with self._memory_scope() as memory: + memory.charge(key, known_bytes=known_bytes, + deadline=deadline.check if deadline is not None else None) + except (MemoryAdmissionRefused, InvalidAllocationPlan, StaleBudgetOwner, ValueError) as error: + raise _ProcessMemoryRefused(f"Model server busy: {error}") from error + + def _reserve_result_assembly(self, shape: tuple, cast_bytes: int = 0) -> None: + self._memory_stage("result assembly", known_bytes=math.prod(shape) * 4, shape=shape) + if cast_bytes: + self._memory_stage("result cast", known_bytes=cast_bytes, shape=(cast_bytes,)) + + def _result_admitter(self) -> Callable[[tuple, int], None] | None: + return self._reserve_result_assembly if getattr(self, "_memory_owner", None) is not None else None + + def _before_result_materialization(self) -> None: + self._memory_stage("result materialize") + + def _result_materializer(self) -> Callable[[], None] | None: + return self._before_result_materialization if getattr(self, "_memory_owner", None) is not None else None + + def _result_admission_options(self, *, materialize: bool = True) -> dict[str, object]: + if getattr(self, "_memory_owner", None) is None: + return {} + options = {"admit": self._result_admitter()} + if materialize: + options["before_materialize"] = self._result_materializer() + return options + + def _retain_memory_response(self, memory: object, response: dict) -> None: + if memory is None: + return + from truememory.memory_budget import InvalidAllocationPlan, MemoryAdmissionRefused, StaleBudgetOwner + try: + for value in response.values(): + if isinstance(value, np.ndarray): + memory.retain_buffer(value) + except (MemoryAdmissionRefused, InvalidAllocationPlan, StaleBudgetOwner, ValueError) as error: + raise _ProcessMemoryRefused(f"Model server busy: {error}") from error def _mark_sticky_cpu(self, kind: str) -> bool: """Permanently degrade *kind* ("embed"/"rerank") to CPU after an @@ -506,32 +621,41 @@ def _recover_embed_oom_locked( inference ownership throughout recovery and retry. Batch retries release the state lock so bookkeeping and the fast lane stay responsive. """ - from truememory.mps_utils import flush_mps_cache - self._mark_sticky_cpu("embed") - flush_mps_cache() - if deadline is not None: - self._check_embed_recovery_deadline_locked(model, deadline) - try: - self._check_process_memory("embed CPU transfer", deadline) - except (_ProcessMemoryRefused, _RequestDeadlineExceeded): - # A refused transfer leaves the failed accelerator instance behind. - # Invalidate it so a cache hit cannot bypass sticky CPU selection. - state = self._embed_state - if state is not None and state.model is model: - self._publish_embed_state(None) - raise - if deadline is not None: - self._check_embed_recovery_deadline_locked(model, deadline) - if hasattr(model, "to"): + @contextmanager + def memory_scope() -> Iterator[object | None]: + if getattr(self, "_memory_owner", None) is None: + yield None + else: + with self._memory_scope() as memory: + yield memory + + with memory_scope() as _memory: + from truememory.mps_utils import flush_mps_cache + self._mark_sticky_cpu("embed") + flush_mps_cache() + if deadline is not None: + self._check_embed_recovery_deadline_locked(model, deadline) try: - model.to("cpu") - except BaseException: - # Failed or cancelled transfers cannot certify this instance - # as CPU-ready. Preserve the original error and sticky choice. + self._check_process_memory("embed CPU transfer", deadline) + except (_ProcessMemoryRefused, _RequestDeadlineExceeded): + # A refused transfer leaves the failed accelerator instance behind. + # Invalidate it so a cache hit cannot bypass sticky CPU selection. state = self._embed_state if state is not None and state.model is model: self._publish_embed_state(None) raise + if deadline is not None: + self._check_embed_recovery_deadline_locked(model, deadline) + if hasattr(model, "to"): + try: + model.to("cpu") + except BaseException: + # Failed or cancelled transfers cannot certify this instance + # as CPU-ready. Preserve the original error and sticky choice. + state = self._embed_state + if state is not None and state.model is model: + self._publish_embed_state(None) + raise def _check_embed_recovery_deadline_locked( self, model: object, deadline: _RequestDeadline, @@ -669,6 +793,8 @@ def _preflight_embed_result(self, tier: str, count: int) -> None: # Legacy minilm/bge-small fall through to model2vec here, not # the 384-dimensional models used by the local loader. _check_result_size((count, 256), "vectors") + if getattr(self, "_memory_owner", None) is not None: + self._reserve_result_assembly((count, 256)) # A custom truncate_dim is only an upper bound. Even ST's dimension # getter can return that bound when native width is unknown; check # the actual first slice before allocating the complete result. @@ -748,7 +874,9 @@ def _rerank_slice_indices( return indices[inverse] @staticmethod - def _preflight_rerank_result(model_name: str | None, count: int) -> None: + def _preflight_rerank_result( + model_name: str | None, count: int, *, admit: Callable[[tuple, int], None] | None = None, + ) -> None: from truememory.reranker import get_current_reranker_name name = model_name or get_current_reranker_name() if count and name in ( @@ -756,6 +884,8 @@ def _preflight_rerank_result(model_name: str | None, count: int) -> None: "Alibaba-NLP/gte-reranker-modernbert-base", ): _check_result_size((count,), "scores") + if admit is not None: + admit((count,), 0) # Custom CrossEncoder constructors preserve num_labels=None; their # output may contain several labels per pair. Inspect its real shape. @@ -795,54 +925,62 @@ def _publish_embed_state(self, state: _EmbedState | None) -> None: self._retire_stale_fast_encoder() def _get_embed_model(self, tier: str, deadline: _RequestDeadline | None = None): - model_id, state = self._resolve_embed_cache(tier) - if state is not None: - if state.tier != tier: - self._publish_embed_state(_EmbedState( - model=state.model, tier=tier, model_id=model_id, generation=state.generation, - prepared_identity=state.prepared_identity, - )) - return state.model - - # Memory-first replacement: failed construction leaves the cache empty. - self._publish_embed_state(None) - self._check_process_memory("embed construction", deadline) - device = self._embed_device() - if deadline is not None: - deadline.check() - model = self._build_embed_model(model_id, device) - self._check_process_memory("embed construction completion", deadline) - # ONE atomic reference assignment of an immutable snapshot — the - # fast lane can never observe a torn (model, tier, model_id) triple. - self._publish_embed_state(_EmbedState(model=model, tier=tier, model_id=model_id)) - log.info("Loaded embedding model for tier=%s (model=%s)", tier, model_id) - return model + with self._memory_scope() as _memory: + model_id, state = self._resolve_embed_cache(tier) + if state is not None: + if state.tier != tier: + self._publish_embed_state(_EmbedState( + model=state.model, tier=tier, model_id=model_id, generation=state.generation, + prepared_identity=state.prepared_identity, + )) + return state.model + + # Memory-first replacement: failed construction leaves the cache empty. + self._publish_embed_state(None) + self._check_process_memory("embed construction", deadline) + device = self._embed_device() + if deadline is not None: + deadline.check() + self._memory_stage("embed construction", deadline, + model_identity=((model_id, 256, "legacy") + if model_id in self._FAST_LANE_SAFE_MODEL_IDS else None), + device=device) + model = self._build_embed_model(model_id, device) + self._check_process_memory("embed construction completion", deadline) + # ONE atomic reference assignment of an immutable snapshot — the + # fast lane can never observe a torn (model, tier, model_id) triple. + self._publish_embed_state(_EmbedState(model=model, tier=tier, model_id=model_id)) + log.info("Loaded embedding model for tier=%s (model=%s)", tier, model_id) + return model def _get_prepared_embed_model(self, target: "EmbeddingTarget", deadline: _RequestDeadline) -> object: - from truememory.embedding_target import build_target_model + with self._memory_scope() as _memory: + from truememory.embedding_target import build_target_model - target.check_configuration() - deadline.check() - state = self._embed_state - if state is not None and ( - state.prepared_identity == target.identity - or (state.model_id == target.model_id and target.model_id in self._FAST_LANE_SAFE_MODEL_IDS) - ): - return state.model - # A legacy custom label is not evidence that its constructor did not - # fall back. Only strict construction can establish that identity. - state = None - self._publish_embed_state(None) - self._check_process_memory("prepared embed construction", deadline) - device = self._embed_device() - deadline.check() - model = build_target_model(target, device, check=deadline.check) - self._check_process_memory("prepared embed construction completion", deadline) - self._publish_embed_state(_EmbedState( - model=model, tier=target.tier, model_id=target.model_id, - prepared_identity=target.identity, - )) - return model + target.check_configuration() + deadline.check() + state = self._embed_state + if state is not None and ( + state.prepared_identity == target.identity + or (state.model_id == target.model_id and target.model_id in self._FAST_LANE_SAFE_MODEL_IDS) + ): + return state.model + # A legacy custom label is not evidence that its constructor did not + # fall back. Only strict construction can establish that identity. + state = None + self._publish_embed_state(None) + self._check_process_memory("prepared embed construction", deadline) + device = self._embed_device() + deadline.check() + self._memory_stage("prepared embed construction", deadline, + model_identity=target.identity, device=device) + model = build_target_model(target, device, check=deadline.check) + self._check_process_memory("prepared embed construction completion", deadline) + self._publish_embed_state(_EmbedState( + model=model, tier=target.tier, model_id=target.model_id, + prepared_identity=target.identity, + )) + return model def _get_fast_encoder( self, tier: str, deadline: _RequestDeadline | None = None, *, expected_target: "EmbeddingTarget | None" = None, @@ -861,81 +999,90 @@ def _get_fast_encoder( locked path) when there is no snapshot for this tier yet, or the loaded model has no explicit deterministic builder branch. """ - state = self._embed_state # single atomic reference read - if state is None or state.tier != tier: - # No main model loaded for this tier yet — declining means the - # one-time load happens exactly once, on the main path, - # without changing the daemon's default selection. - return None + with self._memory_scope() as _memory: + state = self._embed_state # single atomic reference read + if state is None or state.tier != tier: + # No main model loaded for this tier yet — declining means the + # one-time load happens exactly once, on the main path, + # without changing the daemon's default selection. + return None - if state.model_id not in self._FAST_LANE_SAFE_MODEL_IDS: - return None + if state.model_id not in self._FAST_LANE_SAFE_MODEL_IDS: + return None - model_id, generation = state.model_id, state.generation - if expected_target is not None and model_id != expected_target.model_id: - from truememory.embedding_target import EmbeddingTargetError - raise EmbeddingTargetError("Fast encoder residency does not match the serving target") - # A slow CPU constructor must not retain the old main model snapshot. - state = None - if self._fast_encoder is not None and self._fast_generation is generation: - if expected_target is not None and self._fast_model_id != model_id: + model_id, generation = state.model_id, state.generation + if expected_target is not None and model_id != expected_target.model_id: from truememory.embedding_target import EmbeddingTargetError - raise EmbeddingTargetError("Fast encoder cache does not match the serving target") - return self._fast_encoder - - self._fast_encoder = None - self._fast_model_id = None - self._fast_generation = None - self._check_process_memory("fast embed construction", deadline) - model = self._build_embed_model(model_id, "cpu") - self._check_process_memory("fast embed construction completion", deadline) - # A changed generation, including A -> B -> A, must not publish this - # construction. Its already-captured request may still finish on it. - if self._residency_lock.acquire(blocking=False): - try: - state = self._embed_state - if state is not None and state.generation is generation: - self._fast_encoder = model - self._fast_model_id = model_id - self._fast_generation = generation - finally: - self._residency_lock.release() - state = None - log.info( - "Fast-lane CPU encoder loaded (tier=%s, model=%s) — single-text " - "requests no longer queue behind batch work", tier, model_id, - ) - return model + raise EmbeddingTargetError("Fast encoder residency does not match the serving target") + # A slow CPU constructor must not retain the old main model snapshot. + state = None + if self._fast_encoder is not None and self._fast_generation is generation: + if expected_target is not None and self._fast_model_id != model_id: + from truememory.embedding_target import EmbeddingTargetError + raise EmbeddingTargetError("Fast encoder cache does not match the serving target") + return self._fast_encoder + + self._fast_encoder = None + self._fast_model_id = None + self._fast_generation = None + self._check_process_memory("fast embed construction", deadline) + self._memory_stage("fast embed construction", deadline, + model_identity=(model_id, 256, "legacy"), device="cpu") + model = self._build_embed_model(model_id, "cpu") + self._check_process_memory("fast embed construction completion", deadline) + # A changed generation, including A -> B -> A, must not publish this + # construction. Its already-captured request may still finish on it. + if self._residency_lock.acquire(blocking=False): + try: + state = self._embed_state + if state is not None and state.generation is generation: + self._fast_encoder = model + self._fast_model_id = model_id + self._fast_generation = generation + finally: + self._residency_lock.release() + state = None + log.info( + "Fast-lane CPU encoder loaded (tier=%s, model=%s) — single-text " + "requests no longer queue behind batch work", tier, model_id, + ) + return model def _get_reranker( self, model_name: str | None = None, deadline: _RequestDeadline | None = None, ): - from truememory.reranker import get_current_reranker_name - name = model_name or get_current_reranker_name() + from contextlib import nullcontext + scope = self._memory_scope() if getattr(self, "_memory_owner", None) is not None else nullcontext(None) + with scope as _memory: + from truememory.reranker import get_current_reranker_name + name = model_name or get_current_reranker_name() + + if self._reranker is not None and self._reranker_name == name: + return self._reranker + + self._reranker = None + self._reranker_name = None + self._check_process_memory("reranker construction", deadline) + from truememory.mps_utils import auto_detect_device, ensure_mps_memory_budget, resolve_device + if "rerank" in self._sticky_cpu: + device = "cpu" + else: + device = resolve_device(auto_detect_device()) - if self._reranker is not None and self._reranker_name == name: + ensure_mps_memory_budget(device) + from sentence_transformers import CrossEncoder + if deadline is not None: + deadline.check() + if getattr(self, "_memory_owner", None) is not None: + self._memory_stage("reranker construction", deadline, + model_identity=(name, 0, "reranker"), device=device) + model = CrossEncoder(name, device=device) + self._check_process_memory("reranker construction completion", deadline) + self._reranker = model + self._reranker_name = name + log.info("Loaded reranker model=%s device=%s", name, device) return self._reranker - self._reranker = None - self._reranker_name = None - self._check_process_memory("reranker construction", deadline) - from truememory.mps_utils import auto_detect_device, ensure_mps_memory_budget, resolve_device - if "rerank" in self._sticky_cpu: - device = "cpu" - else: - device = resolve_device(auto_detect_device()) - - ensure_mps_memory_budget(device) - from sentence_transformers import CrossEncoder - if deadline is not None: - deadline.check() - model = CrossEncoder(name, device=device) - self._check_process_memory("reranker construction completion", deadline) - self._reranker = model - self._reranker_name = name - log.info("Loaded reranker model=%s device=%s", name, device) - return self._reranker - def _check_serving_embed_model(self, model: object, target: "EmbeddingTarget | None") -> None: if target is None: return @@ -984,6 +1131,8 @@ def _handle_fast_embed( (locked) path. """ deadline = deadline or _RequestDeadline() + result_options = (self._result_admission_options() + if getattr(self, "_memory_owner", None) is not None else {}) deadline.check() if self._inference_lock.acquire(blocking=False): try: @@ -1015,7 +1164,8 @@ def _handle_fast_embed( deadline.check() vectors = model.encode(texts, batch_size=1, show_progress_bar=False) return self._serving_embed_response( - _checked_batch(vectors, len(texts), len(texts), "vectors"), len(texts), expected_target, + _checked_batch(vectors, len(texts), len(texts), "vectors", **result_options), + len(texts), expected_target, ) finally: model = None @@ -1045,7 +1195,8 @@ def _handle_fast_embed( vectors = model.encode(texts, batch_size=1, show_progress_bar=False) if expected_target is not None: response = self._serving_embed_response( - _checked_batch(vectors, len(texts), len(texts), "vectors"), len(texts), expected_target, + _checked_batch(vectors, len(texts), len(texts), "vectors", **result_options), + len(texts), expected_target, ) finally: model = None @@ -1053,7 +1204,8 @@ def _handle_fast_embed( if fast_owned: self._retire_stale_fast_encoder() return (response if expected_target is not None else - {"ok": True, "vectors": _checked_batch(vectors, len(texts), len(texts), "vectors")}) + {"ok": True, "vectors": _checked_batch(vectors, len(texts), len(texts), "vectors", + **result_options)}) except (_RequestDeadlineExceeded, _ResultTooLarge, _ProcessMemoryRefused): raise except Exception as exc: @@ -1081,7 +1233,12 @@ def handle_request(self, request: dict) -> dict: self._last_activity = time.time() self._inflight += 1 try: - return self._handle_request_inner(request) + if getattr(self, "_memory_owner", None) is None: + return self._handle_request_inner(request) + with self._memory_scope() as memory: + response = self._handle_request_inner(request) + self._retain_memory_response(memory, response) + return response except _RequestDeadlineExceeded as exc: return {"ok": False, "error": str(exc)} except _ResultTooLarge as exc: @@ -1089,17 +1246,22 @@ def handle_request(self, request: dict) -> dict: except _ProcessMemoryRefused as exc: return {"ok": False, "error": str(exc), "error_code": "server_busy", "retry_after_ms": 250} finally: + self._transport_context.memory_deadline = None with self._activity_lock: self._inflight -= 1 self._last_activity = time.time() def _handle_request_inner(self, request: dict) -> dict: op = request.get("op") + enrolled = getattr(self, "_memory_owner", None) is not None + result_options = self._result_admission_options() if enrolled else {} + preflight_options = self._result_admission_options(materialize=False) if enrolled else {} # Convert the wire epoch once. Queueing, loading and recovery consume # one monotonic budget; wall-clock adjustments cannot renew it. deadline = _RequestDeadline.from_wall_clock(request.get("deadline")) deadline = _RequestDeadline(deadline.expires_at, getattr(self._transport_context, "request", None)) + self._transport_context.memory_deadline = deadline deadline.check() if op == "ping": @@ -1179,6 +1341,8 @@ def _handle_request_inner(self, request: dict) -> dict: self._preflight_embed_result(tier, len(texts)) else: _check_result_size((len(texts), target.dimension), "vectors") + if enrolled: + self._reserve_result_assembly((len(texts), target.dimension)) deadline.check() model = (self._get_embed_model(tier, deadline=deadline) if target is None else self._get_prepared_embed_model(target, deadline)) @@ -1210,6 +1374,7 @@ def _handle_request_inner(self, request: dict) -> dict: check_target_vectors(target or expected_target, values, len(batch)) vectors = _store_batch_result( vectors, values, offset, len(batch), len(texts), indices=indices, + **result_options, ) offset += len(batch) if retry is not None: @@ -1223,6 +1388,7 @@ def _handle_request_inner(self, request: dict) -> dict: check_target_vectors(target or expected_target, values, len(retry)) vectors = _store_batch_result( vectors, values, offset, len(retry), len(texts), indices=indices, + **result_options, ) offset += len(retry) self._after_request_batches(throttler, len(texts), encode_start, deadline) @@ -1267,7 +1433,7 @@ def _handle_request_inner(self, request: dict) -> dict: with deadline.locked(self._lock): if reranker is None: deadline.start_inference() - self._preflight_rerank_result(model_name, len(pairs)) + self._preflight_rerank_result(model_name, len(pairs), **preflight_options) deadline.check() reranker = self._get_reranker(model_name, deadline=deadline) self._check_serving_reranker(reranker, expected_name) @@ -1307,10 +1473,12 @@ def _handle_request_inner(self, request: dict) -> dict: retry = batch break if indices is None: - scores = _store_batch_result(scores, values, offset, len(batch), len(pairs), "scores") + scores = _store_batch_result(scores, values, offset, len(batch), len(pairs), + "scores", **result_options) else: scores = _store_batch_result( scores, values, offset, len(batch), len(pairs), "scores", indices=indices, + **result_options, ) offset += len(batch) if retry is not None: @@ -1318,10 +1486,12 @@ def _handle_request_inner(self, request: dict) -> dict: deadline.check() values = reranker.predict(retry, batch_size=limit, show_progress_bar=False) if indices is None: - scores = _store_batch_result(scores, values, offset, len(retry), len(pairs), "scores") + scores = _store_batch_result(scores, values, offset, len(retry), len(pairs), + "scores", **result_options) else: scores = _store_batch_result( scores, values, offset, len(retry), len(pairs), "scores", indices=indices, + **result_options, ) offset += len(retry) self._after_request_batches(throttler, len(pairs), predict_start, deadline) @@ -1503,7 +1673,7 @@ def _reject_client(self, conn: socket.socket, length: int, code: str, message: s if not chunk: break length -= len(chunk) - except OSError: + except (OSError, _ProcessMemoryRefused): pass def _dispatch_client(self, conn: socket.socket) -> Future | None: @@ -1529,7 +1699,8 @@ def _run_admitted_client(self, conn: socket.socket, admitted: _TransportRequest) return admitted.claimed = True try: - self._serve_client(conn, admitted) + with self._memory_scope() as _memory: + self._serve_client(conn, admitted) finally: # The serving frame and exception traceback have returned. No # decoded request, raw body, or response survives credit release. @@ -1552,6 +1723,9 @@ def _serve_client(self, conn: socket.socket, admitted: _TransportRequest) -> Non data = self._recv_exact(conn, admitted.length, admitted.frame_expires_at) if data is None: return + import hashlib + self._memory_stage("request materialize", known_bytes=0, shape=(len(data),), + wire_signature=hashlib.sha256(data).hexdigest()) request = json.loads(data, object_hook=_json_object_hook) data = None if not isinstance(request, dict): @@ -1572,6 +1746,9 @@ def _serve_client(self, conn: socket.socket, admitted: _TransportRequest) -> Non identity_error = identity_error or isinstance(error, EmbeddingTargetError) if identity_error: failure["error_code"] = "serving_identity_mismatch" + elif isinstance(error, _ProcessMemoryRefused): + failure["error_code"] = "server_busy" + failure["retry_after_ms"] = 250 self._send_response(conn, failure) except Exception: # The connection is already failing. Keep serialization errors @@ -1579,6 +1756,7 @@ def _serve_client(self, conn: socket.socket, admitted: _TransportRequest) -> Non pass finally: self._transport_context.request = None + self._transport_context.memory_deadline = None data = request = response = None def _stop_clients(self) -> None: @@ -1606,39 +1784,56 @@ def _stop_clients(self) -> None: conn.close() def _recv_exact(self, conn: socket.socket, n: int, expires_at: float | None = None) -> bytes | None: - buf = bytearray() - while len(buf) < n: - if self._stopped.is_set(): - return None - if expires_at is not None: - remaining = expires_at - time.monotonic() - if remaining <= 0: - raise TimeoutError("request frame receive timed out: deadline exceeded") - # Cross-thread shutdown does not reliably interrupt recv on - # every platform. Poll shutdown without extending the frame budget. - conn.settimeout(min(remaining, 0.1)) - try: - chunk = conn.recv(min(n - len(buf), 65536)) - except TimeoutError: - if expires_at is None: - raise - continue - if not chunk: - return None - buf.extend(chunk) - return bytes(buf) + with self._memory_scope() as _memory: + self._memory_stage("frame receive", _RequestDeadline(expires_at), + known_bytes=2 * n + min(n, 65536), shape=(n,)) + buf = bytearray(n) + offset = 0 + while offset < n: + if self._stopped.is_set(): + return None + if expires_at is not None: + remaining = expires_at - time.monotonic() + if remaining <= 0: + raise TimeoutError("request frame receive timed out: deadline exceeded") + # Cross-thread shutdown does not reliably interrupt recv on + # every platform. Poll shutdown without extending the frame budget. + conn.settimeout(min(remaining, 0.1)) + try: + chunk = conn.recv(min(n - offset, 65536)) + except TimeoutError: + if expires_at is None: + raise + continue + if not chunk: + return None + buf[offset:offset + len(chunk)] = chunk + offset += len(chunk) + return bytes(buf) def _send_response(self, conn: socket.socket, response: dict): # Issue #646 (M-53): tag every response with the protocol version so # a newer client can detect a version mismatch instead of choking on # an unexpected payload shape. - if "protocol" not in response: - response = {**response, "protocol": PROTOCOL_VERSION} - if _response_wire_size(response) > _MAX_MESSAGE_SIZE: - response = {"ok": False, "error": "Response too large", "protocol": PROTOCOL_VERSION} - data = json.dumps(response, default=_json_default).encode("utf-8") - header = struct.pack(_HEADER_FMT, len(data)) - conn.sendall(header + data) + with self._memory_scope() as _memory: + if "protocol" not in response: + response = {**response, "protocol": PROTOCOL_VERSION} + self._memory_stage("response metadata") + wire_bytes = _response_wire_size(response) + if wire_bytes > _MAX_MESSAGE_SIZE: + response = {"ok": False, "error": "Response too large", "protocol": PROTOCOL_VERSION} + wire_bytes = _response_wire_size(response) + if getattr(self, "_memory_owner", None) is not None: + payload_bytes = sum(math.prod(value.shape) * 4 for value in response.values() + if isinstance(value, np.ndarray)) + encoded_bytes = sum(_array_base64_size(value.shape) for value in response.values() + if isinstance(value, np.ndarray)) + # Original ndarray storage has a separate retained-result ticket. + copies = 2 * payload_bytes + 2 * encoded_bytes + 3 * wire_bytes + _HEADER_SIZE + self._memory_stage("response serialize", known_bytes=copies, shape=(wire_bytes,)) + data = json.dumps(response, default=_json_default).encode("utf-8") + header = struct.pack(_HEADER_FMT, len(data)) + conn.sendall(header + data) def _idle_checker(self): while self._running: @@ -1817,6 +2012,9 @@ def run(self): PID_PATH.write_text(str(os.getpid())) srv.listen(16) srv.settimeout(2.0) + if "TRUEMEMORY_REQUIRE_PROCESS_MEMORY_CAP" in os.environ: + from truememory_memory_bootstrap import record_server_ready + record_server_ready() idle_thread = threading.Thread(target=self._idle_checker, daemon=True) idle_thread.start() @@ -1869,6 +2067,8 @@ def _cleanup(self): self._fast_generation = None self._token = None gc.collect() + if getattr(self, "_memory_owner", None) is not None: + self._memory_owner.close() # Only remove artifacts THIS process owns (issue #646, M-20). After a # crash a fresh server may already hold the lock and have rewritten # PID_PATH; unlinking its live socket/token here would let concurrent @@ -1896,6 +2096,9 @@ def _handle_signal(signum, frame): def main(): + if "TRUEMEMORY_REQUIRE_PROCESS_MEMORY_CAP" in os.environ: + from truememory_memory_bootstrap import require_child_scope + require_child_scope() logging.basicConfig( level=logging.INFO, format="%(asctime)s [model_server] %(levelname)s %(message)s", diff --git a/truememory/reranker.py b/truememory/reranker.py index 35a6d04b..cc2a6977 100644 --- a/truememory/reranker.py +++ b/truememory/reranker.py @@ -31,6 +31,10 @@ from contextlib import contextmanager from typing import TYPE_CHECKING +from truememory.memory_budget import LocalMemoryPolicy + +_LOCAL_MEMORY_POLICY = LocalMemoryPolicy() + log = logging.getLogger(__name__) if TYPE_CHECKING: @@ -268,6 +272,10 @@ def get_reranker( A ``sentence_transformers.CrossEncoder`` instance or RerankerProxy. """ global _model, _model_name, _model_certified + configured_memory = "_LOCAL_MEMORY_POLICY" in globals() + if configured_memory: + from truememory.memory_budget import validate_local_policy + validate_local_policy(globals()["_LOCAL_MEMORY_POLICY"]) from truememory.tier_switch.serving import _control, _check control = _control(None, deadline, cancelled) _check(control) @@ -314,11 +322,29 @@ def get_reranker( from truememory.mps_utils import auto_detect_device, resolve_device device = resolve_device(auto_detect_device()) - from truememory.mps_utils import ensure_mps_memory_budget - ensure_mps_memory_budget(device) - from sentence_transformers import CrossEncoder - _check(control) - _model = CrossEncoder(name, device=device) + budget_owner = None + if configured_memory: + from truememory.memory_budget import ( + ProfileKey, allocation_scope, bind_model_owner, configured_local_owner, + ) + budget_owner = configured_local_owner(globals()["_LOCAL_MEMORY_POLICY"], kind="local_reranker") + try: + with allocation_scope(budget_owner) if configured_memory else nullcontext() as memory: + if memory is not None: + memory.charge(ProfileKey("local reranker construction", "local_reranker", + model_identity=(name, 0, "reranker"), device=device), + deadline=lambda: _check(control)) + from truememory.mps_utils import ensure_mps_memory_budget + ensure_mps_memory_budget(device) + from sentence_transformers import CrossEncoder + _check(control) + _model = CrossEncoder(name, device=device) + if budget_owner is not None: + bind_model_owner(_model, budget_owner) + except BaseException: + if budget_owner is not None: + budget_owner.close() + raise _model_name = name _model_certified = True result = _model @@ -391,6 +417,10 @@ def rerank( Top *top_k* results sorted by cross-encoder score, each with an added ``"rerank_score"`` key. """ + configured_memory = "_LOCAL_MEMORY_POLICY" in globals() + if configured_memory: + from truememory.memory_budget import validate_local_policy + validate_local_policy(globals()["_LOCAL_MEMORY_POLICY"]) if not results: return [] @@ -404,7 +434,22 @@ def rerank( # Score all pairs (locked for thread safety with parallel queries) with _inference_lock: - scores = model.predict(pairs, batch_size=batch_size, show_progress_bar=False) + from contextlib import nullcontext + from truememory.mps_utils import _own_model + if configured_memory: + from truememory.memory_budget import ProfileKey, model_operation_scope + scope = model_operation_scope( + model, ProfileKey("local reranker forward", "local_reranker", + input_item_count=len(pairs), + batch_size=batch_size if type(batch_size) is int and batch_size >= 0 else None), + ) + else: + scope = nullcontext() + with scope as memory: + with _own_model(model): + scores = model.predict(pairs, batch_size=batch_size, show_progress_bar=False) + if memory is not None: + memory.retain_buffer(scores) # Attach scores and sort scored = [] diff --git a/truememory/tier_switch/worker.py b/truememory/tier_switch/worker.py index b219346c..8c697468 100644 --- a/truememory/tier_switch/worker.py +++ b/truememory/tier_switch/worker.py @@ -14,6 +14,7 @@ from contextlib import contextmanager from typing import TYPE_CHECKING, Callable, NamedTuple +from truememory.memory_budget import DEFAULT_LOCAL_MEMORY_POLICY from truememory.tier_switch.cache import VectorCacheRegistry from truememory.tier_switch.throttler import DynamicThrottler @@ -22,6 +23,8 @@ from truememory.tier_switch.source import TierSourcePlan from truememory.vector_search import _PreparedEmbeddingLease +_LOCAL_MEMORY_POLICY = DEFAULT_LOCAL_MEMORY_POLICY + log = logging.getLogger(__name__) _HARD_TIMEOUT = 9000 # 2.5 hours @@ -197,6 +200,10 @@ def result(status: str) -> SourceBuildResult: except _SourceStopped: raise except Exception as error: + if "_LOCAL_MEMORY_POLICY" in globals(): + from truememory.memory_budget import InvalidAllocationPlan, MemoryAdmissionRefused, StaleBudgetOwner + if isinstance(error, (MemoryAdmissionRefused, StaleBudgetOwner, InvalidAllocationPlan)): + raise # No DB queries, status updates or retries after uncertain # rollback. The caller must discard this connection. if (self.conn.in_transaction or (publishing and @@ -403,6 +410,10 @@ def run( ), ) except Exception as exc: + if "_LOCAL_MEMORY_POLICY" in globals(): + from truememory.memory_budget import InvalidAllocationPlan, MemoryAdmissionRefused, StaleBudgetOwner + if isinstance(exc, (MemoryAdmissionRefused, StaleBudgetOwner, InvalidAllocationPlan)): + raise # run() owns a clean connection. An active transaction here # means rollback did not restore that boundary. if self.conn.in_transaction: @@ -486,10 +497,23 @@ def _process_batch( """Prepare both embeddings, then publish their pair and checkpoint atomically.""" from truememory.mps_utils import encode_with_model_ownership + configured_memory = "_LOCAL_MEMORY_POLICY" in globals() + if configured_memory: + from truememory.memory_budget import ( + ProfileKey, budgeted_encode_with_model_ownership, validate_local_policy, + ) + validate_local_policy(globals()["_LOCAL_MEMORY_POLICY"]) texts = [m["content"] for m in batch] ids = [m["id"] for m in batch] - embeddings = encode_with_model_ownership(model, texts, show_progress_bar=False) + if configured_memory: + embeddings = budgeted_encode_with_model_ownership( + model, texts, ProfileKey("rebuild completion embedding", "local_embedding", + input_item_count=len(texts)), + native_kwargs={"show_progress_bar": False}, + ) + else: + embeddings = encode_with_model_ownership(model, texts, show_progress_bar=False) if len(embeddings) != len(ids): raise ValueError("Completion embedding count does not match the rebuild batch") completion_rows = [(mid, serialize_f32(emb)) for mid, emb in zip(ids, embeddings)] @@ -504,7 +528,15 @@ def _process_batch( ) for m in batch ] - sep_embeddings = encode_with_model_ownership(model, sep_texts, show_progress_bar=False) + if configured_memory: + validate_local_policy(globals()["_LOCAL_MEMORY_POLICY"]) + sep_embeddings = budgeted_encode_with_model_ownership( + model, sep_texts, ProfileKey("rebuild separation embedding", "local_embedding", + input_item_count=len(sep_texts)), + native_kwargs={"show_progress_bar": False}, + ) + else: + sep_embeddings = encode_with_model_ownership(model, sep_texts, show_progress_bar=False) if len(sep_embeddings) != len(ids): raise ValueError("Separation embedding count does not match the rebuild batch") separation_rows = [(mid, serialize_f32(emb)) for mid, emb in zip(ids, sep_embeddings)] diff --git a/truememory/vector_search.py b/truememory/vector_search.py index ccd35925..6a776b66 100644 --- a/truememory/vector_search.py +++ b/truememory/vector_search.py @@ -54,8 +54,11 @@ import numpy as np from truememory._platform import _env_int +from truememory.memory_budget import LocalMemoryPolicy from truememory.storage import _deserialize_metadata, select_message_cols +_LOCAL_MEMORY_POLICY = LocalMemoryPolicy() + if TYPE_CHECKING: from truememory.embedding_target import EmbeddingTarget from truememory.tier_switch.writer import WriterCapture @@ -267,6 +270,10 @@ def get_model(): Local loading requires explicit TRUEMEMORY_NO_MODEL_SERVER=1. """ global _model, _embedding_dim + configured_memory = "_LOCAL_MEMORY_POLICY" in globals() + if configured_memory: + from truememory.memory_budget import validate_local_policy + validate_local_policy(globals()["_LOCAL_MEMORY_POLICY"]) if _model is not None: return _model # Fast path, no lock needed from truememory.tier_switch.runtime import require_model_load_allowed @@ -291,71 +298,87 @@ def get_model(): from truememory.mps_utils import ensure_mps_memory_budget, resolve_device - resolved = EMBEDDING_MODEL - if resolved == "model2vec": - from model2vec import StaticModel - _model = StaticModel.from_pretrained("minishlab/potion-base-8M", force_download=False) - _embedding_dim = 256 - elif resolved == "minilm": - device = resolve_device(None) - ensure_mps_memory_budget(device) - from sentence_transformers import SentenceTransformer - _model = SentenceTransformer("all-MiniLM-L6-v2", device=device) - _embedding_dim = 384 - elif resolved == "bge-small": - device = resolve_device(None) - ensure_mps_memory_budget(device) - from sentence_transformers import SentenceTransformer - _model = SentenceTransformer("BAAI/bge-small-en-v1.5", device=device) - _embedding_dim = 384 - elif resolved == "qwen3_256": - device = resolve_device(None) - ensure_mps_memory_budget(device) - from sentence_transformers import SentenceTransformer - import sys as _sys - _mkwargs = {} - if _sys.platform == "darwin": - _mkwargs["attn_implementation"] = "eager" - _model = SentenceTransformer( - "Qwen/Qwen3-Embedding-0.6B", - truncate_dim=256, - model_kwargs=_mkwargs or None, - device=device, + budget_owner = None + if configured_memory: + from truememory.memory_budget import ( + ProfileKey, allocation_scope, bind_model_owner, configured_local_owner, ) - _embedding_dim = 256 - elif resolved not in _MODEL_DIMS: - # Custom tier: load arbitrary SentenceTransformer model. - # Requires TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD=1 (enforced by - # tier_config.resolve_custom_tier at config time). - if os.environ.get("TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD", "").strip() != "1": - logger.warning( - "Custom model %r requested without " - "TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD=1 -- " - "falling back to model2vec.", - resolved, - ) - from model2vec import StaticModel - _model = StaticModel.from_pretrained( - "minishlab/potion-base-8M", force_download=False - ) - _embedding_dim = 256 - else: - from truememory.tier_config import resolve_custom_tier - cfg = resolve_custom_tier() - custom_dim = cfg["embed_dim"] - device = resolve_device(None) - ensure_mps_memory_budget(device) - from sentence_transformers import SentenceTransformer - _model = SentenceTransformer( - resolved, truncate_dim=custom_dim, - trust_remote_code=False, - device=device, - ) - _embedding_dim = custom_dim - else: - from model2vec import StaticModel - _model = StaticModel.from_pretrained("minishlab/potion-base-8M", force_download=False) - _embedding_dim = 256 + budget_owner = configured_local_owner(globals()["_LOCAL_MEMORY_POLICY"], kind="local_embedding") + try: + with allocation_scope(budget_owner) if configured_memory else nullcontext() as memory: + if memory is not None: + memory.charge(ProfileKey("local embedding construction", "local_embedding")) + resolved = EMBEDDING_MODEL + if resolved == "model2vec": + from model2vec import StaticModel + _model = StaticModel.from_pretrained("minishlab/potion-base-8M", force_download=False) + _embedding_dim = 256 + elif resolved == "minilm": + device = resolve_device(None) + ensure_mps_memory_budget(device) + from sentence_transformers import SentenceTransformer + _model = SentenceTransformer("all-MiniLM-L6-v2", device=device) + _embedding_dim = 384 + elif resolved == "bge-small": + device = resolve_device(None) + ensure_mps_memory_budget(device) + from sentence_transformers import SentenceTransformer + _model = SentenceTransformer("BAAI/bge-small-en-v1.5", device=device) + _embedding_dim = 384 + elif resolved == "qwen3_256": + device = resolve_device(None) + ensure_mps_memory_budget(device) + from sentence_transformers import SentenceTransformer + import sys as _sys + _mkwargs = {} + if _sys.platform == "darwin": + _mkwargs["attn_implementation"] = "eager" + _model = SentenceTransformer( + "Qwen/Qwen3-Embedding-0.6B", + truncate_dim=256, + model_kwargs=_mkwargs or None, + device=device, + ) + _embedding_dim = 256 + elif resolved not in _MODEL_DIMS: + # Custom tier: load arbitrary SentenceTransformer model. + # Requires TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD=1 (enforced by + # tier_config.resolve_custom_tier at config time). + if os.environ.get("TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD", "").strip() != "1": + logger.warning( + "Custom model %r requested without " + "TRUEMEMORY_CUSTOM_ALLOW_DOWNLOAD=1 -- " + "falling back to model2vec.", + resolved, + ) + from model2vec import StaticModel + _model = StaticModel.from_pretrained( + "minishlab/potion-base-8M", force_download=False + ) + _embedding_dim = 256 + else: + from truememory.tier_config import resolve_custom_tier + cfg = resolve_custom_tier() + custom_dim = cfg["embed_dim"] + device = resolve_device(None) + ensure_mps_memory_budget(device) + from sentence_transformers import SentenceTransformer + _model = SentenceTransformer( + resolved, truncate_dim=custom_dim, + trust_remote_code=False, + device=device, + ) + _embedding_dim = custom_dim + else: + from model2vec import StaticModel + _model = StaticModel.from_pretrained("minishlab/potion-base-8M", force_download=False) + _embedding_dim = 256 + if budget_owner is not None: + bind_model_owner(_model, budget_owner) + except BaseException: + if budget_owner is not None: + budget_owner.close() + raise return _model @@ -363,10 +386,15 @@ def get_model(): def _load_frozen_embedding_target(target: EmbeddingTarget, *, timeout: float | None = None) -> object: + from contextlib import nullcontext from truememory.embedding_target import TARGET_PROBE_TEXT, build_target_model, check_target_vectors from truememory.model_client import EmbeddingProxy, PreparedEmbeddingProxy, use_model_server from truememory.mps_utils import _own_model, resolve_device + configured_memory = "_LOCAL_MEMORY_POLICY" in globals() + if configured_memory: + from truememory.memory_budget import validate_local_policy + validate_local_policy(globals()["_LOCAL_MEMORY_POLICY"]) expires = _target_deadline(timeout) target.check_configuration() if use_model_server(): @@ -392,14 +420,50 @@ def encode(self, texts: str | list[str], timeout: float | None = None, **kwargs: return proxy if not _prepared_target_slot.acquire(blocking=False): raise RuntimeError("Another prepared embedding target lease is active") + budget_owner = None try: - encoder = build_target_model(target, resolve_device(None), check=lambda: _target_remaining(expires)) - with _own_model(encoder, remaining=lambda: _target_remaining(expires)): - _target_remaining(expires) - values = encoder.encode([TARGET_PROBE_TEXT], batch_size=1, show_progress_bar=False) - _target_remaining(expires) - check_target_vectors(target, np.asarray(values, dtype=np.float32), 1) + if configured_memory: + from truememory.memory_budget import ( + ProfileKey, allocation_scope, bind_model_owner, configured_local_owner, + model_operation_scope, + ) + budget_owner = configured_local_owner(globals()["_LOCAL_MEMORY_POLICY"], kind="local_prepared_embedding") + with allocation_scope(budget_owner) if configured_memory else nullcontext() as memory: + device = resolve_device(None) + if memory is not None: + memory.charge(ProfileKey("local prepared construction", "local_prepared_embedding", + model_identity=target.identity, device=device), + deadline=lambda: _target_remaining(expires)) + encoder = build_target_model(target, device, check=lambda: _target_remaining(expires)) + if budget_owner is not None: + bind_model_owner(encoder, budget_owner) + if configured_memory: + scope = model_operation_scope( + encoder, ProfileKey("local prepared probe", "local_prepared_embedding", + model_identity=target.identity, input_item_count=1, batch_size=1), + deadline=lambda: _target_remaining(expires), + ) + else: + scope = nullcontext() + with scope as memory: + with _own_model(encoder, remaining=lambda: _target_remaining(expires)): + _target_remaining(expires) + if memory is not None: + memory.charge(ProfileKey("local probe result", "local_prepared_embedding", + native_tensor_shape=(1, target.dimension)), + known_bytes=target.dimension * 4, retained_bytes=target.dimension * 4, + deadline=lambda: _target_remaining(expires), group="result assembly") + values = encoder.encode([TARGET_PROBE_TEXT], batch_size=1, show_progress_bar=False) + _target_remaining(expires) + values = np.asarray(values, dtype=np.float32) + check_target_vectors(target, values, 1) + if memory is not None: + memory.retain_buffer(values, deadline=lambda: _target_remaining(expires)) return encoder + except BaseException: + if budget_owner is not None: + budget_owner.close() + raise finally: _prepared_target_slot.release() @@ -495,9 +559,14 @@ def target(self) -> EmbeddingTarget: def encode( self, texts: str | list[str], *, timeout: float | None = None, batch_size: int = 32, ) -> np.ndarray: + from contextlib import nullcontext from truememory.embedding_target import TARGET_PROBE_TEXT, check_target_vectors from truememory.mps_utils import _own_model + configured_memory = "_LOCAL_MEMORY_POLICY" in globals() + if configured_memory: + from truememory.memory_budget import validate_local_policy + validate_local_policy(globals()["_LOCAL_MEMORY_POLICY"]) expires = _target_deadline(timeout) if type(batch_size) is not int or batch_size <= 0: raise ValueError("batch_size must be a positive integer") @@ -523,18 +592,38 @@ def encode( return encoder.encode(texts, timeout=remaining, batch_size=batch_size) # The local tier worker owns its retry policy. Preserve exact-model # exclusion without adding an independent native recovery loop. - with _own_model(encoder, remaining=lambda: _target_remaining(expires)): - self.target.check_configuration() - _target_remaining(expires) - probe = not texts - values = encoder.encode( - [TARGET_PROBE_TEXT] if probe else texts, - batch_size=batch_size, show_progress_bar=False, + probe = not texts + count = 1 if probe else len(texts) + if configured_memory: + from truememory.memory_budget import ProfileKey, model_operation_scope + scope = model_operation_scope( + encoder, ProfileKey("local prepared forward", "local_prepared_embedding", + model_identity=self.target.identity, input_item_count=count, + batch_size=batch_size), + deadline=lambda: _target_remaining(expires), ) - _target_remaining(expires) - values = np.asarray(values, dtype=np.float32) - check_target_vectors(self.target, values, 1 if probe else len(texts)) - return values[:0] if probe else values + else: + scope = nullcontext() + with scope as memory: + with _own_model(encoder, remaining=lambda: _target_remaining(expires)): + self.target.check_configuration() + _target_remaining(expires) + if memory is not None: + payload_bytes = count * self.target.dimension * 4 + memory.charge(ProfileKey("local prepared result", "local_prepared_embedding", + native_tensor_shape=(count, self.target.dimension)), + known_bytes=payload_bytes, retained_bytes=payload_bytes, + deadline=lambda: _target_remaining(expires), group="result assembly") + values = encoder.encode( + [TARGET_PROBE_TEXT] if probe else texts, + batch_size=batch_size, show_progress_bar=False, + ) + _target_remaining(expires) + values = np.asarray(values, dtype=np.float32) + check_target_vectors(self.target, values, count) + if memory is not None: + memory.retain_buffer(values, deadline=lambda: _target_remaining(expires)) + return values[:0] if probe else values finally: encoder = None with self._state_lock: @@ -595,10 +684,15 @@ def prepare_embedding_target( target: EmbeddingTarget, *, timeout: float | None = None, ) -> Iterator[_PreparedEmbeddingLease]: """Prepare outside SQLite transactions; never activate a tier or expose weights.""" + from contextlib import nullcontext from truememory.embedding_target import EmbeddingTarget, TARGET_PROBE_TEXT, build_target_model from truememory.model_client import PreparedEmbeddingProxy, use_model_server from truememory.mps_utils import resolve_device + configured_memory = "_LOCAL_MEMORY_POLICY" in globals() + if configured_memory: + from truememory.memory_budget import validate_local_policy + validate_local_policy(globals()["_LOCAL_MEMORY_POLICY"]) if not isinstance(target, EmbeddingTarget): raise TypeError("Expected an immutable EmbeddingTarget") expires = _target_deadline(timeout) @@ -631,7 +725,25 @@ def prepare_embedding_target( if encoder is None: device = resolve_device(None) _target_remaining(expires) - encoder = build_target_model(target, device, check=lambda: _target_remaining(expires)) + budget_owner = None + if configured_memory: + from truememory.memory_budget import ( + ProfileKey, allocation_scope, bind_model_owner, configured_local_owner, + ) + budget_owner = configured_local_owner(globals()["_LOCAL_MEMORY_POLICY"], kind="local_prepared_embedding") + try: + with allocation_scope(budget_owner) if configured_memory else nullcontext() as memory: + if memory is not None: + memory.charge(ProfileKey("local prepared construction", "local_prepared_embedding", + model_identity=target.identity, device=device), + deadline=lambda: _target_remaining(expires)) + encoder = build_target_model(target, device, check=lambda: _target_remaining(expires)) + if budget_owner is not None: + bind_model_owner(encoder, budget_owner) + except BaseException: + if budget_owner is not None: + budget_owner.close() + raise _target_remaining(expires) lease = _PreparedEmbeddingLease( target, encoder, shared=shared, @@ -1172,6 +1284,19 @@ def _is_mps_oom(exc: Exception) -> bool: def _encode_with_mps_fallback(model, texts, **kwargs): """Encode texts with MPS OOM fallback. Thread-safe device transitions.""" + if "_LOCAL_MEMORY_POLICY" in globals(): + from truememory.memory_budget import ( + ProfileKey, budgeted_encode_with_mps_fallback, validate_local_policy, + ) + validate_local_policy(globals()["_LOCAL_MEMORY_POLICY"]) + count = 1 if type(texts) is str else len(texts) if type(texts) in (list, tuple) else None + batch_size = kwargs.get("batch_size") + return budgeted_encode_with_mps_fallback( + model, texts, ProfileKey("local embedding operation", "local_embedding", + input_item_count=count, + batch_size=batch_size if type(batch_size) is int and batch_size >= 0 else None), + native_kwargs=kwargs, + ) from truememory.mps_utils import encode_with_mps_fallback return encode_with_mps_fallback(model, texts, **kwargs) @@ -1695,6 +1820,10 @@ def embed_single(conn: sqlite3.Connection, message_id: int, content: str) -> Non if sep_normed is not None: separation = serialize_f32(sep_normed) except Exception as _serving_error: + if "_LOCAL_MEMORY_POLICY" in globals(): + from truememory.memory_budget import InvalidAllocationPlan, MemoryAdmissionRefused, StaleBudgetOwner + if isinstance(_serving_error, (MemoryAdmissionRefused, StaleBudgetOwner, InvalidAllocationPlan)): + raise from truememory.tier_switch.runtime import raise_if_serving_rejection raise_if_serving_rejection(_serving_error) logger.warning("Failed to prepare separation vector during incremental publication", exc_info=True) diff --git a/truememory_memory_bootstrap.py b/truememory_memory_bootstrap.py new file mode 100644 index 00000000..e1e2bc56 --- /dev/null +++ b/truememory_memory_bootstrap.py @@ -0,0 +1,5367 @@ +"""Opt-in Linux charged-domain launch, separate from physical RSS admission. + +The child enters its manager-owned cgroup before the interpreter starts. This +module uses only the standard library until that child has passed its barrier. +Kernel ceiling C is immutable; it does not track the cooperative RSS budget B. +""" +from __future__ import annotations + +from collections.abc import Mapping +import ctypes +from dataclasses import asdict, dataclass +import hashlib +import importlib.util +import json +import math +import os +from pathlib import Path +import re +import secrets +import select +import signal +import socket +import stat +import struct +import subprocess +import sys +import threading +import time +from types import MappingProxyType + + +_MAX = 2**63 - 1 +_READ_BOUND = 65536 +_CGROUP2_MAGIC = 0x63677270 +_METRIC = "linux_cgroup_charged_domain" +_FLAG = "TRUEMEMORY_REQUIRE_PROCESS_MEMORY_CAP" +_GENERATION_ENV = "TRUEMEMORY_MODEL_SERVER_GENERATION" +_MANIFEST_ENV = "TRUEMEMORY_CAP_MANIFEST" +_IDENTITY_ENV = "TRUEMEMORY_CAP_IDENTITY_SHA256" +_BARRIER_ENV = "TRUEMEMORY_CAP_BARRIER_FD" +_INTERNAL_ENV = (_GENERATION_ENV, "TRUEMEMORY_CAP_CHILD", _MANIFEST_ENV, _IDENTITY_ENV, _BARRIER_ENV) +_HEX = re.compile(r"^[0-9a-f]+$") +_UUID = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$") +_UNIT = re.compile(r"^[A-Za-z0-9_][A-Za-z0-9_.@:-]*$") + + +class EnforcementUnavailable(RuntimeError): + """Required enforcement or complete owned evidence could not be proved.""" + + +def _integer(value: object, name: str, *, positive: bool = False) -> int: + if type(value) is not int or not (1 if positive else 0) <= value <= _MAX: + raise ValueError(f"{name} must be an exact {'positive' if positive else 'nonnegative'} signed64 integer") + return value + + +def _literal(value: object, expected: object, name: str) -> None: + if type(value) is not type(expected) or value != expected: + raise ValueError(f"Invalid {name}") + + +def _literal_packet(value: object, expected: dict[str, object], name: str) -> dict[str, object]: + if type(value) is not dict or any(type(key) is not str for key in value) or set(value) != set(expected): + raise EnforcementUnavailable(f"Invalid {name} schema") + for key, literal in expected.items(): + _literal(value[key], literal, name + " " + key) + return value + + +def _hex(value: object, length: int, name: str) -> str: + if type(value) is not str or len(value) != length or _HEX.fullmatch(value) is None: + raise ValueError(f"{name} must be {length} lowercase hexadecimal characters") + return value + + +def _text(value: object, name: str, *, bound: int = 256) -> str: + if type(value) is not str or not value or len(value) > bound or "\x00" in value: + raise ValueError(f"Invalid {name}") + return value + + +def _relative(value: object, name: str) -> str: + _text(value, name, bound=4096) + if not value.startswith("/") or value == "/" or any(part in ("", ".", "..") for part in value[1:].split("/")): + raise ValueError(f"{name} must be a canonical cgroup relative path") + return value + + +def _deadline(value: object) -> float: + if type(value) is not float or not math.isfinite(value): + raise ValueError("deadline must be a finite exact float") + if value <= time.monotonic(): + raise TimeoutError("Owned Linux launch deadline expired") + return value + + +def _complete_fields(value: object, cls: type) -> None: + if type(value) is not cls: + raise TypeError(f"Expected exact {cls.__name__}") + if any(name not in vars(value) for name in cls.__dataclass_fields__): + raise ValueError(f"Incomplete {cls.__name__}") + + +def _left(deadline: float) -> float: + _deadline(deadline) + return deadline - time.monotonic() + + +_STARTUP_MANAGER_PATH = "/org/freedesktop/systemd1" +_STARTUP_MANAGER_INTERFACE = "org.freedesktop.systemd1.Manager" +_STARTUP_UNIT_INTERFACE = "org.freedesktop.systemd1.Unit" +_STARTUP_PROPERTIES_INTERFACE = "org.freedesktop.DBus.Properties" +_STARTUP_METADATA_DOMAIN = "truememory.startup.redacted_create_structure.v1" +_STARTUP_EVIDENCE_DOMAIN = "truememory.startup.typed_evidence.v1" +_STARTUP_PHASES = ("prepared", "attempted_unknown", "reply_bound_pending_invocation", "invocation_observed", "correlated_done", "unknown") +_STARTUP_REASONS = ("expired_unsent", "expired_unknown", "transport_gap", "transport_disconnected", "manager_owner_changed", "evidence_overflow", "malformed_evidence", "unsupported_signature", "conflicting_cookie", "conflicting_reply", "conflicting_job", "conflicting_unit", "ambiguous_invocation", "manager_error_unknown", "job_outcome_non_done", "replayed_native_serial", "preexisting_unit") +_STARTUP_VARIANTS = ("s", "b", "u", "t", "ay", "as", "(uo)") +_STARTUP_METADATA_SIGNATURES = _STARTUP_VARIANTS + ("a(sasb)",) +_STARTUP_KINDS = ("create_return", "create_error", "job_new", "job_removed", "unit_new", "unit_removed", "unit_properties") +_STARTUP_ENVELOPE_FIELDS = ("version", "ordinal", "kind", "sender", "destination", "serial", "reply_cookie", "path", "interface", "member", "signature", "payload", "error_name", "error_body_redacted") +_STARTUP_NATIVE_PROPERTIES = (("ActiveState", "s"), ("FreezerState", "s"), ("SubState", "s"), ("StateChangeTimestamp", "t"), ("StateChangeTimestampMonotonic", "t"), ("InactiveExitTimestamp", "t"), ("InactiveExitTimestampMonotonic", "t"), ("ActiveEnterTimestamp", "t"), ("ActiveEnterTimestampMonotonic", "t"), ("ActiveExitTimestamp", "t"), ("ActiveExitTimestampMonotonic", "t"), ("InactiveEnterTimestamp", "t"), ("InactiveEnterTimestampMonotonic", "t"), ("ConditionTimestamp", "t"), ("ConditionTimestampMonotonic", "t"), ("AssertTimestamp", "t"), ("AssertTimestampMonotonic", "t"), ("Job", "(uo)"), ("ConditionResult", "b"), ("AssertResult", "b"), ("InvocationID", "ay")) + + +@dataclass(frozen=True, slots=True) +class StartupCreateStructure: + version: int + unit_name: str + unit_path: str + job_mode: str + property_signatures: tuple[tuple[str, str], ...] + environment_names: tuple[str, ...] + auxiliary_units: tuple + + def __post_init__(self) -> None: + _startup_validate_metadata(self) + + +@dataclass(frozen=True, slots=True) +class StartupBusContext: + version: int + bus_id: str + manager_unique_name: str + client_unique_name: str + manager_uid: int + + def __post_init__(self) -> None: + _startup_validate_context(self) + + +@dataclass(frozen=True, slots=True) +class StartupProperty: + name: str + signature: str + value: object + + def __post_init__(self) -> None: + _startup_validate_property(self) + + +@dataclass(frozen=True, slots=True) +class StartupMessageEnvelope: + version: int + ordinal: int + kind: str + sender: str + destination: str | None + serial: int + reply_cookie: int | None + path: str | None + interface: str | None + member: str | None + signature: str + payload: tuple + error_name: str | None + error_body_redacted: bool + + def __post_init__(self) -> None: + _startup_validate_envelope(self) + + +@dataclass(frozen=True, slots=True) +class StartupCorrelationState: + version: int + context: StartupBusContext + metadata: StartupCreateStructure + redacted_request_metadata_sha256: str + original_deadline: float + last_now: float + phase: str + send_attempted: bool + request_cookie: int | None + envelopes: tuple[StartupMessageEnvelope, ...] + canonical_evidence_bytes: int + reason: str | None + + def __post_init__(self) -> None: + _startup_validate_state(self) + + +@dataclass(frozen=True, slots=True) +class StartupInvocationBinding: + version: int + context: StartupBusContext + unit_name: str + unit_path: str + request_cookie: int + job_id: int + job_path: str + invocation_id: bytes + redacted_request_metadata_sha256: str + reply_evidence_sha256: str + job_new_evidence_sha256: str + joint_evidence_sha256: str + job_removed_evidence_sha256: str | None + job_result: str | None + unit_new_evidence_sha256: str + + def __post_init__(self) -> None: + _startup_validate_binding(self) + + +def _startup_fail() -> None: + raise EnforcementUnavailable("Invalid bounded startup correlation evidence") + + +def _startup_fields(value: object, cls: type) -> None: + if type(value) is not cls: + _startup_fail() + for name in cls.__dataclass_fields__: + try: + object.__getattribute__(value, name) + except AttributeError: + _startup_fail() + + +def _startup_integer(value: object, maximum: int, *, minimum: int = 0) -> int: + if type(value) is not int or not minimum <= value <= maximum: + _startup_fail() + return value + + +def _startup_float(value: object) -> float: + if type(value) is not float or not math.isfinite(value) or value < 0.0: + _startup_fail() + return value + + +def _startup_text(value: object, bound: int, *, empty: bool = False, ascii_only: bool = False) -> str: + if type(value) is not str or len(value) > bound or (not empty and not value) or "\x00" in value: + _startup_fail() + if ascii_only and not value.isascii(): + _startup_fail() + try: + encoded = value.encode("utf-8") + except UnicodeEncodeError: + _startup_fail() + if len(encoded) > bound: + _startup_fail() + return value + + +def _startup_hex(value: object, length: int) -> str: + text = _startup_text(value, length, ascii_only=True) + if len(text) != length or re.fullmatch(r"[0-9a-f]+", text) is None: + _startup_fail() + return text + + +def _startup_name(value: object) -> str: + text = _startup_text(value, 128, ascii_only=True) + if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", text) is None: + _startup_fail() + return text + + +def _startup_bus_name(value: object) -> str: + text = _startup_text(value, 255, ascii_only=True) + if re.fullmatch(r":[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+", text) is None: + _startup_fail() + return text + + +def _startup_unit_path(value: object) -> str: + name = _startup_text(value, 255, ascii_only=True) + if re.fullmatch(r"[A-Za-z0-9_][A-Za-z0-9_.@:-]*", name) is None or not name.endswith((".service", ".slice")): + _startup_fail() + label = "".join(char if char.isalnum() and not (index == 0 and char.isdigit()) else "_" + format(ord(char), "02x") for index, char in enumerate(name)) + return _STARTUP_MANAGER_PATH + "/unit/" + label + + +def _startup_job_path(value: object, *, zero: bool = False) -> int: + text = _startup_text(value, 1024, ascii_only=True) + if zero and text == "/": + return 0 + prefix = _STARTUP_MANAGER_PATH + "/job/" + if not text.startswith(prefix): + _startup_fail() + number = text[len(prefix):] + if len(number) > 10 or re.fullmatch(r"[1-9][0-9]*", number) is None: + _startup_fail() + return _startup_integer(int(number), 2**32 - 1, minimum=1) + + +def _startup_object_path(value: object) -> str: + text = _startup_text(value, 1024, ascii_only=True) + if text != "/" and re.fullmatch(r"/(?:[A-Za-z0-9_]+/)*[A-Za-z0-9_]+", text) is None: + _startup_fail() + return text + + +def _startup_validate_metadata(value: object) -> StartupCreateStructure: + _startup_fields(value, StartupCreateStructure) + if type(value.version) is not int or value.version != 1 or type(value.job_mode) is not str or value.job_mode != "fail": + _startup_fail() + expected = _startup_unit_path(value.unit_name) + if _startup_text(value.unit_path, 1024, ascii_only=True) != expected: + _startup_fail() + pairs = value.property_signatures + if type(pairs) is not tuple or len(pairs) > 64: + _startup_fail() + names = [] + for pair in pairs: + if type(pair) is not tuple or len(pair) != 2: + _startup_fail() + name = _startup_name(pair[0]) + signature = _startup_text(pair[1], 64, ascii_only=True) + if signature not in _STARTUP_METADATA_SIGNATURES or (name == "Environment" and signature != "as"): + _startup_fail() + names.append(name) + if names != sorted(names) or len(set(names)) != len(names): + _startup_fail() + if type(value.environment_names) is not tuple or len(value.environment_names) > 256: + _startup_fail() + for name in value.environment_names: + text = _startup_text(name, 256) + if "=" in text: + _startup_fail() + if tuple(sorted(value.environment_names)) != value.environment_names or len(set(value.environment_names)) != len(value.environment_names): + _startup_fail() + if value.environment_names and ("Environment", "as") not in pairs: + _startup_fail() + if type(value.auxiliary_units) is not tuple or value.auxiliary_units: + _startup_fail() + return value + + +def _startup_validate_context(value: object) -> StartupBusContext: + _startup_fields(value, StartupBusContext) + if type(value.version) is not int or value.version != 1: + _startup_fail() + _startup_hex(value.bus_id, 32) + _startup_bus_name(value.manager_unique_name) + _startup_bus_name(value.client_unique_name) + if value.manager_unique_name == value.client_unique_name: + _startup_fail() + _startup_integer(value.manager_uid, 2**32 - 2) + return value + + +def _startup_validate_property(value: object) -> StartupProperty: + _startup_fields(value, StartupProperty) + name = _startup_name(value.name) + signature = _startup_text(value.signature, 64, ascii_only=True) + if signature not in _STARTUP_VARIANTS: + _startup_fail() + for native_name, native_signature in _STARTUP_NATIVE_PROPERTIES: + if name == native_name and signature != native_signature: + _startup_fail() + item = value.value + if signature == "s": + _startup_text(item, 1024, empty=True) + elif signature == "b": + if type(item) is not bool: + _startup_fail() + elif signature in ("u", "t"): + _startup_integer(item, 2**(32 if signature == "u" else 64) - 1) + elif signature == "ay": + if type(item) is not bytes or len(item) > 4096 or (name == "InvocationID" and len(item) not in (0, 16)): + _startup_fail() + elif signature == "as": + if type(item) is not tuple or len(item) > 64: + _startup_fail() + for text in item: + _startup_text(text, 1024, empty=True) + else: + if type(item) is not tuple or len(item) != 2: + _startup_fail() + number = _startup_integer(item[0], 2**32 - 1) + _startup_object_path(item[1]) + if name == "Job" and _startup_job_path(item[1], zero=True) != number: + _startup_fail() + return value + + +def _startup_validate_envelope(value: object) -> StartupMessageEnvelope: + _startup_fields(value, StartupMessageEnvelope) + if type(value.version) is not int or value.version != 1: + _startup_fail() + _startup_integer(value.ordinal, 2**64 - 1, minimum=1) + _startup_integer(value.serial, 2**32 - 1, minimum=1) + _startup_bus_name(value.sender) + if value.destination is not None: + _startup_bus_name(value.destination) + kind = _startup_text(value.kind, 64, ascii_only=True) + if kind not in _STARTUP_KINDS or type(value.payload) is not tuple or type(value.error_body_redacted) is not bool: + _startup_fail() + signature = _startup_text(value.signature, 64, empty=True, ascii_only=True) + payload = value.payload + if kind in ("create_return", "create_error"): + _startup_integer(value.reply_cookie, 2**32 - 1, minimum=1) + if value.destination is None or any(item is not None for item in (value.path, value.interface, value.member)): + _startup_fail() + if kind == "create_return": + if signature != "o" or len(payload) != 1 or value.error_name is not None or value.error_body_redacted: + _startup_fail() + _startup_job_path(payload[0]) + else: + if signature not in ("", "s") or payload or value.error_body_redacted is not True: + _startup_fail() + error = _startup_text(value.error_name, 255, ascii_only=True) + if re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)+", error) is None: + _startup_fail() + return value + if value.reply_cookie is not None or value.error_name is not None or value.error_body_redacted: + _startup_fail() + for header in (value.path, value.interface, value.member): + _startup_text(header, 1024, ascii_only=True) + _startup_object_path(value.path) + if kind == "unit_properties": + if not value.path.startswith(_STARTUP_MANAGER_PATH + "/unit/") or value.interface != _STARTUP_PROPERTIES_INTERFACE or value.member != "PropertiesChanged" or signature != "sa{sv}as" or len(payload) != 3: + _startup_fail() + if type(payload[0]) is not str or payload[0] != _STARTUP_UNIT_INTERFACE: + _startup_fail() + changed, invalidated = payload[1:] + if type(changed) is not tuple or len(changed) > 64 or type(invalidated) is not tuple or len(invalidated) > 64: + _startup_fail() + changed_names = [] + for item in changed: + _startup_validate_property(item) + changed_names.append(item.name) + for name in invalidated: + _startup_name(name) + if len(set(changed_names)) != len(changed_names) or len(set(invalidated)) != len(invalidated): + _startup_fail() + else: + member, expected_signature, arity = {"job_new": ("JobNew", "uos", 3), "job_removed": ("JobRemoved", "uoss", 4), "unit_new": ("UnitNew", "so", 2), "unit_removed": ("UnitRemoved", "so", 2)}[kind] + if value.path != _STARTUP_MANAGER_PATH or value.interface != _STARTUP_MANAGER_INTERFACE or value.member != member or signature != expected_signature or len(payload) != arity: + _startup_fail() + if kind in ("job_new", "job_removed"): + number = _startup_integer(payload[0], 2**32 - 1, minimum=1) + if _startup_job_path(payload[1]) != number: + _startup_fail() + _startup_unit_path(payload[2]) + if kind == "job_removed": + _startup_text(payload[3], 1024) + elif _startup_text(payload[1], 1024, ascii_only=True) != _startup_unit_path(payload[0]): + _startup_fail() + return value + + +def _startup_metadata_projection(value: StartupCreateStructure) -> dict[str, object]: + return {"domain": _STARTUP_METADATA_DOMAIN, "version": value.version, "path": _STARTUP_MANAGER_PATH, "interface": _STARTUP_MANAGER_INTERFACE, "member": "StartTransientUnit", "signature": "ssa(sv)a(sa(sv))", "unit_name": value.unit_name, "unit_path": value.unit_path, "job_mode": value.job_mode, "property_signatures": value.property_signatures, "environment_names": value.environment_names, "auxiliary_units": value.auxiliary_units} + + +def _startup_property_projection(value: StartupProperty) -> dict[str, object]: + item = {"type": "ay", "hex": value.value.hex()} if value.signature == "ay" else value.value + return {"name": value.name, "signature": value.signature, "value": item} + + +def _startup_envelope_projection(value: StartupMessageEnvelope) -> dict[str, object]: + payload = value.payload + if value.kind == "unit_properties": + payload = (payload[0], tuple(_startup_property_projection(item) for item in payload[1]), payload[2]) + return {"domain": _STARTUP_EVIDENCE_DOMAIN, "version": value.version, "ordinal": value.ordinal, "kind": value.kind, "sender": value.sender, "destination": value.destination, "serial": value.serial, "reply_cookie": value.reply_cookie, "path": value.path, "interface": value.interface, "member": value.member, "signature": value.signature, "payload": payload, "error_name": value.error_name, "error_body_redacted": value.error_body_redacted} + + +def _startup_canonical(value: dict[str, object], bound: int) -> bytes | None: + chunks = [] + size = 0 + encoder = json.JSONEncoder(sort_keys=True, separators=(",", ":"), ensure_ascii=False, allow_nan=False) + for piece in encoder.iterencode(value): + chunk = piece.encode("utf-8") + size += len(chunk) + if size > bound: + return None + chunks.append(chunk) + return b"".join(chunks) + + +def redacted_create_metadata_sha256(metadata: StartupCreateStructure) -> str: + _startup_validate_metadata(metadata) + canonical = _startup_canonical(_startup_metadata_projection(metadata), 16384) + if canonical is None: + _startup_fail() + return hashlib.sha256(canonical).hexdigest() + + +def parse_startup_envelope(value: object) -> StartupMessageEnvelope: + if type(value) is not dict or len(value) != len(_STARTUP_ENVELOPE_FIELDS) or any(type(name) is not str for name in value) or set(value) != set(_STARTUP_ENVELOPE_FIELDS): + _startup_fail() + return StartupMessageEnvelope(*(value[name] for name in _STARTUP_ENVELOPE_FIELDS)) + + +def startup_envelope_evidence_sha256(envelope: StartupMessageEnvelope) -> str: + _startup_validate_envelope(envelope) + canonical = _startup_canonical(_startup_envelope_projection(envelope), 16384) + if canonical is None: + _startup_fail() + return hashlib.sha256(canonical).hexdigest() + + +def _startup_scan(context: StartupBusContext, metadata: StartupCreateStructure, cookie: int | None, envelopes: tuple[StartupMessageEnvelope, ...]) -> tuple[str, str | None, dict[str, object]]: + facts: dict[str, object] = {} + seen_serials = set() + observed_job = None + observed_invocation = None + for index, envelope in enumerate(envelopes, 1): + if envelope.ordinal != index: + return "unknown", "transport_gap", facts + if envelope.sender != context.manager_unique_name: + return "unknown", "manager_owner_changed", facts + if envelope.serial in seen_serials: + return "unknown", "replayed_native_serial", facts + seen_serials.add(envelope.serial) + if envelope.destination not in (None, context.client_unique_name): + return "unknown", "conflicting_reply", facts + kind, payload = envelope.kind, envelope.payload + if kind in ("unit_new", "unit_removed"): + if payload != (metadata.unit_name, metadata.unit_path): + return "unknown", "conflicting_unit", facts + if kind == "unit_removed" or "unit_new" in facts or "job_new" in facts: + return "unknown", "conflicting_unit", facts + facts["unit_new"] = envelope + elif kind in ("create_return", "create_error"): + if "reply" in facts: + return "unknown", "conflicting_reply", facts + facts["reply"] = envelope + if cookie is not None and envelope.reply_cookie != cookie: + return "unknown", "conflicting_reply", facts + if kind == "create_error": + if cookie is not None: + return "unknown", "manager_error_unknown", facts + continue + number = _startup_job_path(payload[0]) + job = (number, payload[0]) + if observed_job is not None and job != observed_job: + return "unknown", "conflicting_job", facts + observed_job = job + elif kind in ("job_new", "job_removed"): + if payload[2] != metadata.unit_name: + return "unknown", "conflicting_unit", facts + job = payload[:2] + if observed_job is not None and job != observed_job: + return "unknown", "conflicting_job", facts + observed_job = job + if kind == "job_new": + if "unit_new" not in facts: + return "unknown", "preexisting_unit", facts + if "job_new" in facts: + return "unknown", "conflicting_job", facts + facts["job_new"] = envelope + else: + if "job_removed" in facts: + return "unknown", "conflicting_job", facts + facts["job_removed"] = envelope + if payload[3] != "done": + return "unknown", "job_outcome_non_done", facts + else: + if envelope.path != metadata.unit_path: + return "unknown", "conflicting_unit", facts + properties = {item.name: item for item in payload[1]} + job_property = properties.get("Job") + invocation_property = properties.get("InvocationID") + job = None if job_property is None or job_property.value[0] == 0 else job_property.value + invocation = None if invocation_property is None or not invocation_property.value or not any(invocation_property.value) else invocation_property.value + if job is not None: + if observed_job is not None and observed_job != job: + return "unknown", "conflicting_job", facts + observed_job = job + if invocation is not None: + if observed_invocation is not None and observed_invocation != invocation: + return "unknown", "ambiguous_invocation", facts + observed_invocation = invocation + if job is not None and invocation is not None and "Job" not in payload[2] and "InvocationID" not in payload[2]: + if "joint" not in facts: + facts["joint"] = envelope + facts["invocation_id"] = invocation + reply = facts.get("reply") + if cookie is None or reply is None or reply.kind != "create_return": + return "attempted_unknown", None, facts + if all(name in facts for name in ("unit_new", "job_new", "joint")): + return ("correlated_done" if "job_removed" in facts else "invocation_observed"), None, facts + return "reply_bound_pending_invocation", None, facts + + +def _startup_validate_state(value: object) -> StartupCorrelationState: + _startup_fields(value, StartupCorrelationState) + if type(value.version) is not int or value.version != 1: + _startup_fail() + _startup_validate_context(value.context) + _startup_validate_metadata(value.metadata) + if _startup_hex(value.redacted_request_metadata_sha256, 64) != redacted_create_metadata_sha256(value.metadata): + _startup_fail() + _startup_float(value.original_deadline) + _startup_float(value.last_now) + if value.original_deadline == 0.0 or type(value.phase) is not str or value.phase not in _STARTUP_PHASES or type(value.send_attempted) is not bool: + _startup_fail() + if value.request_cookie is not None: + _startup_integer(value.request_cookie, 2**32 - 1, minimum=1) + if not value.send_attempted: + _startup_fail() + if type(value.envelopes) is not tuple or len(value.envelopes) > 64: + _startup_fail() + if not value.send_attempted and value.envelopes: + _startup_fail() + total = 0 + for envelope in value.envelopes: + _startup_validate_envelope(envelope) + canonical = _startup_canonical(_startup_envelope_projection(envelope), 16384) + if canonical is None: + _startup_fail() + total += len(canonical) + if total > 65536: + _startup_fail() + _startup_integer(value.canonical_evidence_bytes, 65536) + if value.canonical_evidence_bytes != total: + _startup_fail() + if value.phase == "unknown": + if type(value.reason) is not str or value.reason not in _STARTUP_REASONS: + _startup_fail() + if value.reason in ("expired_unsent", "expired_unknown") and (value.last_now < value.original_deadline or (value.reason == "expired_unknown") != value.send_attempted): + _startup_fail() + else: + if value.reason is not None or value.last_now >= value.original_deadline: + _startup_fail() + if not value.send_attempted: + if value.phase != "prepared" or value.envelopes or value.request_cookie is not None: + _startup_fail() + else: + phase, reason, _ = _startup_scan(value.context, value.metadata, value.request_cookie, value.envelopes) + if phase != value.phase or reason is not None: + _startup_fail() + return value + + +def _startup_validate_binding(value: object) -> StartupInvocationBinding: + _startup_fields(value, StartupInvocationBinding) + if type(value.version) is not int or value.version != 1: + _startup_fail() + _startup_validate_context(value.context) + if _startup_text(value.unit_path, 1024, ascii_only=True) != _startup_unit_path(value.unit_name): + _startup_fail() + _startup_integer(value.request_cookie, 2**32 - 1, minimum=1) + _startup_integer(value.job_id, 2**32 - 1, minimum=1) + if _startup_job_path(value.job_path) != value.job_id or type(value.invocation_id) is not bytes or len(value.invocation_id) != 16 or not any(value.invocation_id): + _startup_fail() + for name in ("redacted_request_metadata_sha256", "reply_evidence_sha256", "job_new_evidence_sha256", "joint_evidence_sha256", "unit_new_evidence_sha256"): + _startup_hex(getattr(value, name), 64) + if value.job_removed_evidence_sha256 is None: + if value.job_result is not None: + _startup_fail() + else: + _startup_hex(value.job_removed_evidence_sha256, 64) + if type(value.job_result) is not str or value.job_result != "done": + _startup_fail() + return value + + +def _startup_now(state: StartupCorrelationState, now: object) -> float: + _startup_validate_state(state) + moment = _startup_float(now) + if moment < state.last_now: + _startup_fail() + return moment + + +def _startup_state(state: StartupCorrelationState, now: float, *, phase: str, reason: str | None = None, attempted: bool | None = None, cookie: int | None = None, envelopes: tuple[StartupMessageEnvelope, ...] | None = None, size: int | None = None) -> StartupCorrelationState: + return StartupCorrelationState(1, state.context, state.metadata, state.redacted_request_metadata_sha256, state.original_deadline, now, phase, state.send_attempted if attempted is None else attempted, state.request_cookie if cookie is None else cookie, state.envelopes if envelopes is None else envelopes, state.canonical_evidence_bytes if size is None else size, reason) + + +def _startup_unknown(state: StartupCorrelationState, now: float, reason: str) -> StartupCorrelationState: + return _startup_state(state, now, phase="unknown", reason=state.reason if state.phase == "unknown" else reason) + + +def _startup_expired(state: StartupCorrelationState, now: float) -> StartupCorrelationState | None: + if state.phase == "unknown": + return _startup_unknown(state, now, state.reason) + if now >= state.original_deadline: + return _startup_unknown(state, now, "expired_unknown" if state.send_attempted else "expired_unsent") + return None + + +def new_startup_correlation(context: StartupBusContext, metadata: StartupCreateStructure, *, deadline: float, now: float) -> StartupCorrelationState: + _startup_validate_context(context) + _startup_validate_metadata(metadata) + moment, end = _startup_float(now), _startup_float(deadline) + if end <= moment: + _startup_fail() + return StartupCorrelationState(1, context, metadata, redacted_create_metadata_sha256(metadata), end, moment, "prepared", False, None, (), 0, None) + + +def mark_startup_send_attempt(state: StartupCorrelationState, *, now: float) -> StartupCorrelationState: + moment = _startup_now(state, now) + terminal = _startup_expired(state, moment) + if terminal is not None: + return terminal + if state.send_attempted: + _startup_fail() + return _startup_state(state, moment, phase="attempted_unknown", attempted=True) + + +def bind_startup_request_cookie(state: StartupCorrelationState, *, cookie: int, now: float) -> StartupCorrelationState: + moment = _startup_now(state, now) + _startup_integer(cookie, 2**32 - 1, minimum=1) + terminal = _startup_expired(state, moment) + if terminal is not None: + return terminal + if not state.send_attempted: + _startup_fail() + if state.request_cookie is not None and cookie != state.request_cookie: + return _startup_unknown(state, moment, "conflicting_cookie") + phase, reason, _ = _startup_scan(state.context, state.metadata, cookie, state.envelopes) + return _startup_state(state, moment, phase=phase, reason=reason, cookie=cookie) + + +def advance_startup_correlation(state: StartupCorrelationState, envelope: StartupMessageEnvelope, *, now: float) -> StartupCorrelationState: + moment = _startup_now(state, now) + _startup_validate_envelope(envelope) + terminal = _startup_expired(state, moment) + if terminal is not None: + return terminal + if not state.send_attempted: + return _startup_unknown(state, moment, "preexisting_unit" if envelope.kind in ("unit_new", "unit_removed") else "transport_gap") + if len(state.envelopes) >= 64: + return _startup_unknown(state, moment, "evidence_overflow") + canonical = _startup_canonical(_startup_envelope_projection(envelope), 16384) + if canonical is None or state.canonical_evidence_bytes + len(canonical) > 65536: + return _startup_unknown(state, moment, "evidence_overflow") + if envelope.serial in tuple(item.serial for item in state.envelopes): + return _startup_unknown(state, moment, "replayed_native_serial") + envelopes = state.envelopes + (envelope,) + phase, reason, _ = _startup_scan(state.context, state.metadata, state.request_cookie, envelopes) + return _startup_state(state, moment, phase=phase, reason=reason, envelopes=envelopes, size=state.canonical_evidence_bytes + len(canonical)) + + +def invalidate_startup_correlation(state: StartupCorrelationState, *, reason: str, now: float) -> StartupCorrelationState: + moment = _startup_now(state, now) + if type(reason) is not str or reason not in _STARTUP_REASONS: + _startup_fail() + terminal = _startup_expired(state, moment) + if terminal is not None: + return terminal + if reason in ("expired_unsent", "expired_unknown"): + _startup_fail() + return _startup_unknown(state, moment, reason) + + +def expire_startup_correlation(state: StartupCorrelationState, *, now: float) -> StartupCorrelationState: + moment = _startup_now(state, now) + terminal = _startup_expired(state, moment) + if terminal is not None: + return terminal + return _startup_state(state, moment, phase=state.phase) + + +def startup_correlation_binding(state: StartupCorrelationState) -> StartupInvocationBinding | None: + _startup_validate_state(state) + if state.phase not in ("invocation_observed", "correlated_done"): + return None + _, _, facts = _startup_scan(state.context, state.metadata, state.request_cookie, state.envelopes) + reply, job_new, joint, unit_new = (facts[name] for name in ("reply", "job_new", "joint", "unit_new")) + removed = facts.get("job_removed") + return StartupInvocationBinding(1, state.context, state.metadata.unit_name, state.metadata.unit_path, state.request_cookie, job_new.payload[0], job_new.payload[1], facts["invocation_id"], state.redacted_request_metadata_sha256, startup_envelope_evidence_sha256(reply), startup_envelope_evidence_sha256(job_new), startup_envelope_evidence_sha256(joint), None if removed is None else startup_envelope_evidence_sha256(removed), None if removed is None else removed.payload[3], startup_envelope_evidence_sha256(unit_new)) + + +_RETENTION_VERSION = 1 +_RETENTION_MAX_OWNERS = 16 +_RETENTION_REQUEST_DOMAIN = "truememory.startup.retention_request.v1" +_RETENTION_PHASES = ("prepared", "local_observed", "terminal_unknown", "expired_unsent", "expired_unknown", "cancelled_unsent", "released_unsent") +_RETENTION_REASONS = _STARTUP_REASONS + ("cancelled_unsent",) + + +@dataclass(frozen=True, slots=True) +class StartupRetentionRequest: + version: int + generation: str + requested_cap_bytes: int + requested_swap_bytes: int + slice_metadata: StartupCreateStructure + service_metadata: StartupCreateStructure + + def __post_init__(self) -> None: + _retention_validate_request(self) + + +@dataclass(frozen=True, slots=True) +class StartupRetentionObservation: + version: int + stage: str + context: StartupBusContext | None + correlation: StartupCorrelationState | None + reason: str | None + + def __post_init__(self) -> None: + _retention_validate_observation(self) + + +@dataclass(frozen=True, slots=True) +class StartupRetentionOwner: + version: int + owner_id: int + creator_pid: int + + def __post_init__(self) -> None: + _retention_validate_owner(self) + + +@dataclass(frozen=True, slots=True) +class StartupRetentionSnapshot: + version: int + owner: StartupRetentionOwner + request: StartupRetentionRequest + original_deadline: float + last_now: float + operation_sequence: int + phase: str + slice_context: StartupBusContext | None + slice_correlation: StartupCorrelationState | None + service_context: StartupBusContext | None + service_correlation: StartupCorrelationState | None + slice_attempted: bool + service_attempted: bool + reason: str | None + + def __post_init__(self) -> None: + _retention_validate_snapshot(self) + + +@dataclass(frozen=True, slots=True) +class _RetentionSeal: + owner: StartupRetentionOwner + owner_id: int + creator_pid: int + original_deadline: float + request_sha256: str + request: StartupRetentionRequest + + +@dataclass(frozen=True, slots=True) +class _RetentionRecord: + seal: _RetentionSeal + last_now: float + operation_sequence: int + phase: str + slice_context: StartupBusContext | None + slice_correlation: StartupCorrelationState | None + service_context: StartupBusContext | None + service_correlation: StartupCorrelationState | None + slice_attempted: bool + service_attempted: bool + reason: str | None + + +_RETENTION_CREATOR_PID = os.getpid() +_RETENTION_LOCK = threading.RLock() +_RETENTION_RECORDS: dict[int, _RetentionRecord] = {} +_RETENTION_NEXT_OWNER_ID = 0 +_RETENTION_BUSY = False + + +def _retention_fail(message: str = "Invalid startup retention input") -> None: + if type(message) is not str or message not in ( + "Invalid startup retention input", "Startup retention belongs to a different process", + "Startup retention registry full", "Startup retention generation already retained", + "Startup retention identifier exhausted", "Startup retention sequence exhausted", + "Startup retention busy", "Startup retention owner is stale", "Startup retention sequence mismatch", + "Startup retention time moved backwards", "Startup retention invariant changed", + "Startup retention is terminal", "Startup retention has an attempted send"): + message = "Invalid startup retention input" + raise EnforcementUnavailable(message) from None + + +def _retention_exact_fields(value: object, cls: type) -> None: + if type(value) is not cls: + _retention_fail() + for name in cls.__dataclass_fields__: + try: + object.__getattribute__(value, name) + except AttributeError: + _retention_fail() + + +def _retention_validate_request(value: object) -> StartupRetentionRequest: + _retention_exact_fields(value, StartupRetentionRequest) + if (type(value.version) is not int or value.version != _RETENTION_VERSION + or type(value.generation) is not str or re.fullmatch(r"[0-9a-f]{32}", value.generation) is None + or type(value.requested_cap_bytes) is not int or not 1 <= value.requested_cap_bytes <= 2**63 - 1 + or type(value.requested_swap_bytes) is not int or value.requested_swap_bytes != 0): + _retention_fail() + try: + for stage, metadata in (("slice", value.slice_metadata), ("service", value.service_metadata)): + _startup_validate_metadata(metadata) + name = ("app-truememory" if stage == "slice" else "truememory-memory") + value.generation + "." + stage + if metadata.unit_name != name or metadata.unit_path != _startup_unit_path(name): + _retention_fail() + redacted_create_metadata_sha256(metadata) + except EnforcementUnavailable: + _retention_fail() + return value + + +def _retention_validate_observation(value: object) -> StartupRetentionObservation: + _retention_exact_fields(value, StartupRetentionObservation) + if type(value.version) is not int or value.version != _RETENTION_VERSION or type(value.stage) is not str or value.stage not in ("slice", "service"): + _retention_fail() + try: + if value.context is None: + if (value.correlation is not None or type(value.reason) is not str + or value.reason not in _STARTUP_REASONS or value.reason in ("expired_unsent", "expired_unknown")): + _retention_fail() + else: + _startup_validate_context(value.context) + if value.reason is not None: + _retention_fail() + if value.correlation is not None: + _startup_validate_state(value.correlation) + if value.correlation.context != value.context: + _retention_fail() + except EnforcementUnavailable: + _retention_fail() + return value + + +def _retention_validate_owner(value: object) -> StartupRetentionOwner: + _retention_exact_fields(value, StartupRetentionOwner) + if (type(value.version) is not int or value.version != _RETENTION_VERSION + or type(value.owner_id) is not int or not 1 <= value.owner_id <= 2**64 - 1 + or type(value.creator_pid) is not int or not 1 <= value.creator_pid <= 2**63 - 1): + _retention_fail() + return value + + +def _retention_validate_snapshot(value: object) -> StartupRetentionSnapshot: + _retention_exact_fields(value, StartupRetentionSnapshot) + _retention_validate_owner(value.owner) + _retention_validate_request(value.request) + if (type(value.version) is not int or value.version != _RETENTION_VERSION + or type(value.original_deadline) is not float or not math.isfinite(value.original_deadline) or value.original_deadline <= 0.0 + or type(value.last_now) is not float or not math.isfinite(value.last_now) or value.last_now < 0.0 + or type(value.operation_sequence) is not int or not 0 <= value.operation_sequence <= 2**64 - 1 + or type(value.phase) is not str or value.phase not in _RETENTION_PHASES + or type(value.slice_attempted) is not bool or type(value.service_attempted) is not bool + or (value.reason is not None and (type(value.reason) is not str or value.reason not in _RETENTION_REASONS))): + _retention_fail() + try: + for stage in ("slice", "service"): + context = getattr(value, stage + "_context") + correlation = getattr(value, stage + "_correlation") + attempted = getattr(value, stage + "_attempted") + if context is not None: + _startup_validate_context(context) + if correlation is None: + if attempted: + _retention_fail() + else: + _startup_validate_state(correlation) + if (context is None or context != correlation.context + or correlation.metadata != getattr(value.request, stage + "_metadata") + or correlation.original_deadline != value.original_deadline + or correlation.last_now > value.last_now or correlation.send_attempted is not attempted): + _retention_fail() + if correlation.phase == "unknown" and value.reason != correlation.reason: + _retention_fail() + except EnforcementUnavailable: + _retention_fail() + if value.slice_context is not None and value.service_context is not None and value.slice_context != value.service_context: + _retention_fail() + attempted = value.slice_attempted or value.service_attempted + if value.phase == "prepared": + if (value.operation_sequence != 0 or value.reason is not None or attempted + or any(item is not None for item in (value.slice_context, value.slice_correlation, value.service_context, value.service_correlation)) + or value.last_now >= value.original_deadline): + _retention_fail() + elif value.phase == "local_observed": + if (value.operation_sequence == 0 or value.reason is not None + or (value.slice_context is None and value.service_context is None) or value.last_now >= value.original_deadline): + _retention_fail() + else: + if value.operation_sequence == 0 or value.reason is None: + _retention_fail() + if value.phase == "terminal_unknown" and value.reason not in _STARTUP_REASONS[2:]: + _retention_fail() + if value.phase in ("expired_unsent", "expired_unknown"): + if (value.last_now < value.original_deadline or value.reason != value.phase + or attempted != (value.phase == "expired_unknown")): + _retention_fail() + if value.phase in ("cancelled_unsent", "released_unsent") and (attempted or value.reason == "expired_unknown"): + _retention_fail() + return value + + +def _retention_validate_record(value: object) -> _RetentionRecord: + _retention_exact_fields(value, _RetentionRecord) + seal = value.seal + _retention_exact_fields(seal, _RetentionSeal) + _retention_validate_owner(seal.owner) + _retention_validate_request(seal.request) + if (type(seal.owner_id) is not int or seal.owner_id != seal.owner.owner_id + or type(seal.creator_pid) is not int or seal.creator_pid != seal.owner.creator_pid + or type(seal.request_sha256) is not str or re.fullmatch(r"[0-9a-f]{64}", seal.request_sha256) is None + or seal.request_sha256 != _retention_request_sha256(seal.request)): + _retention_fail("Startup retention invariant changed") + StartupRetentionSnapshot(_RETENTION_VERSION, seal.owner, seal.request, seal.original_deadline, + value.last_now, value.operation_sequence, value.phase, + value.slice_context, value.slice_correlation, value.service_context, + value.service_correlation, value.slice_attempted, value.service_attempted, value.reason) + return value + + +def _retention_pid() -> int: + pid = os.getpid() + if (type(pid) is not int or not 1 <= pid <= 2**63 - 1 + or type(_RETENTION_CREATOR_PID) is not int or pid != _RETENTION_CREATOR_PID): + _retention_fail("Startup retention belongs to a different process") + return pid + + +def _retention_enter() -> object: + global _RETENTION_BUSY + _retention_pid() + lock = _RETENTION_LOCK + if not lock.acquire(blocking=False): + _retention_fail("Startup retention busy") + try: + _retention_pid() + if type(_RETENTION_BUSY) is not bool or _RETENTION_BUSY or _RETENTION_LOCK is not lock: + _retention_fail("Startup retention busy") + _RETENTION_BUSY = True + return lock + except EnforcementUnavailable: + lock.release() + raise + + +def _retention_exit(lock: object) -> None: + global _RETENTION_BUSY + _RETENTION_BUSY = False + lock.release() + + +def _retention_now(record: _RetentionRecord, now: object) -> float: + if type(now) is not float or not math.isfinite(now) or now < 0.0: + _retention_fail() + if now < record.last_now: + _retention_fail("Startup retention time moved backwards") + return now + + +def _retention_expected_sequence(record: _RetentionRecord, value: object) -> int: + if type(value) is not int or not 0 <= value <= 2**64 - 1: + _retention_fail() + if value != record.operation_sequence: + _retention_fail("Startup retention sequence mismatch") + if value == 2**64 - 1: + _retention_fail("Startup retention sequence exhausted") + return value + 1 + + +def _retention_request_sha256(request: StartupRetentionRequest) -> str: + _retention_validate_request(request) + projection = {"domain": _RETENTION_REQUEST_DOMAIN, "version": request.version, + "generation": request.generation, "requested_cap_bytes": request.requested_cap_bytes, + "requested_swap_bytes": request.requested_swap_bytes, + "slice_metadata_sha256": redacted_create_metadata_sha256(request.slice_metadata), + "service_metadata_sha256": redacted_create_metadata_sha256(request.service_metadata)} + raw = _startup_canonical(projection, 16384) + if raw is None: + _retention_fail() + return hashlib.sha256(raw).hexdigest() + + +def _retention_copy_metadata(value: StartupCreateStructure) -> StartupCreateStructure: + try: + _startup_validate_metadata(value) + redacted_create_metadata_sha256(value) + return StartupCreateStructure(value.version, value.unit_name, value.unit_path, value.job_mode, + tuple((name, signature) for name, signature in value.property_signatures), + tuple(name for name in value.environment_names), ()) + except EnforcementUnavailable: + _retention_fail() + + +def _retention_copy_context(value: StartupBusContext | None) -> StartupBusContext | None: + if value is None: + return None + try: + _startup_validate_context(value) + return StartupBusContext(value.version, value.bus_id, value.manager_unique_name, value.client_unique_name, value.manager_uid) + except EnforcementUnavailable: + _retention_fail() + + +def _retention_copy_property(value: StartupProperty) -> StartupProperty: + try: + _startup_validate_property(value) + item = value.value + if value.signature in ("as", "(uo)"): + item = tuple(part for part in item) + elif value.signature == "ay": + item = bytes(part for part in item) + return StartupProperty(value.name, value.signature, item) + except EnforcementUnavailable: + _retention_fail() + + +def _retention_copy_envelope(value: StartupMessageEnvelope) -> StartupMessageEnvelope: + try: + _startup_validate_envelope(value) + payload = value.payload + if value.kind == "unit_properties": + payload = (payload[0], tuple(_retention_copy_property(item) for item in payload[1]), tuple(name for name in payload[2])) + else: + payload = tuple(item for item in payload) + return StartupMessageEnvelope(value.version, value.ordinal, value.kind, value.sender, value.destination, + value.serial, value.reply_cookie, value.path, value.interface, value.member, + value.signature, payload, value.error_name, value.error_body_redacted) + except EnforcementUnavailable: + _retention_fail() + + +def _retention_copy_correlation(value: StartupCorrelationState | None) -> StartupCorrelationState | None: + if value is None: + return None + try: + _startup_validate_state(value) + return StartupCorrelationState(value.version, _retention_copy_context(value.context), + _retention_copy_metadata(value.metadata), value.redacted_request_metadata_sha256, + value.original_deadline, value.last_now, value.phase, value.send_attempted, + value.request_cookie, tuple(_retention_copy_envelope(item) for item in value.envelopes), + value.canonical_evidence_bytes, value.reason) + except EnforcementUnavailable: + _retention_fail() + + +def _retention_copy_request(value: StartupRetentionRequest) -> StartupRetentionRequest: + _retention_validate_request(value) + return StartupRetentionRequest(value.version, value.generation, value.requested_cap_bytes, + value.requested_swap_bytes, _retention_copy_metadata(value.slice_metadata), + _retention_copy_metadata(value.service_metadata)) + + +def _retention_stage_consistency(record: _RetentionRecord, observation: StartupRetentionObservation, now: float) -> None: + _retention_validate_observation(observation) + stage = observation.stage + context, correlation = observation.context, observation.correlation + previous_context = getattr(record, stage + "_context") + previous_correlation = getattr(record, stage + "_correlation") + other = getattr(record, ("service" if stage == "slice" else "slice") + "_context") + if context is not None and ((previous_context is not None and context != previous_context) or (other is not None and context != other)): + _retention_fail() + if correlation is not None: + if (correlation.metadata != getattr(record.seal.request, stage + "_metadata") + or correlation.original_deadline != record.seal.original_deadline or correlation.last_now > now): + _retention_fail() + if previous_correlation is not None: + if (correlation.last_now < previous_correlation.last_now + or correlation.envelopes[:len(previous_correlation.envelopes)] != previous_correlation.envelopes + or (previous_correlation.request_cookie is not None and correlation.request_cookie != previous_correlation.request_cookie) + or (previous_correlation.send_attempted and not correlation.send_attempted) + or (previous_correlation.reason is not None and correlation.reason != previous_correlation.reason)): + _retention_fail() + if getattr(record, stage + "_attempted") and not correlation.send_attempted: + _retention_fail() + + +def _retention_expire(record: _RetentionRecord, now: float) -> _RetentionRecord: + if record.phase in ("prepared", "local_observed") and now >= record.seal.original_deadline: + sequence = _retention_expected_sequence(record, record.operation_sequence) + phase = "expired_unknown" if record.slice_attempted or record.service_attempted else "expired_unsent" + return _retention_make_record(record, now, sequence, phase, phase) + return _retention_make_record(record, now, record.operation_sequence, record.phase, record.reason) + + +def _retention_make_record(record: _RetentionRecord, now: float, sequence: int, phase: str, reason: str | None, + *, stage: str | None = None, context: StartupBusContext | None = None, + correlation: StartupCorrelationState | None = None) -> _RetentionRecord: + values = [record.slice_context, record.slice_correlation, record.service_context, + record.service_correlation, record.slice_attempted, record.service_attempted] + if stage is not None: + index, attempt_index = (0, 4) if stage == "slice" else (2, 5) + if context is not None: + values[index] = context + if correlation is not None: + values[index + 1] = correlation + values[attempt_index] = values[attempt_index] or correlation.send_attempted + result = _RetentionRecord(record.seal, now, sequence, phase, *values, reason) + _retention_validate_record(result) + return result + + +def _retention_commit(owner: StartupRetentionOwner, previous: _RetentionRecord, + candidate: _RetentionRecord, result: StartupRetentionSnapshot, + *, registry: dict[int, _RetentionRecord]) -> StartupRetentionSnapshot: + _retention_validate_record(candidate) + _retention_validate_snapshot(result) + if (candidate.seal is not previous.seal or result.owner is not owner + or result.request != candidate.seal.request or result.original_deadline != candidate.seal.original_deadline + or tuple(getattr(result, name) for name in _RetentionRecord.__dataclass_fields__ if name != "seal") + != tuple(getattr(candidate, name) for name in _RetentionRecord.__dataclass_fields__ if name != "seal")): + _retention_fail("Startup retention invariant changed") + _retention_guard_seal(owner, previous, registry=registry) + _RETENTION_RECORDS[previous.seal.owner_id] = candidate + return result + + +def _retention_snapshot(record: _RetentionRecord, *, phase: str | None = None, sequence: int | None = None) -> StartupRetentionSnapshot: + _retention_validate_record(record) + return StartupRetentionSnapshot(_RETENTION_VERSION, record.seal.owner, _retention_copy_request(record.seal.request), + record.seal.original_deadline, record.last_now, + record.operation_sequence if sequence is None else sequence, + record.phase if phase is None else phase, + _retention_copy_context(record.slice_context), _retention_copy_correlation(record.slice_correlation), + _retention_copy_context(record.service_context), _retention_copy_correlation(record.service_correlation), + record.slice_attempted, record.service_attempted, record.reason) + + +def _retention_guard_seal(owner: StartupRetentionOwner, expected: _RetentionRecord | None = None, + *, registry: dict[int, _RetentionRecord] | None = None) -> _RetentionRecord: + _retention_pid() + if type(owner) is not StartupRetentionOwner: + _retention_fail() + if (type(_RETENTION_RECORDS) is not dict or len(_RETENTION_RECORDS) > _RETENTION_MAX_OWNERS or _RETENTION_BUSY is not True + or (registry is not None and _RETENTION_RECORDS is not registry) + or type(_RETENTION_NEXT_OWNER_ID) is not int or not 0 <= _RETENTION_NEXT_OWNER_ID <= 2**64 - 1): + _retention_fail("Startup retention invariant changed") + found = None + for key, record in _RETENTION_RECORDS.items(): + _retention_exact_fields(record, _RetentionRecord) + _retention_exact_fields(record.seal, _RetentionSeal) + if type(key) is not int or key != record.seal.owner_id or not 1 <= key <= _RETENTION_NEXT_OWNER_ID: + _retention_fail("Startup retention invariant changed") + if record.seal.owner is owner: + if found is not None or type(key) is not int or key != record.seal.owner_id: + _retention_fail("Startup retention invariant changed") + found = record + if found is None: + _retention_validate_owner(owner) + _retention_fail("Startup retention owner is stale") + if expected is not None and found is not expected: + _retention_fail("Startup retention invariant changed") + try: + _retention_validate_record(found) + except EnforcementUnavailable: + _retention_fail("Startup retention invariant changed") + if found.seal.creator_pid != _RETENTION_CREATOR_PID: + _retention_fail("Startup retention invariant changed") + return found + + +def _retention_unsent(record: _RetentionRecord) -> None: + if record.slice_attempted or record.service_attempted: + _retention_fail("Startup retention has an attempted send") + + +def _retention_release(owner: StartupRetentionOwner, record: _RetentionRecord, + now: float, sequence: int, *, registry: dict[int, _RetentionRecord]) -> StartupRetentionSnapshot: + _retention_unsent(record) + if record.phase not in ("cancelled_unsent", "expired_unsent"): + _retention_fail("Startup retention is terminal") + candidate = _retention_make_record(record, now, sequence, "released_unsent", record.reason) + result = _retention_snapshot(candidate) + _retention_validate_record(candidate) + _retention_validate_snapshot(result) + if (result.owner is not owner or result.request != candidate.seal.request + or result.original_deadline != candidate.seal.original_deadline + or tuple(getattr(result, name) for name in _RetentionRecord.__dataclass_fields__ if name != "seal") + != tuple(getattr(candidate, name) for name in _RetentionRecord.__dataclass_fields__ if name != "seal")): + _retention_fail("Startup retention invariant changed") + _retention_guard_seal(owner, record, registry=registry) + del _RETENTION_RECORDS[record.seal.owner_id] + return result + + +def prepare_startup_retention(request: StartupRetentionRequest, *, deadline: float, now: float) -> StartupRetentionOwner: + global _RETENTION_NEXT_OWNER_ID + lock = _retention_enter() + try: + _retention_validate_request(request) + if (type(now) is not float or not math.isfinite(now) or now < 0.0 + or type(deadline) is not float or not math.isfinite(deadline) or deadline <= now): + _retention_fail() + records = _RETENTION_RECORDS + previous_id = _RETENTION_NEXT_OWNER_ID + if type(records) is not dict or len(records) > _RETENTION_MAX_OWNERS or type(previous_id) is not int or not 0 <= previous_id <= 2**64 - 1: + _retention_fail("Startup retention invariant changed") + if len(records) >= _RETENTION_MAX_OWNERS: + _retention_fail("Startup retention registry full") + for key, record in records.items(): + _retention_validate_record(record) + if type(key) is not int or key != record.seal.owner_id or not 1 <= key <= previous_id or record.seal.creator_pid != _RETENTION_CREATOR_PID: + _retention_fail("Startup retention invariant changed") + if record.seal.request.generation == request.generation: + _retention_fail("Startup retention generation already retained") + if previous_id == 2**64 - 1: + _retention_fail("Startup retention identifier exhausted") + requested_sha = _retention_request_sha256(request) + copied_request = _retention_copy_request(request) + owner = StartupRetentionOwner(_RETENTION_VERSION, previous_id + 1, _retention_pid()) + seal = _RetentionSeal(owner, owner.owner_id, owner.creator_pid, deadline, requested_sha, copied_request) + record = _RetentionRecord(seal, now, 0, "prepared", None, None, None, None, False, False, None) + _retention_validate_record(record) + if (records is not _RETENTION_RECORDS or _RETENTION_NEXT_OWNER_ID != previous_id + or len(records) >= _RETENTION_MAX_OWNERS or _retention_request_sha256(request) != requested_sha + or _RETENTION_BUSY is not True or owner.creator_pid != _retention_pid()): + _retention_fail("Startup retention invariant changed") + for key, existing in records.items(): + _retention_validate_record(existing) + if type(key) is not int or key != existing.seal.owner_id or not 1 <= key <= previous_id or existing.seal.creator_pid != _RETENTION_CREATOR_PID: + _retention_fail("Startup retention invariant changed") + if existing.seal.request.generation == copied_request.generation: + _retention_fail("Startup retention generation already retained") + _retention_validate_record(record) + if (_retention_request_sha256(request) != requested_sha or records is not _RETENTION_RECORDS + or _RETENTION_NEXT_OWNER_ID != previous_id or _RETENTION_BUSY is not True + or owner.creator_pid != _retention_pid()): + _retention_fail("Startup retention invariant changed") + records[owner.owner_id] = record + _RETENTION_NEXT_OWNER_ID = owner.owner_id + return owner + finally: + _retention_exit(lock) + + +def startup_retention_snapshot(owner: StartupRetentionOwner, *, now: float) -> StartupRetentionSnapshot: + lock = _retention_enter() + try: + registry = _RETENTION_RECORDS + record = _retention_guard_seal(owner) + moment = _retention_now(record, now) + candidate = _retention_expire(record, moment) + result = _retention_snapshot(candidate) + return _retention_commit(owner, record, candidate, result, registry=registry) + finally: + _retention_exit(lock) + + +def publish_startup_retention(owner: StartupRetentionOwner, observation: StartupRetentionObservation, + *, expected_sequence: int, now: float) -> StartupRetentionSnapshot: + lock = _retention_enter() + try: + registry = _RETENTION_RECORDS + record = _retention_guard_seal(owner) + moment = _retention_now(record, now) + sequence = _retention_expected_sequence(record, expected_sequence) + _retention_validate_observation(observation) + if record.phase not in ("prepared", "local_observed") or moment >= record.seal.original_deadline: + _retention_fail("Startup retention is terminal") + _retention_stage_consistency(record, observation, moment) + stage, observed_reason = observation.stage, observation.reason + context = _retention_copy_context(observation.context) + correlation = _retention_copy_correlation(observation.correlation) + reason = observed_reason if correlation is None else correlation.reason + phase = "terminal_unknown" if reason is not None else "local_observed" + candidate = _retention_make_record(record, moment, sequence, phase, reason, + stage=stage, context=context, correlation=correlation) + result = _retention_snapshot(candidate) + _retention_stage_consistency(record, observation, moment) + if (observation.stage != stage or observation.context != context + or observation.correlation != correlation or observation.reason != observed_reason): + _retention_fail("Startup retention invariant changed") + return _retention_commit(owner, record, candidate, result, registry=registry) + finally: + _retention_exit(lock) + + +def invalidate_startup_retention(owner: StartupRetentionOwner, *, reason: str, + expected_sequence: int, now: float) -> StartupRetentionSnapshot: + lock = _retention_enter() + try: + registry = _RETENTION_RECORDS + record = _retention_guard_seal(owner) + moment = _retention_now(record, now) + sequence = _retention_expected_sequence(record, expected_sequence) + if type(reason) is not str or reason not in _STARTUP_REASONS[2:]: + _retention_fail() + if record.phase not in ("prepared", "local_observed") or moment >= record.seal.original_deadline: + _retention_fail("Startup retention is terminal") + candidate = _retention_make_record(record, moment, sequence, "terminal_unknown", reason) + return _retention_commit(owner, record, candidate, _retention_snapshot(candidate), registry=registry) + finally: + _retention_exit(lock) + + +def cancel_unsent_startup_retention(owner: StartupRetentionOwner, *, expected_sequence: int, + now: float) -> StartupRetentionSnapshot: + lock = _retention_enter() + try: + registry = _RETENTION_RECORDS + record = _retention_guard_seal(owner) + moment = _retention_now(record, now) + sequence = _retention_expected_sequence(record, expected_sequence) + _retention_unsent(record) + if record.phase not in ("prepared", "local_observed", "terminal_unknown", "expired_unsent"): + _retention_fail("Startup retention is terminal") + candidate = _retention_make_record(record, moment, sequence, "cancelled_unsent", + record.reason if record.reason is not None else "cancelled_unsent") + return _retention_commit(owner, record, candidate, _retention_snapshot(candidate), registry=registry) + finally: + _retention_exit(lock) + + +def release_unsent_startup_retention(owner: StartupRetentionOwner, *, expected_sequence: int, + now: float) -> StartupRetentionSnapshot: + lock = _retention_enter() + try: + registry = _RETENTION_RECORDS + record = _retention_guard_seal(owner) + moment = _retention_now(record, now) + sequence = _retention_expected_sequence(record, expected_sequence) + return _retention_release(owner, record, moment, sequence, registry=registry) + finally: + _retention_exit(lock) + + +@dataclass(frozen=True) +class LinuxDaemonLaunchPolicy: + version: int = 1 + cap_bytes: int = 8589934592 + swap_bytes: int = 0 + metric: str = _METRIC + physical_hard_cap: bool = False + dynamic_registry_cap: bool = False + + def __post_init__(self) -> None: + _validate_policy(self) + + +def _validate_policy(policy: object) -> LinuxDaemonLaunchPolicy: + _complete_fields(policy, LinuxDaemonLaunchPolicy) + _literal(policy.version, 1, "policy version") + _integer(policy.cap_bytes, "cap_bytes", positive=True) + _literal(policy.swap_bytes, 0, "swap_bytes") + _literal(policy.metric, _METRIC, "metric") + _literal(policy.physical_hard_cap, False, "physical_hard_cap") + _literal(policy.dynamic_registry_cap, False, "dynamic_registry_cap") + return policy + + +@dataclass(frozen=True) +class LinuxInvocationIdentity: + version: int + generation: str + boot_id: str + uid: int + slice_unit: str + slice_invocation_id: str + slice_mount_id: int + slice_device: int + slice_inode: int + slice_relative_path: str + service_unit: str + service_invocation_id: str + service_device: int + service_inode: int + service_relative_path: str + main_pid: int + start_ticks: int + birth_time: float + cap_bytes: int + swap_bytes: int + bootstrap_sha256: str + manifest_sha256: str + + def __post_init__(self) -> None: + _validate_identity(self) + + +def _validate_identity(identity: object) -> LinuxInvocationIdentity: + _complete_fields(identity, LinuxInvocationIdentity) + _literal(identity.version, 1, "identity version") + _hex(identity.generation, 32, "generation") + if type(identity.boot_id) is not str or _UUID.fullmatch(identity.boot_id) is None: + raise ValueError("Invalid boot UUID") + _integer(identity.uid, "uid") + for name, suffix in (("slice_unit", ".slice"), ("service_unit", ".service")): + value = _text(getattr(identity, name), name) + if _UNIT.fullmatch(value) is None or not value.endswith(suffix) or identity.generation not in value: + raise ValueError(f"Invalid generation-scoped {name}") + if not identity.slice_unit.startswith("app-"): + raise ValueError("Owned slice must be below the existing app slice") + for name in ("slice_invocation_id", "service_invocation_id"): + _hex(getattr(identity, name), 32, name) + for name in ("slice_mount_id", "slice_device", "slice_inode", "service_device", "service_inode", "main_pid", "start_ticks", "cap_bytes"): + _integer(getattr(identity, name), name, positive=True) + _relative(identity.slice_relative_path, "slice_relative_path") + _relative(identity.service_relative_path, "service_relative_path") + if not identity.service_relative_path.startswith(identity.slice_relative_path + "/"): + raise ValueError("Child service must be strictly inside the owned slice") + if type(identity.birth_time) is not float or not math.isfinite(identity.birth_time) or identity.birth_time <= 0: + raise ValueError("birth_time must be a finite positive exact float") + _literal(identity.swap_bytes, 0, "swap_bytes") + _hex(identity.bootstrap_sha256, 64, "bootstrap_sha256") + _hex(identity.manifest_sha256, 64, "manifest_sha256") + return identity + + +@dataclass(frozen=True) +class LinuxProvisionalLaunch: + identity: LinuxInvocationIdentity + runner: object + pidfd: object + scope_fd: object + endpoint: object + + def __post_init__(self) -> None: + _validate_record(self) + + +def _validate_record(record: object) -> LinuxProvisionalLaunch: + _complete_fields(record, LinuxProvisionalLaunch) + _validate_identity(record.identity) + for name in ("runner", "pidfd", "scope_fd", "endpoint"): + value = getattr(record, name) + if value is None or type(value) in (bool, float, str, bytes, tuple, dict, list): + raise ValueError(f"Missing strong owned {name} handle") + if type(value) is int and (name != "pidfd" or value < 0): + raise ValueError(f"Invalid owned {name} handle") + return record + + +@dataclass(frozen=True) +class LinuxScopeEvidence: + identity: LinuxInvocationIdentity + filesystem_magic: int + mount_type: str + mount_authenticated: bool + namespace_authenticated: bool + membership_authenticated: bool + peer_uid: int + peer_pid: int + peer_start_ticks: int + generation: str + manifest_sha256: str + members: tuple[int, ...] + cap_bytes: int + swap_bytes: int + leaf_memory_max: str + leaf_swap_max: str + kernel_limit_preexec: bool + + def __post_init__(self) -> None: + _validate_evidence(self, self.identity, terminal=True) + + +def _validate_evidence(evidence: object, identity: LinuxInvocationIdentity, *, terminal: bool = False) -> LinuxScopeEvidence: + _validate_identity(identity) + _complete_fields(evidence, LinuxScopeEvidence) + _validate_identity(evidence.identity) + if evidence.identity != identity: + raise EnforcementUnavailable("Invocation identity drift") + _literal(evidence.filesystem_magic, _CGROUP2_MAGIC, "filesystem_magic") + _literal(evidence.mount_type, "cgroup2", "mount_type") + for name in ("mount_authenticated", "namespace_authenticated", "membership_authenticated", "kernel_limit_preexec"): + _literal(getattr(evidence, name), True, name) + for name, expected in (("peer_uid", identity.uid), ("peer_pid", identity.main_pid), ("peer_start_ticks", identity.start_ticks), ("cap_bytes", identity.cap_bytes), ("swap_bytes", 0)): + _literal(getattr(evidence, name), expected, name) + _literal(evidence.generation, identity.generation, "generation") + _literal(evidence.manifest_sha256, identity.manifest_sha256, "manifest_sha256") + if type(evidence.members) is not tuple: + raise ValueError("members must be an exact tuple") + for value in evidence.members: + _integer(value, "member PID", positive=True) + if evidence.members != (identity.main_pid,) and not (terminal and evidence.members == ()): + raise EnforcementUnavailable("Missing or foreign startup member") + leaf = "max" + if terminal and evidence.members == () and type(evidence.leaf_memory_max) is str and evidence.leaf_memory_max == "unavailable_pruned": + leaf = "unavailable_pruned" + _literal(evidence.leaf_memory_max, leaf, "leaf_memory_max") + _literal(evidence.leaf_swap_max, leaf, "leaf_swap_max") + return evidence + + +@dataclass(frozen=True) +class LinuxExitReceipt: + version: int + identity: LinuxInvocationIdentity + exit_code: int | None + signal: int | None + manager_result: str + memory_current: int + memory_peak: int | None + events_before: tuple[tuple[str, int], ...] + events_after: tuple[tuple[str, int], ...] + populated: bool + descendant_members: tuple[tuple[str, tuple[int, ...]], ...] + durable_before_stop: bool + receipt_sequence: int + receipt_chain_sha256: str + artifact_sha256: tuple[tuple[str, str], ...] + metric: str = _METRIC + physical_hard_cap: bool = False + kernel_cap_bytes: int = 8589934592 + dynamic_registry_cap: bool = False + + def __post_init__(self) -> None: + _validate_receipt(self, self.identity) + + +def _events(value: object, name: str) -> tuple[tuple[str, int], ...]: + if type(value) is not tuple: + raise ValueError(f"{name} must be an exact tuple") + names = set() + for item in value: + if type(item) is not tuple or len(item) != 2: + raise ValueError(f"Invalid {name} entry") + key = _text(item[0], name, bound=64) + if not key.isascii() or not re.fullmatch(r"[A-Za-z0-9_]+", key) or key in names: + raise ValueError(f"Invalid or duplicate {name} event") + _integer(item[1], name) + names.add(key) + return value + + +def _validate_receipt(receipt: object, identity: LinuxInvocationIdentity) -> LinuxExitReceipt: + _validate_identity(identity) + _complete_fields(receipt, LinuxExitReceipt) + _literal(receipt.version, 1, "receipt version") + _validate_identity(receipt.identity) + if receipt.identity != identity: + raise EnforcementUnavailable("Terminal identity drift") + if receipt.exit_code is not None: + _integer(receipt.exit_code, "exit_code") + if receipt.signal is not None: + _integer(receipt.signal, "signal", positive=True) + _text(receipt.manager_result, "manager_result") + _integer(receipt.memory_current, "memory_current") + if receipt.memory_peak is not None: + _integer(receipt.memory_peak, "memory_peak") + before = dict(_events(receipt.events_before, "events_before")) + after = dict(_events(receipt.events_after, "events_after")) + if any(name not in after or after[name] < count for name, count in before.items()): + raise EnforcementUnavailable("Controller events decreased or disappeared") + _literal(receipt.populated, False, "populated") + if type(receipt.descendant_members) is not tuple or not receipt.descendant_members: + raise ValueError("Missing recursive membership evidence") + paths = set() + for item in receipt.descendant_members: + if type(item) is not tuple or len(item) != 2: + raise ValueError("Malformed descendant membership") + path, members = item + _relative(path, "descendant path") + if path in paths or not (path == identity.slice_relative_path or path.startswith(identity.slice_relative_path + "/")): + raise EnforcementUnavailable("Foreign or duplicate descendant path") + if type(members) is not tuple or members != (): + raise EnforcementUnavailable("Terminal domain still contains a process") + paths.add(path) + if identity.slice_relative_path not in paths: + raise EnforcementUnavailable("Persistent slice empty proof is missing") + _literal(receipt.durable_before_stop, True, "durable_before_stop") + _integer(receipt.receipt_sequence, "receipt_sequence", positive=True) + _hex(receipt.receipt_chain_sha256, 64, "receipt_chain_sha256") + if type(receipt.artifact_sha256) is not tuple or not receipt.artifact_sha256: + raise ValueError("Missing retained artifact hashes") + names = set() + for item in receipt.artifact_sha256: + if type(item) is not tuple or len(item) != 2: + raise ValueError("Malformed artifact hash") + name = _text(item[0], "artifact name") + if name in names or not re.fullmatch(r"[A-Za-z0-9_.-]+", name) or name in (".", ".."): + raise ValueError("Unsafe or duplicate artifact name") + _hex(item[1], 64, "artifact SHA256") + names.add(name) + _literal(receipt.metric, _METRIC, "metric") + _literal(receipt.physical_hard_cap, False, "physical_hard_cap") + _literal(receipt.kernel_cap_bytes, identity.cap_bytes, "kernel_cap_bytes") + _literal(receipt.dynamic_registry_cap, False, "dynamic_registry_cap") + return receipt + + +def required_cap_selected(environment: Mapping[str, str]) -> bool: + if not isinstance(environment, Mapping): + raise TypeError("environment must be a mapping") + if _FLAG not in environment: + return False + _literal(environment[_FLAG], "1", _FLAG) + return True + + +def plan_required_linux_launch(environment: Mapping[str, str], *, physical_total: int | None) -> LinuxDaemonLaunchPolicy: + if not required_cap_selected(environment): + raise ValueError("Required Linux mode was not explicitly selected") + if "TRUEMEMORY_MODEL_SERVER_MAX_RSS_MB" in environment: + raw = environment["TRUEMEMORY_MODEL_SERVER_MAX_RSS_MB"] + if type(raw) is not str or not re.fullmatch(r"[1-9][0-9]*", raw) or len(raw) > 19: + raise ValueError("Required MAX_RSS_MB must be canonical positive decimal") + megabytes = _integer(int(raw), "MAX_RSS_MB", positive=True) + cap = _integer(megabytes * 1048576, "cap_bytes", positive=True) + else: + total = _integer(physical_total, "physical_total", positive=True) + cap = _integer(min(8589934592, total // 4), "cap_bytes", positive=True) + return LinuxDaemonLaunchPolicy(1, cap, 0) + + +def _validate_claim(claim: object, *, pid: int | None = None, birth_time: float | None = None, + generation: str | None = None, pending: bool = False) -> dict[str, object]: + if type(claim) is not dict or any(type(key) is not str for key in claim) or set(claim) != {"generation", "launcher", "child"}: + raise ValueError("Malformed current startup claim") + _hex(claim["generation"], 32, "claim generation") + for name in ("launcher", "child"): + item = claim[name] + if name == "child" and item is None and pending: + continue + if type(item) is not dict or any(type(key) is not str for key in item) or set(item) != {"pid", "born"}: + raise ValueError("Malformed current process claim") + _integer(item["pid"], "claim pid", positive=True) + if type(item["born"]) is not float or not math.isfinite(item["born"]) or item["born"] <= 0: + raise ValueError("Claim birth must be a finite positive exact float") + if generation is not None: + _literal(claim["generation"], generation, "claim generation") + if pid is not None: + _literal(claim["child"]["pid"], pid, "claim child pid") + if birth_time is not None: + _literal(claim["child"]["born"], birth_time, "claim child birth") + return claim + + +def inspect_existing_owned_linux(policy: LinuxDaemonLaunchPolicy, *, pid: int, birth_time: float, + generation: str, claim: dict[str, object], deadline: float, + adapter: object | None = None) -> LinuxScopeEvidence: + """Observe current capped membership without acquiring launch authority.""" + _validate_policy(policy) + expected_policy = _policy_snapshot(policy) + _integer(pid, "existing pid", positive=True) + if type(birth_time) is not float or not math.isfinite(birth_time) or birth_time <= 0: + raise ValueError("Existing birth must be a finite positive exact float") + _hex(generation, 32, "existing generation") + _validate_claim(claim, pid=pid, birth_time=birth_time, generation=generation) + expected_claim = _canonical(claim) + _deadline(deadline) + observer = get_linux_adapter() if adapter is None else adapter + inspect_existing = getattr(observer, "inspect_existing", None) + if not callable(inspect_existing): + raise EnforcementUnavailable("Readonly Linux inspection is unavailable") + _validate_policy(policy) + _validate_claim(claim, pid=pid, birth_time=birth_time, generation=generation) + if policy != expected_policy or _canonical(claim) != expected_claim: + raise EnforcementUnavailable("Existing launch inputs changed during observer resolution") + _deadline(deadline) + result = inspect_existing(policy, pid=pid, birth_time=birth_time, generation=generation, + claim=claim, deadline=deadline) + _complete_fields(result, LinuxScopeEvidence) + identity = _validate_identity(result.identity) + _validate_evidence(result, identity) + _literal(identity.main_pid, pid, "existing MainPID") + _literal(identity.birth_time, birth_time, "existing birth") + _literal(identity.generation, generation, "existing generation") + _validate_policy(policy) + _validate_claim(claim, pid=pid, birth_time=birth_time, generation=generation) + if policy != expected_policy or _canonical(claim) != expected_claim or identity.cap_bytes > expected_policy.cap_bytes: + raise EnforcementUnavailable("Current capped daemon is incompatible with the requested policy") + _deadline(deadline) + return result + + +class OwnedLinuxLaunch: + def __init__(self, record: LinuxProvisionalLaunch, policy: LinuxDaemonLaunchPolicy, adapter: object) -> None: + self.record = _validate_record(record) + self.policy = _validate_policy(policy) + self._adapter = adapter + self.pid = record.identity.main_pid + self._released = False + + def release_after_claim(self, claim: Mapping[str, object], *, deadline: float) -> None: + record, policy = _validate_handle(self) + _deadline(deadline) + if self._released: + raise EnforcementUnavailable("Owned launch barrier was already released") + identity = _identity_snapshot(record.identity) + expected_policy = _policy_snapshot(policy) + adapter = self._adapter + resources = _resource_snapshot(record) + _validate_claim(claim, pid=identity.main_pid, birth_time=identity.birth_time, generation=identity.generation) + expected_claim = _canonical(claim) + _deadline(deadline) + _validate_evidence(adapter.inspect(record, deadline), identity) + _validate_handle(self) + _same_snapshot(record, policy, identity, expected_policy, resources) + if self.record is not record or self.policy is not policy or self._released: + raise EnforcementUnavailable("Owned handle changed during claim inspection") + if self._adapter is not adapter: + raise EnforcementUnavailable("Retained adapter changed during claim inspection") + _validate_claim(claim, pid=identity.main_pid, birth_time=identity.birth_time, generation=identity.generation) + if _canonical(claim) != expected_claim: + raise EnforcementUnavailable("Startup claim changed during inspection") + _deadline(deadline) + adapter.barrier(record, identity, deadline) + _validate_handle(self) + _same_snapshot(record, policy, identity, expected_policy, resources) + if self.record is not record or self.policy is not policy or self._adapter is not adapter: + raise EnforcementUnavailable("Owned handle changed during barrier release") + _validate_claim(claim, pid=identity.main_pid, birth_time=identity.birth_time, generation=identity.generation) + if _canonical(claim) != expected_claim: + raise EnforcementUnavailable("Startup claim changed during release") + _deadline(deadline) + self._released = True + + +def _validate_handle(handle: object) -> tuple[LinuxProvisionalLaunch, LinuxDaemonLaunchPolicy]: + if type(handle) is not OwnedLinuxLaunch: + raise TypeError("Expected exact OwnedLinuxLaunch") + record, policy = _validate_record(handle.record), _validate_policy(handle.policy) + _literal(handle.pid, record.identity.main_pid, "owned MainPID") + _literal(record.identity.cap_bytes, policy.cap_bytes, "owned cap") + _literal(record.identity.swap_bytes, policy.swap_bytes, "owned swap") + if type(handle._released) is not bool: + raise ValueError("Malformed release state") + if handle._adapter is None: + raise ValueError("Missing retained adapter") + return record, policy + + +def _identity_snapshot(identity: LinuxInvocationIdentity) -> LinuxInvocationIdentity: + _validate_identity(identity) + return LinuxInvocationIdentity(**asdict(identity)) + + +def _policy_snapshot(policy: LinuxDaemonLaunchPolicy) -> LinuxDaemonLaunchPolicy: + _validate_policy(policy) + return LinuxDaemonLaunchPolicy(**asdict(policy)) + + +def _resource_snapshot(record: LinuxProvisionalLaunch) -> tuple[object, ...]: + _validate_record(record) + return (record.runner, record.pidfd, record.scope_fd, record.endpoint) + + +def _same_snapshot(record: LinuxProvisionalLaunch, policy: LinuxDaemonLaunchPolicy, + identity: LinuxInvocationIdentity, expected_policy: LinuxDaemonLaunchPolicy, + resources: tuple[object, ...]) -> None: + _validate_record(record) + _validate_policy(policy) + if record.identity != identity or policy != expected_policy: + raise EnforcementUnavailable("Owned policy or invocation changed during a callback") + if any(actual is not expected for actual, expected in zip(_resource_snapshot(record), resources)): + raise EnforcementUnavailable("Strong owned resources changed during a callback") + + +def launch_owned_linux(policy: LinuxDaemonLaunchPolicy, *, generation: str, python_executable: str, deadline: float) -> OwnedLinuxLaunch: + _validate_policy(policy) + expected_policy = _policy_snapshot(policy) + _hex(generation, 32, "generation") + _text(python_executable, "python_executable", bound=4096) + if not Path(python_executable).is_absolute(): + raise ValueError("python_executable must be absolute") + _deadline(deadline) + adapter = get_linux_adapter() + _deadline(deadline) + record = _validate_record(adapter.start(policy, generation, python_executable, deadline)) + _literal(record.identity.generation, generation, "returned generation") + _literal(record.identity.cap_bytes, policy.cap_bytes, "returned cap") + _literal(record.identity.swap_bytes, policy.swap_bytes, "returned swap") + identity = _identity_snapshot(record.identity) + resources = _resource_snapshot(record) + _same_snapshot(record, policy, identity, expected_policy, resources) + _validate_policy(policy) + _deadline(deadline) + _validate_evidence(adapter.inspect(record, deadline), identity) + _same_snapshot(record, policy, identity, expected_policy, resources) + _deadline(deadline) + return OwnedLinuxLaunch(record, policy, adapter) + + +def child_gate(identity: LinuxInvocationIdentity, *, deadline: float) -> None: + _validate_identity(identity) + expected = _identity_snapshot(identity) + _deadline(deadline) + adapter = get_linux_adapter() + _validate_evidence(adapter.inspect(identity, deadline), expected) + _validate_identity(identity) + if identity != expected: + raise EnforcementUnavailable("Child identity changed during inspection") + _deadline(deadline) + + +def capture_owned_exit(handle: OwnedLinuxLaunch, *, deadline: float) -> LinuxExitReceipt: + record, policy = _validate_handle(handle) + identity, expected_policy, adapter = _identity_snapshot(record.identity), _policy_snapshot(policy), handle._adapter + resources = _resource_snapshot(record) + _deadline(deadline) + evidence = _validate_evidence(adapter.inspect(record, deadline), identity, terminal=True) + if evidence.members: + raise EnforcementUnavailable("Terminal capture requires an actually empty owned domain") + _validate_handle(handle) + _same_snapshot(record, policy, identity, expected_policy, resources) + if handle.record is not record or handle.policy is not policy or handle._adapter is not adapter: + raise EnforcementUnavailable("Owned handle changed during terminal inspection") + _deadline(deadline) + receipt = _validate_receipt(adapter.capture(record, identity, deadline), identity) + _validate_handle(handle) + _same_snapshot(record, policy, identity, expected_policy, resources) + if handle.record is not record or handle.policy is not policy or handle._adapter is not adapter: + raise EnforcementUnavailable("Retained adapter changed during capture") + _deadline(deadline) + return receipt + + +def stop_owned_exact(handle: OwnedLinuxLaunch, *, deadline: float) -> LinuxExitReceipt: + record, policy = _validate_handle(handle) + identity, expected_policy, adapter = _identity_snapshot(record.identity), _policy_snapshot(policy), handle._adapter + resources = _resource_snapshot(record) + _deadline(deadline) + receipt = capture_owned_exit(handle, deadline=deadline) + evidence = _validate_evidence(adapter.inspect(record, deadline), identity, terminal=True) + if evidence.members: + raise EnforcementUnavailable("Owned stop requires an actually empty owned domain") + _validate_receipt(receipt, identity) + _validate_handle(handle) + _same_snapshot(record, policy, identity, expected_policy, resources) + if handle.record is not record or handle.policy is not policy or handle._adapter is not adapter: + raise EnforcementUnavailable("Retained adapter changed before stop") + _deadline(deadline) + adapter.stop(record, identity, deadline) + return receipt + + +_termination_context = threading.local() + + +def _termination_deadline(deadline: float, entry: float) -> float: + if type(deadline) is not float or not math.isfinite(deadline): + raise ValueError("Termination deadline must be a finite exact float") + if deadline <= entry: + raise TimeoutError("Owned termination deadline expired") + return min(deadline, entry + 5.0) + + +def _termination_owner_guard(record: LinuxProvisionalLaunch) -> None: + context = getattr(_termination_context, "owner", None) + if context is None: + return + handle, expected_record, policy, identity, expected_policy, resources, adapter, released, store, directory, metadata, path = context + _validate_handle(handle) + _same_snapshot(record, policy, identity, expected_policy, resources) + if record is not expected_record or handle.record is not record or handle.policy is not policy or handle._adapter is not adapter or handle._released is not released or record.runner.store is not store or type(store.directory) is not type(directory) or store.directory != directory or store.metadata is not metadata or store.path is not path: + raise EnforcementUnavailable("Public termination owner changed during a callback") + + +def _termination_emit(record: LinuxProvisionalLaunch, phase: str, payload: dict[str, object], deadline: float) -> None: + _deadline(deadline) + _validate_record(record) + _termination_owner_guard(record) + record.runner.store.verify() + _termination_owner_guard(record) + _deadline(deadline) + result = record.runner.store.append(phase, payload) + if type(result) is not tuple or len(result) != 2: + raise EnforcementUnavailable("Termination intent has no durable receipt") + _integer(result[0], "termination receipt sequence", positive=True) + _hex(result[1], 64, "termination receipt hash") + _termination_owner_guard(record) + _deadline(deadline) + + +def terminate_owned_exact(handle: OwnedLinuxLaunch, *, deadline: float) -> LinuxExitReceipt: + """Terminate only the retained incarnation, then capture and empty-stop it.""" + effective = _termination_deadline(deadline, time.monotonic()) + record, policy = _validate_handle(handle) + identity, expected_policy = _identity_snapshot(record.identity), _policy_snapshot(policy) + resources, adapter, released = _resource_snapshot(record), handle._adapter, handle._released + method = getattr(adapter, "terminate", None) + if not callable(method): + raise EnforcementUnavailable("Retained adapter cannot perform owned pidfd termination") + previous = getattr(_termination_context, "owner", None) + context = (handle, record, policy, identity, expected_policy, resources, adapter, released, + record.runner.store, record.runner.store.directory, record.runner.store.metadata, + record.runner.store.path) + _termination_context.owner = context + try: + _termination_emit(record, "termination-intent", {"version": 1, "generation": identity.generation, + "main_pid": identity.main_pid, "start_ticks": identity.start_ticks, + "birth_time": identity.birth_time, "original_deadline": deadline, + "effective_deadline": effective, "kernel_cap_bytes": identity.cap_bytes, + "swap_bytes": 0, "metric": _METRIC, "physical_hard_cap": False, + "dynamic_registry_cap": False}, effective) + current = _validate_evidence(adapter.inspect(record, effective), identity, terminal=True) + _termination_owner_guard(record) + _deadline(effective) + if current.members: + if method(record, identity, effective) is not None: + raise EnforcementUnavailable("Owned termination returned an unsupported result") + _termination_owner_guard(record) + _deadline(effective) + terminal = _validate_evidence(adapter.inspect(record, effective), identity, terminal=True) + _termination_owner_guard(record) + if terminal.members: + raise EnforcementUnavailable("Owned termination did not observe an empty terminal domain") + _deadline(effective) + result = stop_owned_exact(handle, deadline=effective) + _termination_owner_guard(record) + _deadline(effective) + return _validate_receipt(result, identity) + except (OSError, RuntimeError, ValueError, TypeError, TimeoutError) as error: + try: + _termination_emit(record, "termination-unresolved", {"generation": identity.generation, + "error_type": type(error).__name__}, effective) + except (OSError, RuntimeError, ValueError, TypeError, TimeoutError): + pass + raise + finally: + _termination_context.owner = previous + + +def _termination_resources(record: LinuxProvisionalLaunch) -> tuple[tuple[object, ...], tuple[object, ...], bytes]: + _validate_record(record) + if type(record.runner) is not _NativeRunner or type(record.scope_fd) is not _NativeScope or type(record.endpoint) is not _NativeEndpoint: + raise EnforcementUnavailable("Native termination resources changed") + runner, scope, endpoint = record.runner, record.scope_fd, record.endpoint + if type(runner.stop_complete) is not bool or runner.stop_complete: + raise EnforcementUnavailable("Owned manager lifetime already stopped") + for value in (record.pidfd, scope.slice_fd, scope.service_fd, scope.pidfd_device, scope.pidfd_inode): + _integer(value, "owned termination descriptor or pin") + objects = (runner, scope, endpoint, runner.store, runner.store.metadata, runner.store.path, + runner.process, runner.manifest, runner.forwarded, + endpoint.listener, endpoint.connection, endpoint.hello) + values = (record.pidfd, scope.slice_fd, scope.service_fd, scope.mount_id, scope.mount_point, + scope.pidfd_device, scope.pidfd_inode, endpoint.listener.fileno(), endpoint.connection.fileno(), + endpoint.listener_device, endpoint.listener_inode, endpoint.connection_device, + endpoint.connection_inode, endpoint.peer_pid, endpoint.peer_uid, runner.store.directory, + runner.store.metadata.st_dev, runner.store.metadata.st_ino, runner.store.metadata.st_uid) + return objects, values, _canonical(runner.manifest) + + +def _same_termination_resources(record: LinuxProvisionalLaunch, identity: LinuxInvocationIdentity, + expected: tuple[tuple[object, ...], tuple[object, ...], bytes]) -> None: + _validate_identity(identity) + _validate_record(record) + if record.identity != identity: + raise EnforcementUnavailable("Termination invocation changed") + objects, values, manifest = _termination_resources(record) + if any(actual is not saved for actual, saved in zip(objects, expected[0])) or len(objects) != len(expected[0]): + raise EnforcementUnavailable("Termination resource reference changed") + if len(values) != len(expected[1]) or any(type(actual) is not type(saved) or actual != saved for actual, saved in zip(values, expected[1])) or manifest != expected[2]: + raise EnforcementUnavailable("Termination resource pin or manifest changed") + _termination_owner_guard(record) + + +def _pidfd_pin(descriptor: int, device: int, inode: int) -> None: + _integer(descriptor, "pidfd") + _integer(device, "pidfd device") + _integer(inode, "pidfd inode", positive=True) + metadata = os.fstat(descriptor) + _integer(metadata.st_dev, "observed pidfd device") + _integer(metadata.st_ino, "observed pidfd inode", positive=True) + if (metadata.st_dev, metadata.st_ino) != (device, inode): + raise EnforcementUnavailable("Authenticated pidfd descriptor was replaced") + + +def _pidfd_exited(descriptor: int, device: int, inode: int, *, timeout: float = 0.0) -> bool: + _pidfd_pin(descriptor, device, inode) + ready = select.select([descriptor], [], [], timeout)[0] + _pidfd_pin(descriptor, device, inode) + if type(ready) is not list or any(type(value) is not int or value != descriptor for value in ready): + raise EnforcementUnavailable("Pidfd wait returned an unrelated descriptor") + return bool(ready) + + +def _pidfd_target(descriptor: int, target: int) -> bool: + _integer(descriptor, "pidfd") + _integer(target, "pidfd target", positive=True) + raw = _read_path(Path(f"/proc/self/fdinfo/{descriptor}"), bound=4096).decode("ascii") + values: dict[str, str] = {} + for line in raw.splitlines(): + name, separator, value = line.partition(":") + if not separator or name in values: + raise EnforcementUnavailable("Malformed pidfd descriptor information") + values[name] = value.strip() + if "Pid" not in values or "NSpid" not in values: + raise EnforcementUnavailable("Descriptor is not an authenticated pidfd") + if values["Pid"] == "-1": + if values["NSpid"] not in ("-1", ""): + raise EnforcementUnavailable("Exited pidfd has inconsistent namespace identity") + return False + if _number(values["Pid"], "pidfd target", positive=True) != target: + raise EnforcementUnavailable("Pidfd target differs from the admitted MainPID") + namespace = values["NSpid"].split() + if not namespace or len(namespace) > 16 or any(_number(value, "pidfd namespace PID", positive=True) <= 0 for value in namespace) or _number(namespace[0], "pidfd namespace target", positive=True) != target: + raise EnforcementUnavailable("Pidfd namespace target differs") + return True + + +def _signal_owned_pidfd(record: LinuxProvisionalLaunch, identity: LinuxInvocationIdentity, + resources: tuple[tuple[object, ...], tuple[object, ...], bytes], + requested_signal: int, deadline: float) -> bool: + """Observers run before this helper; final verification has no callbacks.""" + _deadline(deadline) + if type(requested_signal) not in (int, signal.Signals) or requested_signal not in (signal.SIGTERM, signal.SIGKILL): + raise ValueError("Only owned TERM or KILL signals are supported") + _same_termination_resources(record, identity, resources) + scope = record.scope_fd + owned_store = record.runner.store + original, device, inode = record.pidfd, scope.pidfd_device, scope.pidfd_inode + _pidfd_pin(original, device, inode) + if _pidfd_exited(original, device, inode): + return True + if not _pidfd_target(original, identity.main_pid): + if _pidfd_exited(original, device, inode): + return True + raise EnforcementUnavailable("Pidfd target disappeared without observed exit") + fcntl = importlib.import_module("fcntl") + if not hasattr(fcntl, "F_DUPFD_CLOEXEC") or not callable(getattr(signal, "pidfd_send_signal", None)): + raise EnforcementUnavailable("Atomic pidfd signaling is unavailable") + duplicate = fcntl.fcntl(original, fcntl.F_DUPFD_CLOEXEC, 3) + _integer(duplicate, "private pidfd") + if duplicate < 3 or duplicate == original: + raise EnforcementUnavailable("Atomic private pidfd duplication failed") + try: + _same_termination_resources(record, identity, resources) + _pidfd_pin(original, device, inode) + _pidfd_pin(duplicate, device, inode) + if not _pidfd_target(duplicate, identity.main_pid): + if _pidfd_exited(duplicate, device, inode): + return True + raise EnforcementUnavailable("Private pidfd has no live target or observed exit") + if _pidfd_exited(duplicate, device, inode): + return True + _same_termination_resources(record, identity, resources) + _deadline(deadline) + _pidfd_pin(original, device, inode) + _pidfd_pin(duplicate, device, inode) + try: + signal.pidfd_send_signal(duplicate, requested_signal, None, 0) + except ProcessLookupError: + if not _pidfd_exited(duplicate, device, inode): + raise EnforcementUnavailable("ESRCH without actual retained pidfd exit") + return True + return _pidfd_exited(duplicate, device, inode) + finally: + try: + _pidfd_pin(duplicate, device, inode) + os.close(duplicate) + except (OSError, RuntimeError, ValueError, TypeError) as error: + _unresolved_launches.append(("termination-private-fd", duplicate, device, inode, owned_store)) + try: + _same_termination_resources(record, identity, resources) + _termination_emit(record, "termination-private-close-failed", {"generation": identity.generation, + "descriptor": duplicate, "device": device, "inode": inode, + "error_type": type(error).__name__}, deadline) + except (OSError, RuntimeError, ValueError, TypeError, TimeoutError): + pass + raise + + +def _wait_pidfd_exit(record: LinuxProvisionalLaunch, identity: LinuxInvocationIdentity, + resources: tuple[tuple[object, ...], tuple[object, ...], bytes], deadline: float) -> bool: + _same_termination_resources(record, identity, resources) + scope = record.scope_fd + exited = _pidfd_exited(record.pidfd, scope.pidfd_device, scope.pidfd_inode, timeout=_left(deadline)) + _same_termination_resources(record, identity, resources) + return exited + + +def _termination_native_snapshot(adapter: LinuxAdapter, record: LinuxProvisionalLaunch, + identity: LinuxInvocationIdentity, + resources: tuple[tuple[object, ...], tuple[object, ...], bytes], + deadline: float) -> tuple[bool, bool]: + _deadline(deadline) + _same_termination_resources(record, identity, resources) + scope = record.scope_fd + record.runner.store.verify() + _same_termination_resources(record, identity, resources) + if sys.platform != "linux" or os.getuid() != identity.uid or _boot_id() != identity.boot_id or _mount() != (scope.mount_id, scope.mount_point): + raise EnforcementUnavailable("Termination native boot, UID or mount changed") + for descriptor, device, inode in ((scope.slice_fd, identity.slice_device, identity.slice_inode), + (scope.service_fd, identity.service_device, identity.service_inode)): + metadata = os.fstat(descriptor) + if (metadata.st_uid, metadata.st_dev, metadata.st_ino) != (identity.uid, device, inode): + raise EnforcementUnavailable("Termination controller identity changed") + _filesystem_magic(descriptor) + slice_props = _properties(identity.slice_unit, ("InvocationID", "ControlGroup", "MemoryMax", "MemorySwapMax", "ActiveState"), deadline) + service = _properties(identity.service_unit, ("InvocationID", "ControlGroup", "MainPID", "ExecMainPID", "ActiveState", "SubState"), deadline) + if slice_props["InvocationID"] != identity.slice_invocation_id or service["InvocationID"] != identity.service_invocation_id or slice_props["ControlGroup"] != identity.slice_relative_path or slice_props["ActiveState"] != "active": + raise EnforcementUnavailable("Termination manager invocation or persistent scope changed") + if _number(slice_props["MemoryMax"], "termination manager cap", positive=True) != identity.cap_bytes or _number(slice_props["MemorySwapMax"], "termination manager swap") != 0 or _number(_read_at(scope.slice_fd, "memory.max"), "termination kernel cap", positive=True) != identity.cap_bytes or _number(_read_at(scope.slice_fd, "memory.swap.max"), "termination kernel swap") != 0: + raise EnforcementUnavailable("Termination charged-domain ceiling changed") + _ancestor_limits(scope.mount_point, identity.slice_relative_path.rsplit("/", 1)[0], identity.cap_bytes) + census = _recursive_members(scope.slice_fd, identity.slice_relative_path) + members = tuple(pid for _, values in census for pid in values) + if members not in ((), (identity.main_pid,)) or _number(service["ExecMainPID"], "termination ExecMainPID", positive=True) != identity.main_pid: + raise EnforcementUnavailable("Termination census or retained workload changed") + main = _number(service["MainPID"], "termination MainPID") + exited = _pidfd_exited(record.pidfd, scope.pidfd_device, scope.pidfd_inode) + _same_termination_resources(record, identity, resources) + _deadline(deadline) + if not exited: + if main != identity.main_pid or members != (identity.main_pid,): + raise EnforcementUnavailable("Live termination workload is not the exact owned member") + _validate_evidence(adapter.inspect(record, deadline), identity) + _same_termination_resources(record, identity, resources) + _deadline(deadline) + return _pidfd_exited(record.pidfd, scope.pidfd_device, scope.pidfd_inode), False + if members or main not in (0, identity.main_pid): + raise EnforcementUnavailable("Exited pidfd has a foreign or unresolved member") + if main == 0 and service["SubState"] in ("exited", "failed", "dead"): + _validate_evidence(adapter.inspect(record, deadline), identity, terminal=True) + _same_termination_resources(record, identity, resources) + _deadline(deadline) + return True, True + if service["ControlGroup"] != identity.service_relative_path or service["SubState"] not in ("running", "stop-sigterm", "stop-sigkill", "stop-post", "final-sigterm", "final-sigkill"): + raise EnforcementUnavailable("Manager state is not an authenticated pending reap") + return True, False + + +def _wait_owned_terminal(adapter: LinuxAdapter, record: LinuxProvisionalLaunch, + identity: LinuxInvocationIdentity, + resources: tuple[tuple[object, ...], tuple[object, ...], bytes], deadline: float) -> None: + while True: + exited, reaped = _termination_native_snapshot(adapter, record, identity, resources, deadline) + if exited and reaped: + return + if not exited: + raise TimeoutError("Owned pidfd has not exited within bounded termination waits") + select.select([], [], [], min(0.05, _left(deadline))) + _same_termination_resources(record, identity, resources) + + +def _canonical(value: object) -> bytes: + return json.dumps(value, sort_keys=True, separators=(",", ":"), allow_nan=False).encode("utf-8") + + +def _strict_json(raw: bytes) -> object: + def pairs(values: list[tuple[str, object]]) -> dict[str, object]: + result = {} + for key, value in values: + if key in result: + raise ValueError("Duplicate JSON key") + result[key] = value + return result + return json.loads(raw, object_pairs_hook=pairs, + parse_constant=lambda _: (_ for _ in ()).throw(ValueError("Nonfinite JSON"))) + + +def _read_fd(descriptor: int, *, bound: int = _READ_BOUND) -> bytes: + metadata = os.fstat(descriptor) + if not stat.S_ISREG(metadata.st_mode): + raise EnforcementUnavailable("Expected a regular bounded evidence file") + result = bytearray() + while len(result) <= bound: + piece = os.read(descriptor, min(4096, bound + 1 - len(result))) + if not piece: + return bytes(result) + result.extend(piece) + raise EnforcementUnavailable("Readback exceeded its finite bound") + + +def _read_path(path: Path, *, bound: int = _READ_BOUND) -> bytes: + descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK | os.O_CLOEXEC) + try: + return _read_fd(descriptor, bound=bound) + finally: + os.close(descriptor) + + +def _read_at(directory: int, name: str) -> str: + descriptor = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK | os.O_CLOEXEC, dir_fd=directory) + try: + return _read_fd(descriptor).decode("ascii").strip() + finally: + os.close(descriptor) + + +def _number(text: str, name: str, *, positive: bool = False) -> int: + if type(text) is not str or not re.fullmatch(r"0|[1-9][0-9]*", text) or len(text) > 19: + raise EnforcementUnavailable(f"Noncanonical native {name}") + return _integer(int(text), name, positive=positive) + + +def _process_identity(pid: int) -> tuple[int, float]: + _integer(pid, "pid", positive=True) + raw = _read_path(Path(f"/proc/{pid}/stat"), bound=8192).decode("ascii") + prefix, opening, _ = raw.partition(" (") + closing = raw.rfind(")") + if not opening or _number(prefix, "proc stat pid", positive=True) != pid or closing < len(prefix) + 2 or raw[closing + 1:closing + 2] != " ": + raise EnforcementUnavailable("Malformed native process stat framing") + tail = raw[raw.rfind(")") + 2:].split() + if len(tail) < 20 or tail[0] not in tuple("RSDTtWXxKWPIN"): + raise EnforcementUnavailable("Workload process is absent or unreaped") + ticks = _number(tail[19], "start_ticks", positive=True) + lines = _read_path(Path("/proc/stat"), bound=1048576).decode("ascii").splitlines() + boot = [line.split()[1] for line in lines if line.startswith("btime ")] + if len(boot) != 1: + raise EnforcementUnavailable("Cannot verify process birth clock") + clock = os.sysconf("SC_CLK_TCK") + _integer(clock, "clock ticks", positive=True) + birth = float(_number(boot[0], "boot time", positive=True) + ticks / clock) + if not math.isfinite(birth) or birth <= 0: + raise EnforcementUnavailable("Invalid process birth candidate") + return ticks, birth + + +def _boot_id() -> str: + value = _read_path(Path("/proc/sys/kernel/random/boot_id"), bound=128).decode("ascii").strip() + if _UUID.fullmatch(value) is None: + raise EnforcementUnavailable("Malformed current boot identity") + return value + + +def _load_capsule() -> object: + path = Path(__file__).absolute().parent / "truememory" / "memory_bootstrap.py" + raw = _read_path(path, bound=1048576) + if hashlib.sha256(raw).hexdigest() != "a33c0f4bf25bfa590390f50296623455491333ace98da97e4a3511e6aee50c5d": + raise EnforcementUnavailable("Protected readback capsule changed") + name = "_truememory_owned_readback_capsule" + spec = importlib.util.spec_from_file_location(name, str(path)) + if spec is None or spec.loader is None: + raise EnforcementUnavailable("Absolute readback capsule is unavailable") + module = importlib.util.module_from_spec(spec) + sys.modules[name] = module + spec.loader.exec_module(module) + return module + + +class _StatFS(ctypes.Structure): + _fields_ = [("f_type", ctypes.c_long), ("f_bsize", ctypes.c_long), + ("f_blocks", ctypes.c_ulong), ("f_bfree", ctypes.c_ulong), + ("f_bavail", ctypes.c_ulong), ("f_files", ctypes.c_ulong), + ("f_ffree", ctypes.c_ulong), ("f_fsid", ctypes.c_int * 2), + ("f_namelen", ctypes.c_long), ("f_frsize", ctypes.c_long), + ("f_flags", ctypes.c_long), ("f_spare", ctypes.c_long * 4)] + + +def _filesystem_magic(descriptor: int) -> int: + if sys.platform != "linux" or ctypes.sizeof(ctypes.c_long) != 8 or os.uname().machine not in ("x86_64", "aarch64"): + raise EnforcementUnavailable("No validated Linux statfs ABI for this architecture") + function = ctypes.CDLL(None, use_errno=True).fstatfs + function.argtypes = (ctypes.c_int, ctypes.POINTER(_StatFS)) + function.restype = ctypes.c_int + result = _StatFS() + if function(descriptor, ctypes.byref(result)) != 0: + raise OSError(ctypes.get_errno(), "Owned cgroup fstatfs unavailable") + if result.f_type != _CGROUP2_MAGIC: + raise EnforcementUnavailable("Controller directory is not authentic cgroup2fs") + return result.f_type + + +def _unescape_mount(value: str) -> str: + escapes = {"040": " ", "011": "\t", "012": "\n", "134": "\\"} + if any(match.group(1) not in escapes for match in re.finditer(r"\\([^ ]{3})", value)): + raise EnforcementUnavailable("Unsupported mountinfo escape") + remainder = re.sub(r"\\(?:040|011|012|134)", "", value) + if "\\" in remainder: + raise EnforcementUnavailable("Malformed mountinfo escape") + return re.sub(r"\\(040|011|012|134)", lambda match: escapes[match.group(1)], value) + + +def _mount() -> tuple[int, str]: + raw = _read_path(Path("/proc/self/mountinfo"), bound=1048576).decode("utf-8") + candidates = [] + for line in raw.splitlines(): + before, separator, after = line.partition(" - ") + if not separator: + raise EnforcementUnavailable("Malformed mountinfo") + fields, kind = before.split(), after.split() + if len(fields) < 6 or len(kind) < 3: + raise EnforcementUnavailable("Malformed mount record") + if kind[0] == "cgroup2": + if _unescape_mount(fields[3]) != "/": + raise EnforcementUnavailable("Unsupported delegated cgroup namespace root") + point = _unescape_mount(fields[4]) + if not Path(point).is_absolute() or "\x00" in point: + raise EnforcementUnavailable("Invalid cgroup mount point") + candidates.append((_number(fields[0], "mount id", positive=True), point)) + if len(candidates) != 1: + raise EnforcementUnavailable("Exactly one authenticated cgroup2 mount is required") + return candidates[0] + + +def _membership(pid: int) -> str: + raw = _read_path(Path(f"/proc/{pid}/cgroup"), bound=_READ_BOUND).decode("ascii").splitlines() + values = [line[3:] for line in raw if line.startswith("0::")] + if len(values) != 1: + raise EnforcementUnavailable("Missing unified cgroup membership") + return _relative(values[0], "actual membership") + + +def _open_directory(path: Path, *, uid: int | None = None) -> int: + if not path.is_absolute(): + raise EnforcementUnavailable("Controller path must be absolute") + descriptor = os.open("/", os.O_RDONLY | os.O_DIRECTORY | os.O_CLOEXEC) + try: + for part in path.parts[1:]: + if part in ("", ".", ".."): + raise EnforcementUnavailable("Noncanonical native directory") + child = os.open(part, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC, dir_fd=descriptor) + os.close(descriptor) + descriptor = child + metadata = os.fstat(descriptor) + if uid is not None and metadata.st_uid != uid: + raise EnforcementUnavailable("Owned directory UID changed") + return descriptor + except (OSError, ValueError, RuntimeError): + os.close(descriptor) + raise + + +def _controller_events(descriptor: int, name: str = "memory.events") -> tuple[tuple[str, int], ...]: + values = [] + for line in _read_at(descriptor, name).splitlines(): + fields = line.split() + if len(fields) != 2: + raise EnforcementUnavailable("Malformed controller events") + values.append((fields[0], _number(fields[1], "controller event"))) + return _events(tuple(values), "native memory events") + + +def _members(descriptor: int) -> tuple[int, ...]: + values = tuple(_number(line, "controller member", positive=True) for line in _read_at(descriptor, "cgroup.procs").splitlines()) + if len(values) > 4096 or len(set(values)) != len(values): + raise EnforcementUnavailable("Controller membership exceeds the ownership bound") + return values + + +def _recursive_members(descriptor: int, relative: str) -> tuple[tuple[str, tuple[int, ...]], ...]: + result = [] + pending = [(os.dup(descriptor), relative, 0)] + try: + while pending: + current, path, depth = pending.pop() + try: + if len(result) >= 128 or depth > 16: + raise EnforcementUnavailable("Owned recursive census exceeds its bound") + _filesystem_magic(current) + result.append((path, _members(current))) + with os.scandir(current) as entries: + for entry in entries: + if entry.is_symlink(): + raise EnforcementUnavailable("Symlink in owned cgroup census") + if entry.is_dir(follow_symlinks=False): + if len(pending) + len(result) >= 128: + raise EnforcementUnavailable("Owned recursive census exceeds its bound") + child = os.open(entry.name, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC, dir_fd=current) + pending.append((child, path + "/" + entry.name, depth + 1)) + finally: + os.close(current) + return tuple(sorted(result)) + finally: + for descriptor, _, _ in pending: + os.close(descriptor) + + +def _hash_path(path: Path, *, bound: int = 16777216) -> str: + descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK | os.O_CLOEXEC) + digest = hashlib.sha256() + total = 0 + try: + if not stat.S_ISREG(os.fstat(descriptor).st_mode): + raise EnforcementUnavailable("Artifact is not a regular file") + while True: + piece = os.read(descriptor, min(65536, bound + 1 - total)) + if not piece: + return digest.hexdigest() + total += len(piece) + if total > bound: + raise EnforcementUnavailable("Artifact exceeds retained hashing bound") + digest.update(piece) + finally: + os.close(descriptor) + + +def _exclusive_file(directory: int, name: str, raw: bytes) -> None: + if not re.fullmatch(r"[A-Za-z0-9_.-]+", name) or name in (".", ".."): + raise ValueError("Unsafe exclusive artifact name") + descriptor = os.open(name, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW | os.O_CLOEXEC, 0o600, dir_fd=directory) + try: + position = 0 + while position < len(raw): + written = os.write(descriptor, raw[position:]) + if written <= 0: + raise EnforcementUnavailable("Exclusive evidence write made no progress") + position += written + os.fsync(descriptor) + finally: + os.close(descriptor) + os.fsync(directory) + + +def _executable_pin(executable: str) -> dict[str, object]: + _text(executable, "selected interpreter", bound=4096) + selected = Path(executable) + if not selected.is_absolute(): + raise EnforcementUnavailable("Selected interpreter is not absolute") + resolved = selected.resolve(strict=True) + metadata = os.stat(selected) + target = os.stat(resolved, follow_symlinks=False) + if not stat.S_ISREG(target.st_mode) or (metadata.st_dev, metadata.st_ino) != (target.st_dev, target.st_ino) or not os.access(selected, os.X_OK): + raise EnforcementUnavailable("Selected interpreter target is not a stable executable") + digest = _hash_path(resolved, bound=67108864) + after = os.stat(selected) + if (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns, after.st_ctime_ns) != (metadata.st_dev, metadata.st_ino, metadata.st_size, metadata.st_mtime_ns, metadata.st_ctime_ns): + raise EnforcementUnavailable("Selected interpreter changed during readback") + return {"path": executable, "resolved_path": str(resolved), "device": target.st_dev, + "inode": target.st_ino, "sha256": digest} + + +def _private_artifact(directory: int, name: str, *, durable: bool = False, + bound: int = _READ_BOUND) -> tuple[bytes, str]: + if type(name) is not str or not re.fullmatch(r"[A-Za-z0-9_.-]+", name) or name in (".", ".."): + raise ValueError("Unsafe private artifact name") + descriptor = os.open(name, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK | os.O_CLOEXEC, dir_fd=directory) + try: + before = os.fstat(descriptor) + if not stat.S_ISREG(before.st_mode) or before.st_uid != os.getuid() or stat.S_IMODE(before.st_mode) != 0o600: + raise EnforcementUnavailable("Retained artifact is not a private regular file") + raw = _read_fd(descriptor, bound=bound) + if durable: + os.fsync(descriptor) + after = os.fstat(descriptor) + current = os.stat(name, dir_fd=directory, follow_symlinks=False) + fields = ("st_dev", "st_ino", "st_uid", "st_mode", "st_size", "st_mtime_ns", "st_ctime_ns") + if any(getattr(before, field) != getattr(after, field) or getattr(after, field) != getattr(current, field) for field in fields): + raise EnforcementUnavailable("Retained artifact changed during readback") + if durable: + os.fsync(directory) + return raw, hashlib.sha256(raw).hexdigest() + finally: + os.close(descriptor) + + +def _barrier_pin(value: object) -> dict[str, int]: + if type(value) is not dict or any(type(key) is not str for key in value) or set(value) != {"descriptor", "device", "inode"}: + raise EnforcementUnavailable("Malformed retained barrier pin") + for name in value: + _integer(value[name], "barrier " + name, positive=True) + if value["descriptor"] < 3 or value["descriptor"] > 2147483647: + raise EnforcementUnavailable("Retained barrier descriptor is out of range") + return value + + +def _preexec_payload(identity: LinuxInvocationIdentity, identity_sha: str, barrier: object) -> dict[str, object]: + _validate_identity(identity) + _hex(identity_sha, 64, "retained identity hash") + return {"version": 1, "generation": identity.generation, "manifest_sha256": identity.manifest_sha256, + "pid": identity.main_pid, "start_ticks": identity.start_ticks, "identity_sha256": identity_sha, + "bootstrap_sha256": identity.bootstrap_sha256, "kernel_cap_bytes": identity.cap_bytes, + "metric": _METRIC, "physical_hard_cap": False, "dynamic_registry_cap": False, + "barrier": _barrier_pin(barrier)} + + +def _receipt_chain(directory: int, *, expected: dict[str, str] | None = None) -> tuple[int, str, list[dict[str, object]]]: + names = [] + with os.scandir(directory) as entries: + for entry in entries: + if len(names) >= 4120: + raise EnforcementUnavailable("Retained generation exceeds its file bound") + names.append(entry.name) + receipts = sorted(name for name in names if name.startswith("receipt-")) + if len(receipts) > 4096 or any(re.fullmatch(r"receipt-[0-9]{6}\.json", name) is None for name in receipts): + raise EnforcementUnavailable("Malformed retained receipt sequence") + chain = "0" * 64 + payloads = [] + for sequence, name in enumerate(receipts, 1): + if name != f"receipt-{sequence:06d}.json": + raise EnforcementUnavailable("Retained receipt sequence has a gap") + raw, digest = _private_artifact(directory, name) + if expected is not None and expected.get(name) != digest: + raise EnforcementUnavailable("Retained receipt hash changed") + item = _strict_json(raw) + if type(item) is not dict or set(item) != {"version", "sequence", "previous_sha256", "phase", "payload", "monotonic"}: + raise EnforcementUnavailable("Malformed retained receipt") + _literal(item["version"], 1, "receipt version") + _literal(item["sequence"], sequence, "receipt sequence") + _literal(item["previous_sha256"], chain, "receipt previous hash") + _text(item["phase"], "receipt phase") + if type(item["monotonic"]) is not float or not math.isfinite(item["monotonic"]) or item["monotonic"] <= 0: + raise EnforcementUnavailable("Invalid retained receipt clock") + chain = digest + payloads.append(item) + return len(receipts), chain, payloads + + +class _EvidenceStore: + def __init__(self, path: Path, manifest: dict[str, object]) -> None: + self.path = path + self.directory = _open_directory(path, uid=os.getuid()) + self.metadata = os.fstat(self.directory) + self.lock = threading.RLock() + self.sequence = 0 + self.chain = "0" * 64 + self.hashes: dict[str, str] = {} + self.manifest_raw = _canonical(manifest) + self.manifest_sha256 = hashlib.sha256(self.manifest_raw).hexdigest() + _exclusive_file(self.directory, "manifest-v1.json", self.manifest_raw) + self.hashes["manifest-v1.json"] = self.manifest_sha256 + + def append(self, phase: str, payload: object) -> tuple[int, str]: + with self.lock: + if self.sequence >= 4096: + raise EnforcementUnavailable("Retained receipt sequence exceeds its bound") + sequence = self.sequence + 1 + raw = _canonical({"version": 1, "sequence": sequence, "previous_sha256": self.chain, + "phase": phase, "payload": payload, "monotonic": time.monotonic()}) + name = f"receipt-{sequence:06d}.json" + _exclusive_file(self.directory, name, raw) + self.sequence = sequence + self.chain = hashlib.sha256(raw).hexdigest() + self.hashes[name] = self.chain + return sequence, self.chain + + def retain(self, name: str, payload: object) -> None: + raw = _canonical(payload) + _exclusive_file(self.directory, name, raw) + self.hashes[name] = hashlib.sha256(raw).hexdigest() + + def observe(self, name: str, *, durable: bool = False, bound: int = _READ_BOUND) -> None: + with self.lock: + _, digest = _private_artifact(self.directory, name, durable=durable, bound=bound) + if name in self.hashes and self.hashes[name] != digest: + raise EnforcementUnavailable("Retained immutable artifact changed") + self.hashes[name] = digest + + def verify(self) -> None: + with self.lock: + current = os.stat(self.path, follow_symlinks=False) + metadata = os.fstat(self.directory) + if (current.st_dev, current.st_ino, metadata.st_dev, metadata.st_ino, metadata.st_uid, + stat.S_IMODE(metadata.st_mode)) != (self.metadata.st_dev, self.metadata.st_ino, + self.metadata.st_dev, self.metadata.st_ino, os.getuid(), 0o700): + raise EnforcementUnavailable("Retained generation directory changed") + sequence, chain, _ = _receipt_chain(self.directory, expected=self.hashes) + if (sequence, chain) != (self.sequence, self.chain): + raise EnforcementUnavailable("Retained receipt chain differs from owned lifetime") + for name, expected in self.hashes.items(): + _, digest = _private_artifact(self.directory, name, bound=16777216 if name == "stderr-v1.log" else _READ_BOUND) + if digest != expected: + raise EnforcementUnavailable("Retained owned artifact changed") + os.fsync(self.directory) + + +def _run_command(arguments: list[str], deadline: float, *, environment: dict[str, str] | None = None) -> tuple[subprocess.Popen[bytes], bytes]: + _deadline(deadline) + environment = _manager_environment(environment) + process = subprocess.Popen(arguments, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, + stderr=subprocess.PIPE, env=environment, close_fds=True) + output: dict[int, bytearray] = {} + streams = [] + try: + for stream in (process.stdout, process.stderr): + if stream is None: + raise EnforcementUnavailable("Manager command lacks bounded output handles") + os.set_blocking(stream.fileno(), False) + streams.append(stream) + output[stream.fileno()] = bytearray() + active = list(output) + while active: + ready, _, _ = select.select(active, [], [], _left(deadline)) + if not ready: + raise TimeoutError("Manager command deadline expired") + for descriptor in ready: + piece = os.read(descriptor, 4096) + if not piece: + active.remove(descriptor) + else: + output[descriptor].extend(piece) + if sum(map(len, output.values())) > _READ_BOUND: + raise EnforcementUnavailable("Manager property output exceeds its bound") + status = process.wait(timeout=_left(deadline)) + if status != 0: + raise EnforcementUnavailable(f"Owned manager command failed with status {status}") + return process, bytes(output[process.stdout.fileno()]) + except (OSError, RuntimeError, ValueError, TimeoutError, subprocess.TimeoutExpired): + if process.poll() is None: + try: + process.kill() + except ProcessLookupError: + pass + try: + process.wait(timeout=max(0.0, deadline - time.monotonic())) + except subprocess.TimeoutExpired: + _unresolved_launches.append(process) + raise + finally: + for stream in streams: + stream.close() + + +def _properties(unit: str, names: tuple[str, ...], deadline: float) -> dict[str, str]: + _, raw = _run_command(["/usr/bin/systemctl", "--user", "show", unit, "--no-pager", + "--property=" + ",".join(names)], deadline) + result = {} + for line in raw.decode("utf-8").splitlines(): + name, separator, value = line.partition("=") + if not separator or name not in names or name in result: + raise EnforcementUnavailable("Malformed bounded manager properties") + result[name] = value + if set(result) != set(names): + raise EnforcementUnavailable("Manager omitted required properties") + return result + + +def _packet_send(endpoint: socket.socket, value: object, deadline: float) -> None: + raw = _canonical(value) + if len(raw) + 4 > _READ_BOUND: + raise EnforcementUnavailable("Barrier packet exceeds its bound") + endpoint.settimeout(_left(deadline)) + endpoint.sendall(struct.pack(">I", len(raw)) + raw) + + +def _packet_receive(endpoint: socket.socket, deadline: float) -> dict[str, object]: + def receive(length: int) -> bytes: + value = bytearray() + while len(value) < length: + endpoint.settimeout(_left(deadline)) + piece = endpoint.recv(min(4096, length - len(value))) + if not piece: + raise EnforcementUnavailable("Owned barrier peer closed before completion") + value.extend(piece) + return bytes(value) + length = struct.unpack(">I", receive(4))[0] + if length == 0 or length + 4 > _READ_BOUND: + raise EnforcementUnavailable("Invalid barrier packet length") + value = _strict_json(receive(length)) + if type(value) is not dict: + raise EnforcementUnavailable("Barrier packet must be one object") + return value + + +def _peer(endpoint: socket.socket) -> tuple[int, int]: + pid, uid, _ = struct.unpack("3i", endpoint.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, struct.calcsize("3i"))) + _integer(pid, "peer PID", positive=True) + _integer(uid, "peer UID") + return pid, uid + + +@dataclass +class _NativeScope: + mount_id: int + mount_point: str + slice_fd: int + service_fd: int + baseline: tuple[tuple[str, int], ...] + baseline_local: tuple[tuple[str, int], ...] = () + leaf_baseline: tuple[tuple[str, int], ...] = () + leaf_baseline_local: tuple[tuple[str, int], ...] = () + pidfd_device: int = 0 + pidfd_inode: int = 0 + + +@dataclass +class _NativeRunner: + process: subprocess.Popen[bytes] + store: _EvidenceStore + manifest: dict[str, object] + stop_complete: bool = False + forwarded: Mapping[str, str] | None = None + + +@dataclass +class _NativeEndpoint: + listener: socket.socket + connection: socket.socket + peer_pid: int + peer_uid: int + hello: dict[str, object] + listener_device: int = 0 + listener_inode: int = 0 + connection_device: int = 0 + connection_inode: int = 0 + + +_unresolved_launches: list[object] = [] +_child_context: tuple[Path, LinuxInvocationIdentity, socket.socket, int, int] | None = None + + +def _original_environment(value: object, *, generation: str, names: list[str] | None = None) -> dict[str, str]: + if type(value) not in (dict, MappingProxyType) or not value or len(value) > 4096: + raise EnforcementUnavailable("Original environment snapshot is malformed or exceeds its name bound") + result = {} + for name, item in value.items(): + if type(name) is not str or re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name) is None or type(item) is not str or "\x00" in item: + raise EnforcementUnavailable("Original environment snapshot contains an invalid string") + result[name] = item + if names is not None and sorted(result) != names: + raise EnforcementUnavailable("Original environment snapshot names differ from the frozen manifest") + for name, expected in ((_GENERATION_ENV, generation), (_FLAG, "1"), ("TRUEMEMORY_CAP_CHILD", "1")): + _literal(result.get(name), expected, "original environment marker " + name) + return result + + +def _original_environment_packet(value: object, *, generation: str, manifest_sha256: str, + names: list[str] | None = None) -> dict[str, object]: + _hex(generation, 32, "environment generation") + _hex(manifest_sha256, 64, "environment manifest pin") + packet = {"generation": generation, "manifest_sha256": manifest_sha256, "preexec_witness_retained": True, + "forwarded_environment": _original_environment(value, generation=generation, names=names)} + if len(_canonical(packet)) + 4 > _READ_BOUND: + raise EnforcementUnavailable("Complete original environment frame exceeds its bound") + return packet + + +def _manager_environment(environment: dict[str, str] | None) -> dict[str, str]: + supplied = dict(os.environ if environment is None else environment) + runtime = f"/run/user/{os.getuid()}" + address = "unix:path=" + runtime + "/bus" + for name, expected in (("XDG_RUNTIME_DIR", runtime), ("DBUS_SESSION_BUS_ADDRESS", address)): + if name in supplied and supplied[name] != expected: + raise EnforcementUnavailable("Required local manager transport has an unsupported address") + supplied[name] = expected + return supplied + + +def _ancestor_limits(mount_point: str, relative: str, cap_bytes: int) -> tuple[tuple[str, str], ...]: + result = [] + parts = relative[1:].split("/") + if len(parts) > 32: + raise EnforcementUnavailable("Native ancestor chain exceeds its bound") + for count in range(len(parts), 0, -1): + path = "/" + "/".join(parts[:count]) + descriptor = _open_directory(Path(mount_point + path)) + try: + _filesystem_magic(descriptor) + maximum = _read_at(descriptor, "memory.max") + if maximum != "max" and _number(maximum, "ancestor memory.max") < cap_bytes: + raise EnforcementUnavailable("An existing stricter ancestor cap cannot be represented as selected C") + result.append((path, maximum)) + finally: + os.close(descriptor) + return tuple(result) + + +def _required_linux_support(policy: LinuxDaemonLaunchPolicy, deadline: float) -> tuple[int, str, str, tuple[tuple[str, str], ...]]: + _validate_policy(policy) + _deadline(deadline) + if sys.platform != "linux" or not hasattr(os, "pidfd_open") or not hasattr(socket, "SO_PEERCRED"): + raise EnforcementUnavailable("Required Linux ownership primitives are unavailable") + if ctypes.sizeof(ctypes.c_long) != 8 or os.uname().machine not in ("x86_64", "aarch64"): + raise EnforcementUnavailable("Required Linux filesystem ABI is unsupported") + _manager_environment(None) + runtime = Path(f"/run/user/{os.getuid()}") + runtime_fd = _open_directory(runtime, uid=os.getuid()) + try: + if stat.S_IMODE(os.fstat(runtime_fd).st_mode) != 0o700: + raise EnforcementUnavailable("User manager runtime directory is not private") + bus = os.stat("bus", dir_fd=runtime_fd, follow_symlinks=False) + if not stat.S_ISSOCK(bus.st_mode) or bus.st_uid != os.getuid(): + raise EnforcementUnavailable("User manager bus endpoint is not owned") + finally: + os.close(runtime_fd) + _load_capsule() + _, raw = _run_command(["/usr/bin/busctl", "--user", "get-property", "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", "org.freedesktop.systemd1.Manager", "Version"], deadline) + match = re.fullmatch(r's "([0-9]{3})([A-Za-z0-9._+~()-]*)"\n?', raw.decode("ascii")) + if match is None or int(match.group(1)) < 249: + raise EnforcementUnavailable("Required running user-manager version is unsupported") + parent = _properties("app.slice", ("LoadState", "ActiveState", "ControlGroup"), deadline) + if parent["LoadState"] != "loaded" or parent["ActiveState"] != "active": + raise EnforcementUnavailable("An existing active delegated app.slice is required") + parent_relative = _relative(parent["ControlGroup"], "existing app slice") + if not parent_relative.endswith("/app.slice"): + raise EnforcementUnavailable("Manager app.slice is not the admitted existing parent") + _membership(os.getpid()) + mount_id, mount_point = _mount() + parent_fd = _open_directory(Path(mount_point + parent_relative), uid=os.getuid()) + try: + _filesystem_magic(parent_fd) + if "memory" not in _read_at(parent_fd, "cgroup.controllers").split(): + raise EnforcementUnavailable("Existing app.slice has no memory controller") + finally: + os.close(parent_fd) + ancestors = _ancestor_limits(mount_point, parent_relative, policy.cap_bytes) + _deadline(deadline) + return mount_id, mount_point, parent_relative, ancestors + + +class LinuxAdapter: + """Actual user-manager adapter. Native support is established by readback.""" + + def inspect_existing(self, policy: LinuxDaemonLaunchPolicy, *, pid: int, birth_time: float, + generation: str, claim: dict[str, object], deadline: float) -> LinuxScopeEvidence: + _validate_policy(policy) + _integer(pid, "existing pid", positive=True) + _hex(generation, 32, "existing generation") + _validate_claim(claim, pid=pid, birth_time=birth_time, generation=generation) + _deadline(deadline) + if sys.platform != "linux": + raise EnforcementUnavailable("Readonly charged-domain inspection requires Linux") + path = Path.home() / ".truememory" / "memory-cap-launches" / generation + directory = _open_directory(path, uid=os.getuid()) + owned = [directory] + endpoint = None + try: + if stat.S_IMODE(os.fstat(directory).st_mode) != 0o700: + raise EnforcementUnavailable("Current generation directory is not private") + manifest_raw, manifest_sha = _private_artifact(directory, "manifest-v1.json") + identity_raw, identity_sha = _private_artifact(directory, "identity-v1.json") + manifest, data = _strict_json(manifest_raw), _strict_json(identity_raw) + if type(manifest) is not dict or type(data) is not dict: + raise EnforcementUnavailable("Current invocation metadata is malformed") + identity = LinuxInvocationIdentity(**data) + if (identity.main_pid, identity.birth_time, identity.generation, identity.uid, identity.boot_id) != (pid, birth_time, generation, os.getuid(), _boot_id()) or identity.cap_bytes > policy.cap_bytes: + raise EnforcementUnavailable("Current invocation identity or ceiling is incompatible") + _literal(identity.manifest_sha256, manifest_sha, "current manifest pin") + _literal(identity.bootstrap_sha256, _hash_path(Path(__file__).absolute(), bound=1048576), "current bootstrap pin") + for name, expected in (("version", 1), ("generation", generation), ("uid", identity.uid), ("boot_id", identity.boot_id), + ("slice_unit", identity.slice_unit), ("service_unit", identity.service_unit), + ("cap_bytes", identity.cap_bytes), ("swap_bytes", 0), ("bootstrap_sha256", identity.bootstrap_sha256)): + _literal(manifest.get(name), expected, "current manifest " + name) + expected_claim_path = Path.home() / ".truememory" / "model_server.start.json" + _literal(manifest.get("claim_path"), str(expected_claim_path), "current claim path") + claim_directory = _open_directory(expected_claim_path.parent, uid=identity.uid) + owned.append(claim_directory) + claim_raw, claim_sha = _private_artifact(claim_directory, expected_claim_path.name, bound=4096) + current_claim = _validate_claim(_strict_json(claim_raw), pid=pid, birth_time=birth_time, generation=generation) + if current_claim != claim or current_claim["launcher"] != {"pid": manifest.get("parent_pid"), "born": manifest.get("parent_birth")}: + raise EnforcementUnavailable("Current durable claim differs from the requested generation") + _, _, receipts = _receipt_chain(directory) + durable = [item["payload"] for item in receipts if item["phase"] == "claim-durable-before-release"] + if len(durable) != 1 or type(durable[0]) is not dict or durable[0].get("claim_sha256") != claim_sha or durable[0].get("generation") != generation or durable[0].get("pid") != pid or durable[0].get("birth_time") != birth_time: + raise EnforcementUnavailable("Current claim has no matching durable admission witness") + preexec_raw, _ = _private_artifact(directory, "pre-reexec-v1.json") + preexec_data = _strict_json(preexec_raw) + if type(preexec_data) is not dict: + raise EnforcementUnavailable("Current pre-exec witness is incompatible") + _literal_packet(preexec_data, _preexec_payload(identity, identity_sha, preexec_data.get("barrier")), "current pre-exec witness") + barrier = preexec_data["barrier"] + barrier_metadata = os.stat(f"/proc/{pid}/fd/{barrier['descriptor']}") + if (barrier_metadata.st_dev, barrier_metadata.st_ino, barrier_metadata.st_uid) != (barrier["device"], barrier["inode"], identity.uid) or not stat.S_ISSOCK(barrier_metadata.st_mode): + raise EnforcementUnavailable("Current inherited barrier descriptor changed") + ready_raw, _ = _private_artifact(directory, "ready-v1.json") + expected_ready = {"version": 1, "generation": generation, "manifest_sha256": manifest_sha, "pid": pid, + "kernel_cap_bytes": identity.cap_bytes, "metric": _METRIC, + "physical_hard_cap": False, "dynamic_registry_cap": False} + _literal_packet(_strict_json(ready_raw), expected_ready, "current application readiness") + actual_policy = LinuxDaemonLaunchPolicy(1, identity.cap_bytes, 0) + mount_id, mount_point, parent, _ = _required_linux_support(actual_policy, deadline) + if mount_id != identity.slice_mount_id or identity.slice_relative_path != parent + "/" + identity.slice_unit: + raise EnforcementUnavailable("Current charged-domain mount or parent changed") + descriptors = [] + for relative, device, inode in ((identity.slice_relative_path, identity.slice_device, identity.slice_inode), + (identity.service_relative_path, identity.service_device, identity.service_inode)): + descriptor = _open_directory(Path(mount_point + relative), uid=identity.uid) + owned.append(descriptor) + descriptors.append(descriptor) + metadata = os.fstat(descriptor) + _filesystem_magic(descriptor) + if (metadata.st_dev, metadata.st_ino) != (device, inode): + raise EnforcementUnavailable("Current controller inode changed") + slice_fd, service_fd = descriptors + slice_props = _properties(identity.slice_unit, ("InvocationID", "ControlGroup", "MemoryMax", "MemorySwapMax", "ActiveState"), deadline) + service_props = _properties(identity.service_unit, ("InvocationID", "ControlGroup", "MainPID", "ExecMainPID", "ActiveState"), deadline) + if slice_props["InvocationID"] != identity.slice_invocation_id or service_props["InvocationID"] != identity.service_invocation_id or slice_props["ControlGroup"] != identity.slice_relative_path or service_props["ControlGroup"] != identity.service_relative_path or slice_props["ActiveState"] != "active" or service_props["ActiveState"] != "active": + raise EnforcementUnavailable("Current manager invocation changed") + if _number(slice_props["MemoryMax"], "current manager cap", positive=True) != identity.cap_bytes or _number(slice_props["MemorySwapMax"], "current manager swap") != 0 or _number(_read_at(slice_fd, "memory.max"), "current kernel cap", positive=True) != identity.cap_bytes or _number(_read_at(slice_fd, "memory.swap.max"), "current kernel swap") != 0: + raise EnforcementUnavailable("Current charged ceiling changed") + if _number(service_props["MainPID"], "current MainPID", positive=True) != pid or _number(service_props["ExecMainPID"], "current ExecMainPID", positive=True) != pid: + raise EnforcementUnavailable("Current actual workload PID changed") + if _process_identity(pid) != (identity.start_ticks, birth_time) or _membership(pid) != identity.service_relative_path: + raise EnforcementUnavailable("Current workload incarnation or membership changed") + _verified_psutil_birth(pid, birth_time) + for namespace in ("mnt", "cgroup"): + own_namespace, workload_namespace = os.stat(f"/proc/self/ns/{namespace}"), os.stat(f"/proc/{pid}/ns/{namespace}") + if (own_namespace.st_dev, own_namespace.st_ino) != (workload_namespace.st_dev, workload_namespace.st_ino): + raise EnforcementUnavailable("Current workload namespace changed") + census = _recursive_members(slice_fd, identity.slice_relative_path) + members = tuple(member for _, values in census for member in values) + if members != (pid,): + raise EnforcementUnavailable("Current charged domain has missing or foreign members") + socket_path = expected_claim_path.parent / "model.sock" + socket_metadata = os.stat(socket_path, follow_symlinks=False) + if not stat.S_ISSOCK(socket_metadata.st_mode) or socket_metadata.st_uid != identity.uid or stat.S_IMODE(socket_metadata.st_mode) != 0o600: + raise EnforcementUnavailable("Current serving endpoint is not private") + endpoint = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + endpoint.settimeout(_left(deadline)) + endpoint.connect(str(socket_path)) + if _peer(endpoint) != (pid, identity.uid): + raise EnforcementUnavailable("Current serving peer is not the actual workload") + socket_after = os.stat(socket_path, follow_symlinks=False) + if (socket_after.st_dev, socket_after.st_ino, socket_after.st_uid, socket_after.st_mode) != (socket_metadata.st_dev, socket_metadata.st_ino, socket_metadata.st_uid, socket_metadata.st_mode): + raise EnforcementUnavailable("Serving endpoint changed during observation") + if _private_artifact(claim_directory, expected_claim_path.name, bound=4096)[0] != claim_raw or _process_identity(pid) != (identity.start_ticks, birth_time): + raise EnforcementUnavailable("Current claim or incarnation changed during observation") + if _properties(identity.slice_unit, ("InvocationID", "ControlGroup", "MemoryMax", "MemorySwapMax", "ActiveState"), deadline) != slice_props or _properties(identity.service_unit, ("InvocationID", "ControlGroup", "MainPID", "ExecMainPID", "ActiveState"), deadline) != service_props: + raise EnforcementUnavailable("Current manager identity changed during observation") + for relative, device, inode in ((identity.slice_relative_path, identity.slice_device, identity.slice_inode), + (identity.service_relative_path, identity.service_device, identity.service_inode)): + current_fd = _open_directory(Path(mount_point + relative), uid=identity.uid) + try: + current_meta = os.fstat(current_fd) + _filesystem_magic(current_fd) + if (current_meta.st_dev, current_meta.st_ino) != (device, inode): + raise EnforcementUnavailable("Current controller path changed during observation") + finally: + os.close(current_fd) + if tuple(member for _, values in _recursive_members(slice_fd, identity.slice_relative_path) for member in values) != (pid,): + raise EnforcementUnavailable("Current membership changed during observation") + result = LinuxScopeEvidence(identity, _CGROUP2_MAGIC, "cgroup2", True, True, True, identity.uid, pid, + identity.start_ticks, generation, manifest_sha, members, identity.cap_bytes, 0, + _read_at(service_fd, "memory.max"), _read_at(service_fd, "memory.swap.max"), True) + _deadline(deadline) + return result + finally: + if endpoint is not None: + endpoint.close() + for descriptor in reversed(owned): + os.close(descriptor) + + def start(self, policy: LinuxDaemonLaunchPolicy, generation: str, python_executable: str, deadline: float) -> LinuxProvisionalLaunch: + _validate_policy(policy) + _hex(generation, 32, "generation") + _deadline(deadline) + if sys.platform != "linux" or not hasattr(os, "pidfd_open") or not hasattr(socket, "SO_PEERCRED"): + raise EnforcementUnavailable("Required Linux ownership primitives are unavailable") + if not Path(python_executable).is_absolute() or not os.access(python_executable, os.X_OK): + raise EnforcementUnavailable("Owned interpreter must be an absolute executable") + mount_id, mount_point, parent_relative, ancestors = _required_linux_support(policy, deadline) + forwarded = {} + for name, value in os.environ.items(): + if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name) or type(value) is not str or "\x00" in value: + raise EnforcementUnavailable("Malformed application environment forwarding") + if name not in _INTERNAL_ENV: + forwarded[name] = value + forwarded[_GENERATION_ENV] = generation + forwarded[_FLAG] = "1" + forwarded["TRUEMEMORY_CAP_CHILD"] = "1" + _original_environment_packet(forwarded, generation=generation, manifest_sha256="0" * 64) + slice_unit = f"app-truememory{generation}.slice" + service_unit = f"truememory-memory{generation}.service" + evidence_root = Path.home() / ".truememory" / "memory-cap-launches" + evidence_root.mkdir(mode=0o700, parents=True, exist_ok=True) + root_fd = _open_directory(evidence_root, uid=os.getuid()) + try: + if stat.S_IMODE(os.fstat(root_fd).st_mode) != 0o700: + raise EnforcementUnavailable("Evidence root must already be owner-only") + os.mkdir(generation, mode=0o700, dir_fd=root_fd) + os.fsync(root_fd) + finally: + os.close(root_fd) + path = evidence_root / generation + bootstrap = Path(__file__).absolute() + bootstrap_sha = _hash_path(bootstrap, bound=1048576) + interpreter = _executable_pin(python_executable) + parent_ticks, parent_birth = _process_identity(os.getpid()) + manifest = {"version": 1, "generation": generation, "uid": os.getuid(), "boot_id": _boot_id(), + "slice_unit": slice_unit, "service_unit": service_unit, "cap_bytes": policy.cap_bytes, + "swap_bytes": 0, "bootstrap_sha256": bootstrap_sha, "deadline": deadline, + "parent_pid": os.getpid(), "parent_start_ticks": parent_ticks, "parent_birth": parent_birth, + "claim_path": str(Path.home() / ".truememory" / "model_server.start.json"), + "forwarded_names": sorted(forwarded), "interpreter": interpreter, + "ancestor_memory_max": ancestors} + store = _EvidenceStore(path, manifest) + _unresolved_launches.append(store) + listener = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + _unresolved_launches.append(listener) + listener.bind(str(path / "barrier-v1.sock")) + os.chmod(path / "barrier-v1.sock", 0o600) + listener.listen(1) + store.append("prepared", {"generation": generation, "kernel_cap_bytes": policy.cap_bytes, "swap_bytes": 0, + "metric": _METRIC, "physical_hard_cap": False, "dynamic_registry_cap": False}) + try: + _run_command(["/usr/bin/busctl", "--user", "call", "org.freedesktop.systemd1", + "/org/freedesktop/systemd1", "org.freedesktop.systemd1.Manager", "StartTransientUnit", + "ssa(sv)a(sa(sv))", slice_unit, "fail", "3", "MemoryAccounting", "b", "true", + "MemoryMax", "t", str(policy.cap_bytes), "MemorySwapMax", "t", "0", "0"], deadline) + slice_properties = _properties(slice_unit, ("LoadState", "ActiveState", "InvocationID", "ControlGroup", "MemoryMax", "MemorySwapMax"), deadline) + if slice_properties["LoadState"] != "loaded" or slice_properties["ActiveState"] != "active": + raise EnforcementUnavailable("Owned slice did not enter its persistent active state") + slice_relative = _relative(slice_properties["ControlGroup"], "slice path") + if slice_relative != parent_relative + "/" + slice_unit: + raise EnforcementUnavailable("Owned slice is not a direct child of admitted app.slice") + slice_fd = _open_directory(Path(mount_point + slice_relative), uid=os.getuid()) + _unresolved_launches.append(slice_fd) + _filesystem_magic(slice_fd) + if _number(_read_at(slice_fd, "memory.max"), "memory.max", positive=True) != policy.cap_bytes or _number(_read_at(slice_fd, "memory.swap.max"), "memory.swap.max") != 0: + raise EnforcementUnavailable("Slice kernel ceiling is not installed before exec") + baseline = _controller_events(slice_fd) + baseline_local = _controller_events(slice_fd, "memory.events.local") + if any(members for _, members in _recursive_members(slice_fd, slice_relative)): + raise EnforcementUnavailable("Fresh slice already contains a process") + store.append("preexec-slice", {"invocation_id": slice_properties["InvocationID"], "relative_path": slice_relative, + "events_hierarchical": baseline, "events_local": baseline_local, + "memory_current": _number(_read_at(slice_fd, "memory.current"), "memory.current")}) + stderr_fd = os.open("stderr-v1.log", os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW | os.O_CLOEXEC, 0o600, dir_fd=store.directory) + os.fsync(stderr_fd) + os.close(stderr_fd) + os.fsync(store.directory) + arguments = ["/usr/bin/systemd-run", "--user", "--unit=" + service_unit, "--slice=" + slice_unit, + "--service-type=exec", "--property=MemoryMax=infinity", "--property=MemorySwapMax=infinity", + "--property=RemainAfterExit=yes", "--property=StandardOutput=null", + "--property=StandardError=append:" + str(path / "stderr-v1.log"), + "--working-directory=" + os.getcwd()] + arguments.extend("--setenv=" + name for name in sorted(forwarded)) + arguments.extend([python_executable, "-I", "-S", str(bootstrap), "--child", str(path / "manifest-v1.json")]) + runner_process, _ = _run_command(arguments, deadline, environment=forwarded) + runner = _NativeRunner(runner_process, store, manifest, forwarded=MappingProxyType(dict(forwarded))) + _unresolved_launches.append(runner) + listener.settimeout(_left(deadline)) + connection, _ = listener.accept() + _unresolved_launches.append(connection) + peer_pid, peer_uid = _peer(connection) + if peer_uid != os.getuid(): + raise EnforcementUnavailable("Barrier workload UID differs") + hello = _packet_receive(connection, deadline) + service = _properties(service_unit, ("LoadState", "ActiveState", "InvocationID", "ControlGroup", "MainPID", "ExecMainPID"), deadline) + main_pid = _number(service["MainPID"], "actual MainPID", positive=True) + if peer_pid != main_pid or _number(service["ExecMainPID"], "ExecMainPID", positive=True) != main_pid: + raise EnforcementUnavailable("Manager runner is not the authenticated workload MainPID") + ticks, birth = _process_identity(main_pid) + service_relative = _relative(service["ControlGroup"], "service path") + service_fd = _open_directory(Path(mount_point + service_relative), uid=os.getuid()) + _unresolved_launches.append(service_fd) + _filesystem_magic(service_fd) + slice_meta, service_meta = os.fstat(slice_fd), os.fstat(service_fd) + identity = LinuxInvocationIdentity(1, generation, manifest["boot_id"], os.getuid(), slice_unit, + slice_properties["InvocationID"], mount_id, slice_meta.st_dev, slice_meta.st_ino, slice_relative, + service_unit, service["InvocationID"], service_meta.st_dev, service_meta.st_ino, service_relative, + main_pid, ticks, birth, policy.cap_bytes, 0, bootstrap_sha, store.manifest_sha256) + listener_meta, connection_meta = os.fstat(listener.fileno()), os.fstat(connection.fileno()) + endpoint = _NativeEndpoint(listener, connection, peer_pid, peer_uid, hello, + listener_meta.st_dev, listener_meta.st_ino, + connection_meta.st_dev, connection_meta.st_ino) + pidfd = os.pidfd_open(main_pid) + _unresolved_launches.append(pidfd) + pidfd_meta = os.fstat(pidfd) + scope = _NativeScope(mount_id, mount_point, slice_fd, service_fd, baseline, baseline_local, + _controller_events(service_fd), _controller_events(service_fd, "memory.events.local"), + pidfd_meta.st_dev, pidfd_meta.st_ino) + record = LinuxProvisionalLaunch(identity, runner, pidfd, scope, endpoint) + _unresolved_launches.append(record) + store.retain("identity-v1.json", asdict(identity)) + store.append("started-at-barrier", {"identity": asdict(identity)}) + self.inspect(record, deadline) + return record + except (OSError, RuntimeError, ValueError, TypeError, TimeoutError) as error: + store.append("startup-unresolved", {"error_type": type(error).__name__, "generation": generation}) + raise + + def inspect(self, record: LinuxProvisionalLaunch | LinuxInvocationIdentity, deadline: float) -> LinuxScopeEvidence: + _deadline(deadline) + child = type(record) is LinuxInvocationIdentity + identity = _validate_identity(record) if child else _validate_record(record).identity + if sys.platform != "linux" or _boot_id() != identity.boot_id or os.getuid() != identity.uid: + raise EnforcementUnavailable("Boot or UID ownership changed") + mount_id, mount_point = _mount() + if mount_id != identity.slice_mount_id: + raise EnforcementUnavailable("Cgroup mount identity changed") + owned = [] + try: + if child: + if _child_context is None or _child_context[1] != identity or _child_context[3] <= 0 or _child_context[4] != identity.uid: + raise EnforcementUnavailable("Child has no authenticated parent barrier") + slice_fd = _open_directory(Path(mount_point + identity.slice_relative_path), uid=identity.uid) + owned.append(slice_fd) + service_fd = _open_directory(Path(mount_point + identity.service_relative_path), uid=identity.uid) + owned.append(service_fd) + else: + if type(record.runner) is not _NativeRunner or type(record.scope_fd) is not _NativeScope or type(record.endpoint) is not _NativeEndpoint or type(record.pidfd) is not int: + raise EnforcementUnavailable("Native launch resources changed") + slice_fd, service_fd = record.scope_fd.slice_fd, record.scope_fd.service_fd + if record.runner.stop_complete: + raise EnforcementUnavailable("Owned manager lifetime already stopped") + pidfd_meta = os.fstat(record.pidfd) + if (pidfd_meta.st_dev, pidfd_meta.st_ino) != (record.scope_fd.pidfd_device, record.scope_fd.pidfd_inode): + raise EnforcementUnavailable("Strong MainPID pidfd was replaced") + for descriptor, device, inode in ((slice_fd, identity.slice_device, identity.slice_inode), (service_fd, identity.service_device, identity.service_inode)): + metadata = os.fstat(descriptor) + if (metadata.st_uid, metadata.st_dev, metadata.st_ino) != (identity.uid, device, inode): + raise EnforcementUnavailable("Owned controller directory identity changed") + _filesystem_magic(descriptor) + current = _open_directory(Path(mount_point + identity.slice_relative_path), uid=identity.uid) + try: + metadata = os.fstat(current) + _filesystem_magic(current) + if (metadata.st_dev, metadata.st_ino) != (identity.slice_device, identity.slice_inode): + raise EnforcementUnavailable("Current owned slice path was replaced") + finally: + os.close(current) + slice_props = _properties(identity.slice_unit, ("InvocationID", "ControlGroup", "MemoryMax", "MemorySwapMax", "ActiveState"), deadline) + service_props = _properties(identity.service_unit, ("InvocationID", "ControlGroup", "MainPID", "ExecMainPID", "ActiveState", "SubState"), deadline) + if slice_props["InvocationID"] != identity.slice_invocation_id or service_props["InvocationID"] != identity.service_invocation_id: + raise EnforcementUnavailable("Manager InvocationID changed") + if slice_props["ControlGroup"] != identity.slice_relative_path or service_props["ControlGroup"] not in (identity.service_relative_path, "") or slice_props["ActiveState"] != "active": + raise EnforcementUnavailable("Persistent native scope changed") + if _number(slice_props["MemoryMax"], "manager MemoryMax", positive=True) != identity.cap_bytes or _number(slice_props["MemorySwapMax"], "manager MemorySwapMax") != 0: + raise EnforcementUnavailable("Manager ceiling drift") + if _number(_read_at(slice_fd, "memory.max"), "memory.max", positive=True) != identity.cap_bytes or _number(_read_at(slice_fd, "memory.swap.max"), "memory.swap.max") != 0: + raise EnforcementUnavailable("Kernel ceiling drift") + _ancestor_limits(mount_point, identity.slice_relative_path.rsplit("/", 1)[0], identity.cap_bytes) + census = _recursive_members(slice_fd, identity.slice_relative_path) + members = tuple(pid for _, values in census for pid in values) + if len(set(members)) != len(members) or members not in ((), (identity.main_pid,)): + raise EnforcementUnavailable("Owned slice contains an unrelated process") + main = _number(service_props["MainPID"], "MainPID") + if _number(service_props["ExecMainPID"], "ExecMainPID", positive=True) != identity.main_pid: + raise EnforcementUnavailable("Retained ExecMainPID differs from actual admitted workload") + if members: + if service_props["ControlGroup"] != identity.service_relative_path or main != identity.main_pid or _process_identity(main) != (identity.start_ticks, identity.birth_time) or _membership(main) != identity.service_relative_path: + raise EnforcementUnavailable("Workload process incarnation or membership changed") + for namespace in ("mnt", "cgroup"): + own_namespace = os.stat(f"/proc/self/ns/{namespace}") + workload_namespace = os.stat(f"/proc/{main}/ns/{namespace}") + if (own_namespace.st_dev, own_namespace.st_ino) != (workload_namespace.st_dev, workload_namespace.st_ino): + raise EnforcementUnavailable("Workload native namespace differs") + else: + if child or main != 0 or service_props["SubState"] not in ("exited", "failed", "dead"): + raise EnforcementUnavailable("Manager has not reaped the admitted workload") + if not select.select([record.pidfd], [], [], 0)[0]: + raise EnforcementUnavailable("Owned MainPID pidfd has not observed exit") + try: + ticks, birth = _process_identity(identity.main_pid) + except FileNotFoundError: + pass + else: + if (ticks, birth) != (identity.start_ticks, identity.birth_time): + raise EnforcementUnavailable("MainPID number now names a different process") + try: + current = _open_directory(Path(mount_point + identity.service_relative_path), uid=identity.uid) + except FileNotFoundError: + if child or members: + raise EnforcementUnavailable("Live owned service leaf disappeared") + current = _open_directory(Path(mount_point + identity.slice_relative_path), uid=identity.uid) + try: + metadata = os.fstat(current) + _filesystem_magic(current) + if (metadata.st_dev, metadata.st_ino) != (identity.slice_device, identity.slice_inode): + raise EnforcementUnavailable("Persistent slice changed during leaf pruning readback") + finally: + os.close(current) + if _number(_read_at(slice_fd, "memory.max"), "terminal memory.max", positive=True) != identity.cap_bytes or _number(_read_at(slice_fd, "memory.swap.max"), "terminal memory.swap.max") != 0: + raise EnforcementUnavailable("Persistent cap changed during leaf pruning readback") + if any(values for _, values in _recursive_members(slice_fd, identity.slice_relative_path)): + raise EnforcementUnavailable("Persistent slice acquired a member during leaf pruning readback") + leaf_max = leaf_swap = "unavailable_pruned" + else: + try: + metadata = os.fstat(current) + _filesystem_magic(current) + if (metadata.st_dev, metadata.st_ino) != (identity.service_device, identity.service_inode) or service_props["ControlGroup"] != identity.service_relative_path: + raise EnforcementUnavailable("Current owned service path was replaced") + finally: + os.close(current) + leaf_max, leaf_swap = _read_at(service_fd, "memory.max"), _read_at(service_fd, "memory.swap.max") + if child: + if identity.main_pid != os.getpid(): + raise EnforcementUnavailable("Child gate identity is not its own actual PID") + peer_uid, peer_pid = identity.uid, identity.main_pid + else: + endpoint = record.endpoint + for descriptor, device, inode in ((endpoint.listener.fileno(), endpoint.listener_device, endpoint.listener_inode), + (endpoint.connection.fileno(), endpoint.connection_device, endpoint.connection_inode)): + metadata = os.fstat(descriptor) + if (metadata.st_dev, metadata.st_ino) != (device, inode): + raise EnforcementUnavailable("Strong barrier descriptor was replaced") + if _peer(endpoint.connection) != (identity.main_pid, identity.uid): + raise EnforcementUnavailable("Owned barrier peer changed") + hello = endpoint.hello + if type(hello) is not dict or set(hello) != {"generation", "manifest_sha256", "pid", "start_ticks", "bootstrap_sha256"}: + raise EnforcementUnavailable("Malformed authenticated hello schema") + for name, expected in (("generation", identity.generation), ("manifest_sha256", identity.manifest_sha256), ("pid", identity.main_pid), ("start_ticks", identity.start_ticks), ("bootstrap_sha256", identity.bootstrap_sha256)): + _literal(hello.get(name), expected, "hello " + name) + peer_uid, peer_pid = endpoint.peer_uid, endpoint.peer_pid + _deadline(deadline) + return LinuxScopeEvidence(identity, _CGROUP2_MAGIC, "cgroup2", True, True, True, + peer_uid, peer_pid, identity.start_ticks, identity.generation, + identity.manifest_sha256, members, identity.cap_bytes, 0, leaf_max, leaf_swap, True) + finally: + for descriptor in owned: + os.close(descriptor) + + def barrier(self, record: LinuxProvisionalLaunch, identity: LinuxInvocationIdentity, deadline: float) -> None: + _validate_record(record) + _validate_identity(identity) + _validate_evidence(self.inspect(record, deadline), identity) + _verified_psutil_birth(identity.main_pid, identity.birth_time) + runner = record.runner + claim_path = Path(runner.manifest["claim_path"]) + descriptor = os.open(claim_path, os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC) + directory = _open_directory(claim_path.parent, uid=identity.uid) + try: + raw = _read_fd(descriptor, bound=4096) + metadata = os.fstat(descriptor) + claim = _strict_json(raw) + if metadata.st_uid != identity.uid or stat.S_IMODE(metadata.st_mode) != 0o600: + raise EnforcementUnavailable("Current startup claim is not owner-only") + if type(claim) is not dict or set(claim) != {"generation", "launcher", "child"} or claim.get("generation") != identity.generation or claim.get("child") != {"pid": identity.main_pid, "born": identity.birth_time}: + raise EnforcementUnavailable("Durable current startup claim differs") + for name in ("launcher", "child"): + item = claim[name] + if type(item) is not dict or set(item) != {"pid", "born"} or type(item["pid"]) is not int or type(item["born"]) is not float: + raise EnforcementUnavailable("Durable startup claim types differ") + if claim["launcher"] != {"pid": runner.manifest["parent_pid"], "born": runner.manifest["parent_birth"]}: + raise EnforcementUnavailable("Durable startup launcher differs from authenticated parent") + if _process_identity(runner.manifest["parent_pid"]) != (runner.manifest["parent_start_ticks"], runner.manifest["parent_birth"]): + raise EnforcementUnavailable("Parent incarnation changed before release") + _verified_psutil_birth(runner.manifest["parent_pid"], runner.manifest["parent_birth"]) + _deadline(deadline) + os.fsync(descriptor) + os.fsync(directory) + current = os.stat(claim_path, follow_symlinks=False) + if (current.st_dev, current.st_ino) != (metadata.st_dev, metadata.st_ino) or _read_path(claim_path, bound=4096) != raw: + raise EnforcementUnavailable("Current startup claim changed during durability readback") + finally: + os.close(descriptor) + os.close(directory) + runner.store.append("claim-durable-before-release", {"claim_sha256": hashlib.sha256(raw).hexdigest(), "generation": identity.generation, + "pid": identity.main_pid, "birth_time": identity.birth_time}) + _validate_evidence(self.inspect(record, deadline), identity) + _packet_send(record.endpoint.connection, {"identity": asdict(identity), "release": True}, deadline) + preexec = _packet_receive(record.endpoint.connection, deadline) + _literal_packet(preexec, {"generation": identity.generation, "manifest_sha256": identity.manifest_sha256, + "preexec_gate_passed": True}, "initial pre-exec gate") + runner.store.observe("pre-reexec-v1.json") + runner.store.append("accepted-pre-reexec", {"generation": identity.generation, "pid": identity.main_pid, + "start_ticks": identity.start_ticks, "birth_time": identity.birth_time}) + environment_packet = _original_environment_packet(runner.forwarded, generation=identity.generation, + manifest_sha256=identity.manifest_sha256, names=runner.manifest["forwarded_names"]) + _packet_send(record.endpoint.connection, environment_packet, deadline) + acknowledgement = _packet_receive(record.endpoint.connection, deadline) + _literal_packet(acknowledgement, {"generation": identity.generation, "manifest_sha256": identity.manifest_sha256, + "gate_passed": True}, "restored pre-import gate") + runner.store.observe("first-import-v1.json") + runner.store.append("first-import", {"generation": identity.generation, "source_sha256": identity.bootstrap_sha256}) + while True: + _validate_evidence(self.inspect(record, deadline), identity) + try: + raw, _ = _private_artifact(runner.store.directory, "ready-v1.json") + except FileNotFoundError: + time.sleep(min(0.01, _left(deadline))) + continue + expected = {"version": 1, "generation": identity.generation, "manifest_sha256": identity.manifest_sha256, + "pid": identity.main_pid, "kernel_cap_bytes": identity.cap_bytes, "metric": _METRIC, + "physical_hard_cap": False, "dynamic_registry_cap": False} + _literal_packet(_strict_json(raw), expected, "application ready receipt") + runner.store.observe("ready-v1.json") + runner.store.append("application-ready", expected) + _validate_evidence(self.inspect(record, deadline), identity) + _deadline(deadline) + return + + def terminate(self, record: LinuxProvisionalLaunch, identity: LinuxInvocationIdentity, deadline: float) -> None: + effective = _termination_deadline(deadline, time.monotonic()) + _validate_record(record) + expected = _identity_snapshot(identity) + if record.identity != expected: + raise EnforcementUnavailable("Termination identity differs from the strong record") + resources = _termination_resources(record) + _same_termination_resources(record, expected, resources) + try: + _termination_emit(record, "termination-native-intent", {"version": 1, + "generation": expected.generation, "main_pid": expected.main_pid, + "start_ticks": expected.start_ticks, "birth_time": expected.birth_time, + "original_pidfd": record.pidfd, "effective_deadline": effective, + "kernel_cap_bytes": expected.cap_bytes, "swap_bytes": 0}, effective) + _same_termination_resources(record, expected, resources) + exited, _ = _termination_native_snapshot(self, record, expected, resources, effective) + if not exited: + for requested_signal in (signal.SIGTERM, signal.SIGKILL): + _termination_emit(record, "termination-signal-intent", {"generation": expected.generation, + "signal": int(requested_signal)}, effective) + _same_termination_resources(record, expected, resources) + exited, _ = _termination_native_snapshot(self, record, expected, resources, effective) + if exited: + break + _same_termination_resources(record, expected, resources) + exited = _signal_owned_pidfd(record, expected, resources, requested_signal, effective) + _termination_emit(record, "termination-signal-result", {"generation": expected.generation, + "signal": int(requested_signal), "exit_observed": exited}, effective) + _same_termination_resources(record, expected, resources) + if exited or _wait_pidfd_exit(record, expected, resources, min(effective, time.monotonic() + 1.0)): + break + _wait_owned_terminal(self, record, expected, resources, effective) + _termination_emit(record, "termination-manager-reaped", {"generation": expected.generation, + "main_pid": expected.main_pid, "exit_observed": True}, effective) + _same_termination_resources(record, expected, resources) + _deadline(effective) + except (OSError, RuntimeError, ValueError, TypeError, TimeoutError) as error: + try: + _same_termination_resources(record, expected, resources) + _termination_emit(record, "termination-native-unresolved", {"generation": expected.generation, + "error_type": type(error).__name__, + "errno": error.errno if isinstance(error, OSError) else None}, effective) + except (OSError, RuntimeError, ValueError, TypeError, TimeoutError): + pass + raise + + def capture(self, record: LinuxProvisionalLaunch, identity: LinuxInvocationIdentity, deadline: float) -> LinuxExitReceipt: + _validate_record(record) + _validate_identity(identity) + evidence = _validate_evidence(self.inspect(record, deadline), identity, terminal=True) + if evidence.members: + raise EnforcementUnavailable("Cannot capture terminal evidence for a live workload") + scope, store = record.scope_fd, record.runner.store + manager = _properties(identity.service_unit, ("InvocationID", "ExecMainPID", "ExecMainCode", "ExecMainStatus", "Result", "MainPID", "SubState"), deadline) + if manager["InvocationID"] != identity.service_invocation_id or _number(manager["ExecMainPID"], "ExecMainPID", positive=True) != identity.main_pid or manager["MainPID"] != "0": + raise EnforcementUnavailable("Terminal manager identity is unresolved") + code = _number(manager["ExecMainCode"], "ExecMainCode") + status = _number(manager["ExecMainStatus"], "ExecMainStatus") + if code not in (1, 2, 3): + raise EnforcementUnavailable("Manager has no explicit reaped exit or signal") + census = _recursive_members(scope.slice_fd, identity.slice_relative_path) + populated_lines = dict(line.split() for line in _read_at(scope.slice_fd, "cgroup.events").splitlines()) + if populated_lines.get("populated") != "0" or any(members for _, members in census): + raise EnforcementUnavailable("Persistent aggregate domain is not actually empty") + current = _number(_read_at(scope.slice_fd, "memory.current"), "memory.current") + after = _controller_events(scope.slice_fd) + slice_local_after = _controller_events(scope.slice_fd, "memory.events.local") + leaf_after: tuple[tuple[str, int], ...] | None = None + leaf_local_after: tuple[tuple[str, int], ...] | None = None + if evidence.leaf_memory_max != "unavailable_pruned": + try: + leaf_after = _controller_events(scope.service_fd) + leaf_local_after = _controller_events(scope.service_fd, "memory.events.local") + except FileNotFoundError: + evidence = _validate_evidence(self.inspect(record, deadline), identity, terminal=True) + if evidence.members or evidence.leaf_memory_max != "unavailable_pruned": + raise EnforcementUnavailable("Leaf counters disappeared without proven terminal pruning") + leaf_after = leaf_local_after = None + try: + peak = _number(_read_at(scope.slice_fd, "memory.peak"), "memory.peak") + except FileNotFoundError: + peak = None + counter_pairs = [(scope.baseline, after), (scope.baseline_local, slice_local_after)] + if leaf_after is not None and leaf_local_after is not None: + counter_pairs.extend(((scope.leaf_baseline, leaf_after), (scope.leaf_baseline_local, leaf_local_after))) + for previous, current_events in counter_pairs: + current_values = dict(current_events) + if any(name not in current_values or current_values[name] < value for name, value in previous): + raise EnforcementUnavailable("Owned controller counters decreased or disappeared") + for name in ("stderr-v1.log", "pre-reexec-v1.json", "first-import-v1.json", "ready-v1.json", "child-startup-failed-v1.json"): + try: + store.observe(name, durable=True, bound=16777216 if name == "stderr-v1.log" else _READ_BOUND) + except FileNotFoundError: + if name == "stderr-v1.log": + raise EnforcementUnavailable("Retained stderr evidence disappeared") + before = dict(scope.baseline) + deltas = tuple((name, value - before.get(name, 0)) for name, value in after) + leaf_deltas = None if leaf_after is None else tuple((name, dict(leaf_after)[name] - value) for name, value in scope.leaf_baseline) + leaf_local_deltas = None if leaf_local_after is None else tuple((name, dict(leaf_local_after)[name] - value) for name, value in scope.leaf_baseline_local) + payload = {"identity": asdict(identity), "exit_code": status if code == 1 else None, + "signal": status if code in (2, 3) else None, "manager_result": manager["Result"], + "memory_current": current, "memory_peak": peak, "events_before": scope.baseline, + "events_after": after, "event_deltas": deltas, "populated": False, + "counter_scope": "cap_bearing_slice_hierarchical", + "slice_local_before": scope.baseline_local, "slice_local_after": slice_local_after, + "leaf_hierarchical_before": scope.leaf_baseline, "leaf_hierarchical_after": leaf_after, + "leaf_local_before": scope.leaf_baseline_local, "leaf_local_after": leaf_local_after, + "leaf_hierarchical_deltas": leaf_deltas, "leaf_local_deltas": leaf_local_deltas, + "leaf_counters_available": leaf_after is not None, + "leaf_counter_status": "available" if leaf_after is not None else "unavailable_pruned", + "descendant_members": census, "kernel_cap_bytes": identity.cap_bytes, "physical_hard_cap": False, + "dynamic_registry_cap": False, "metric": _METRIC} + sequence, chain = store.append("terminal-before-stop", payload) + store.verify() + artifacts = tuple(sorted(store.hashes.items())) + _deadline(deadline) + return LinuxExitReceipt(1, identity, payload["exit_code"], payload["signal"], manager["Result"], current, peak, + scope.baseline, after, False, census, True, sequence, chain, artifacts, + _METRIC, False, identity.cap_bytes, False) + + def stop(self, record: LinuxProvisionalLaunch, identity: LinuxInvocationIdentity, deadline: float) -> None: + _validate_record(record) + _validate_identity(identity) + if self.inspect(record, deadline).members: + raise EnforcementUnavailable("Owned stop requires a reaped empty workload") + store = record.runner.store + store.verify() + store.append("owned-stop-request", {"generation": identity.generation, "service_invocation_id": identity.service_invocation_id, + "slice_invocation_id": identity.slice_invocation_id}) + self.inspect(record, deadline) + _run_command(["/usr/bin/systemctl", "--user", "stop", identity.service_unit], deadline) + slice_props = _properties(identity.slice_unit, ("InvocationID", "ControlGroup", "MemoryMax", "MemorySwapMax", "ActiveState"), deadline) + metadata = os.fstat(record.scope_fd.slice_fd) + if slice_props["InvocationID"] != identity.slice_invocation_id or slice_props["ControlGroup"] != identity.slice_relative_path or (metadata.st_dev, metadata.st_ino) != (identity.slice_device, identity.slice_inode): + raise EnforcementUnavailable("Slice replaced before owned final stop") + if _number(slice_props["MemoryMax"], "MemoryMax", positive=True) != identity.cap_bytes or _number(slice_props["MemorySwapMax"], "MemorySwapMax") != 0: + raise EnforcementUnavailable("Slice cap changed before owned final stop") + if any(members for _, members in _recursive_members(record.scope_fd.slice_fd, identity.slice_relative_path)): + raise EnforcementUnavailable("Slice acquired a member before owned final stop") + _run_command(["/usr/bin/systemctl", "--user", "stop", identity.slice_unit], deadline) + store.append("owned-stop-complete", {"generation": identity.generation}) + self._close_resolved(record, identity) + record.runner.stop_complete = True + + def _close_resolved(self, record: LinuxProvisionalLaunch, identity: LinuxInvocationIdentity) -> None: + scope, endpoint, store = record.scope_fd, record.endpoint, record.runner.store + expected = ((scope.slice_fd, identity.slice_device, identity.slice_inode), + (scope.service_fd, identity.service_device, identity.service_inode), + (record.pidfd, scope.pidfd_device, scope.pidfd_inode), + (endpoint.listener.fileno(), endpoint.listener_device, endpoint.listener_inode), + (endpoint.connection.fileno(), endpoint.connection_device, endpoint.connection_inode), + (store.directory, store.metadata.st_dev, store.metadata.st_ino)) + for descriptor, device, inode in expected: + metadata = os.fstat(descriptor) + if (metadata.st_dev, metadata.st_ino) != (device, inode): + raise EnforcementUnavailable("Resolved resource identity changed; no borrowed descriptor was closed") + endpoint.connection.close() + endpoint.listener.close() + os.close(record.pidfd) + os.close(scope.service_fd) + os.close(scope.slice_fd) + os.close(store.directory) + + +def get_linux_adapter() -> LinuxAdapter: + return LinuxAdapter() + + +def _physical_total() -> int: + pages = _integer(os.sysconf("SC_PHYS_PAGES"), "physical pages", positive=True) + page_size = _integer(os.sysconf("SC_PAGE_SIZE"), "physical page size", positive=True) + return _integer(pages * page_size, "physical_total", positive=True) + + +def _child_bundle(manifest_path: str) -> tuple[int, Path, dict[str, object], str]: + path = Path(manifest_path) + if not path.is_absolute() or path.name != "manifest-v1.json": + raise EnforcementUnavailable("Child requires an absolute generation manifest") + directory = _open_directory(path.parent, uid=os.getuid()) + try: + if stat.S_IMODE(os.fstat(directory).st_mode) != 0o700: + raise EnforcementUnavailable("Child generation directory is not private") + raw, manifest_sha = _private_artifact(directory, path.name) + manifest = _strict_json(raw) + fields = {"version", "generation", "uid", "boot_id", "slice_unit", "service_unit", "cap_bytes", "swap_bytes", + "bootstrap_sha256", "deadline", "parent_pid", "parent_start_ticks", "parent_birth", "claim_path", + "ancestor_memory_max", "forwarded_names", "interpreter"} + if type(manifest) is not dict or set(manifest) != fields: + raise EnforcementUnavailable("Manifest must have the complete exact schema") + _literal(manifest.get("version"), 1, "manifest version") + generation = _hex(manifest.get("generation"), 32, "manifest generation") + if path.parent.name != generation: + raise EnforcementUnavailable("Manifest generation directory differs") + _literal(manifest.get("uid"), os.getuid(), "manifest uid") + _literal(manifest.get("boot_id"), _boot_id(), "manifest boot") + _literal(manifest.get("swap_bytes"), 0, "manifest swap") + _integer(manifest.get("cap_bytes"), "manifest cap", positive=True) + _deadline(manifest.get("deadline")) + _integer(manifest.get("parent_pid"), "parent pid", positive=True) + _integer(manifest.get("parent_start_ticks"), "parent start ticks", positive=True) + if type(manifest["parent_birth"]) is not float or not math.isfinite(manifest["parent_birth"]) or manifest["parent_birth"] <= 0: + raise EnforcementUnavailable("Manifest parent birth is invalid") + if _process_identity(manifest["parent_pid"]) != (manifest["parent_start_ticks"], manifest["parent_birth"]): + raise EnforcementUnavailable("Parent process incarnation changed") + names = manifest["forwarded_names"] + if type(names) is not list or not names or len(names) > 4096 or any(type(name) is not str or re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name) is None for name in names) or names != sorted(set(names)): + raise EnforcementUnavailable("Manifest forwarding names are malformed") + interpreter = manifest["interpreter"] + if type(interpreter) is not dict or set(interpreter) != {"path", "resolved_path", "device", "inode", "sha256"}: + raise EnforcementUnavailable("Manifest interpreter pin is malformed") + if _executable_pin(interpreter["path"]) != interpreter or os.path.abspath(sys.executable) != interpreter["path"]: + raise EnforcementUnavailable("Selected interpreter identity changed") + bootstrap_sha = _hash_path(Path(__file__).absolute(), bound=1048576) + _literal(manifest.get("bootstrap_sha256"), bootstrap_sha, "manifest bootstrap") + _load_capsule() + return directory, path, manifest, manifest_sha + except (OSError, RuntimeError, ValueError, TypeError, TimeoutError): + os.close(directory) + raise + + +def _child_main(manifest_path: str) -> None: + global _child_context + if sys.platform != "linux" or not sys.flags.no_site or not sys.flags.isolated: + raise EnforcementUnavailable("Initial child must start on Linux with isolated site disabled") + directory, path, manifest, manifest_sha = _child_bundle(manifest_path) + endpoint = None + try: + deadline = manifest["deadline"] + generation, bootstrap_sha = manifest["generation"], manifest["bootstrap_sha256"] + ticks, _ = _process_identity(os.getpid()) + endpoint = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + endpoint.settimeout(_left(deadline)) + endpoint.connect(str(path.parent / "barrier-v1.sock")) + parent_pid, parent_uid = _peer(endpoint) + if parent_pid != manifest["parent_pid"] or parent_uid != os.getuid() or _process_identity(parent_pid)[0] != manifest["parent_start_ticks"]: + raise EnforcementUnavailable("Child barrier is not its authenticated parent incarnation") + _packet_send(endpoint, {"generation": generation, "manifest_sha256": manifest_sha, "pid": os.getpid(), + "start_ticks": ticks, "bootstrap_sha256": bootstrap_sha}, deadline) + release = _packet_receive(endpoint, deadline) + if set(release) != {"identity", "release"} or release["release"] is not True or type(release["identity"]) is not dict: + raise EnforcementUnavailable("Parent did not send a valid release identity") + identity = LinuxInvocationIdentity(**release["identity"]) + if identity.generation != generation or identity.manifest_sha256 != manifest_sha or identity.bootstrap_sha256 != bootstrap_sha: + raise EnforcementUnavailable("Child release identity differs from its manifest") + identity_raw, identity_sha = _private_artifact(directory, "identity-v1.json") + if _strict_json(identity_raw) != asdict(identity): + raise EnforcementUnavailable("Retained release identity differs from child gate") + _child_context = (path.parent, identity, endpoint, parent_pid, parent_uid) + child_gate(identity, deadline=deadline) + barrier_metadata = os.fstat(endpoint.fileno()) + barrier = {"descriptor": endpoint.fileno(), "device": barrier_metadata.st_dev, "inode": barrier_metadata.st_ino} + _exclusive_file(directory, "pre-reexec-v1.json", _canonical(_preexec_payload(identity, identity_sha, barrier))) + _packet_send(endpoint, {"generation": generation, "manifest_sha256": manifest_sha, + "preexec_gate_passed": True}, deadline) + retained = _packet_receive(endpoint, deadline) + if set(retained) != {"generation", "manifest_sha256", "preexec_witness_retained", "forwarded_environment"}: + raise EnforcementUnavailable("Parent did not retain the accepted pre-exec witness") + _literal(retained["generation"], generation, "retained environment generation") + _literal(retained["manifest_sha256"], manifest_sha, "retained environment manifest") + _literal(retained["preexec_witness_retained"], True, "retained pre-exec witness") + clean_environment = _original_environment(retained["forwarded_environment"], generation=generation, + names=manifest["forwarded_names"]) + clean_environment[_MANIFEST_ENV] = str(path) + clean_environment[_IDENTITY_ENV] = identity_sha + clean_environment[_BARRIER_ENV] = str(endpoint.fileno()) + child_gate(identity, deadline=deadline) + descriptors = [] + with os.scandir("/proc/self/fd") as entries: + for entry in entries: + if len(descriptors) >= 128: + raise EnforcementUnavailable("Child descriptor inventory exceeds its bound") + descriptors.append(int(entry.name)) + for descriptor in descriptors: + if descriptor > 2 and descriptor != endpoint.fileno(): + try: + os.set_inheritable(descriptor, False) + except OSError as error: + if error.errno != 9: + raise + os.set_inheritable(endpoint.fileno(), True) + # exec preserves the admitted PID/cgroup while restoring ordinary flags, + # site startup and all originally forwarded PYTHON* interpreter settings. + os.execve(manifest["interpreter"]["path"], + [manifest["interpreter"]["path"], "-m", "truememory.model_server"], clean_environment) + except (OSError, RuntimeError, ValueError, TypeError, TimeoutError) as error: + _exclusive_file(directory, "child-startup-failed-v1.json", _canonical({"version": 1, + "generation": manifest["generation"], "pid": os.getpid(), "error_type": type(error).__name__})) + raise + finally: + if endpoint is not None: + endpoint.close() + os.close(directory) + + +def _restore_child_context() -> None: + global _child_context + if sys.platform != "linux" or not required_cap_selected(os.environ): + raise EnforcementUnavailable("Required daemon cannot restore an unsupported gate") + manifest_path = _text(os.environ.get(_MANIFEST_ENV), "inherited manifest", bound=4096) + expected_identity_sha = _hex(os.environ.get(_IDENTITY_ENV), 64, "inherited identity hash") + raw_descriptor = os.environ.get(_BARRIER_ENV) + if type(raw_descriptor) is not str or re.fullmatch(r"[0-9]+", raw_descriptor) is None or len(raw_descriptor) > 10: + raise EnforcementUnavailable("Required daemon has no bounded inherited barrier descriptor") + descriptor = _integer(int(raw_descriptor), "inherited barrier descriptor") + if descriptor < 3: + raise EnforcementUnavailable("Barrier cannot replace a standard stream") + directory, path, manifest, manifest_sha = _child_bundle(manifest_path) + endpoint = None + try: + identity_raw, identity_sha = _private_artifact(directory, "identity-v1.json") + _literal(identity_sha, expected_identity_sha, "inherited identity hash") + identity_data = _strict_json(identity_raw) + if type(identity_data) is not dict: + raise EnforcementUnavailable("Retained identity is malformed") + identity = LinuxInvocationIdentity(**identity_data) + if identity.manifest_sha256 != manifest_sha or identity.main_pid != os.getpid() or identity.generation != manifest["generation"] or _process_identity(os.getpid()) != (identity.start_ticks, identity.birth_time): + raise EnforcementUnavailable("Reexecuted process is not the admitted incarnation") + preexec_raw, _ = _private_artifact(directory, "pre-reexec-v1.json") + preexec_data = _strict_json(preexec_raw) + if type(preexec_data) is not dict: + raise EnforcementUnavailable("Durable pre-exec witness differs from the admitted identity") + _literal_packet(preexec_data, _preexec_payload(identity, identity_sha, preexec_data.get("barrier")), "durable pre-exec witness") + barrier = preexec_data["barrier"] + _literal(descriptor, barrier["descriptor"], "inherited barrier number") + barrier_metadata = os.fstat(descriptor) + if (barrier_metadata.st_dev, barrier_metadata.st_ino, barrier_metadata.st_uid) != (barrier["device"], barrier["inode"], identity.uid) or not stat.S_ISSOCK(barrier_metadata.st_mode): + raise EnforcementUnavailable("Inherited barrier descriptor was reused; it was left untouched") + os.set_inheritable(descriptor, False) + endpoint = socket.socket(fileno=descriptor) + if endpoint.family != socket.AF_UNIX or endpoint.getsockopt(socket.SOL_SOCKET, socket.SO_TYPE) != socket.SOCK_STREAM: + raise EnforcementUnavailable("Inherited barrier is not the original local stream") + parent_pid, parent_uid = _peer(endpoint) + if (parent_pid, parent_uid) != (manifest["parent_pid"], identity.uid) or _process_identity(parent_pid) != (manifest["parent_start_ticks"], manifest["parent_birth"]): + raise EnforcementUnavailable("Inherited barrier no longer names the original parent") + claim_path = Path(manifest["claim_path"]) + claim_directory = _open_directory(claim_path.parent, uid=identity.uid) + try: + claim_raw, claim_sha = _private_artifact(claim_directory, claim_path.name, bound=4096) + claim = _validate_claim(_strict_json(claim_raw), pid=identity.main_pid, birth_time=identity.birth_time, generation=identity.generation) + if claim["launcher"] != {"pid": parent_pid, "born": manifest["parent_birth"]}: + raise EnforcementUnavailable("Inherited gate launcher differs from the current durable claim") + _, _, receipts = _receipt_chain(directory) + witnesses = [item["payload"] for item in receipts if item["phase"] == "claim-durable-before-release"] + if len(witnesses) != 1 or type(witnesses[0]) is not dict or witnesses[0].get("claim_sha256") != claim_sha: + raise EnforcementUnavailable("Inherited gate has no current durable claim witness") + finally: + os.close(claim_directory) + _child_context = (path.parent, identity, endpoint, parent_pid, parent_uid) + child_gate(identity, deadline=manifest["deadline"]) + _exclusive_file(directory, "first-import-v1.json", _canonical(preexec_data)) + _packet_send(endpoint, {"generation": identity.generation, "manifest_sha256": manifest_sha, + "gate_passed": True}, manifest["deadline"]) + except (OSError, RuntimeError, ValueError, TypeError, TimeoutError): + if endpoint is not None: + endpoint.close() + _child_context = None + raise + finally: + os.close(directory) + + +def require_child_scope() -> None: + if not required_cap_selected(os.environ): + raise EnforcementUnavailable("Required daemon was not admitted through the pre-exec child gate") + if _child_context is None: + _restore_child_context() + path, identity, _, _, _ = _child_context + directory = _open_directory(path, uid=identity.uid) + try: + raw, digest = _private_artifact(directory, "manifest-v1.json") + _literal(digest, identity.manifest_sha256, "required current manifest") + manifest = _strict_json(raw) + if type(manifest) is not dict: + raise EnforcementUnavailable("Required current manifest is malformed") + child_gate(identity, deadline=manifest["deadline"]) + finally: + os.close(directory) + + +def record_server_ready() -> None: + require_child_scope() + path, identity, _, _, _ = _child_context + directory = _open_directory(path, uid=identity.uid) + try: + _exclusive_file(directory, "ready-v1.json", _canonical({"version": 1, "generation": identity.generation, + "manifest_sha256": identity.manifest_sha256, "pid": identity.main_pid, + "kernel_cap_bytes": identity.cap_bytes, "metric": _METRIC, + "physical_hard_cap": False, "dynamic_registry_cap": False})) + finally: + os.close(directory) + + +def _write_console_claim(path: Path, claim: dict[str, object]) -> None: + raw = _canonical(claim) + directory = _open_directory(path.parent, uid=os.getuid()) + name = path.name + "." + secrets.token_hex(8) + ".tmp" + try: + _exclusive_file(directory, name, raw) + os.rename(name, path.name, src_dir_fd=directory, dst_dir_fd=directory) + os.fsync(directory) + finally: + os.close(directory) + + +def _verified_psutil_birth(pid: int, candidate: float) -> float: + # This parent-only verification does not run in the pre-site child gate. + psutil = importlib.import_module("psutil") + process = psutil.Process(pid) + observed = process.create_time() + if process.status() == psutil.STATUS_ZOMBIE or type(observed) is not float or observed != candidate: + raise EnforcementUnavailable("Actual psutil process birth differs from the native incarnation") + return observed + + +def main() -> None: + if len(sys.argv) == 3 and sys.argv[1] == "--child": + _child_main(sys.argv[2]) + return + if len(sys.argv) != 1: + raise SystemExit("The capped launcher accepts no application arguments") + environment = dict(os.environ) + if _FLAG in environment: + required_cap_selected(environment) + else: + environment[_FLAG] = "1" + if sys.platform != "linux": + raise EnforcementUnavailable("A required Linux charged-domain cap is unsupported on this platform") + policy = plan_required_linux_launch(environment, physical_total=None if "TRUEMEMORY_MODEL_SERVER_MAX_RSS_MB" in environment else _physical_total()) + deadline = time.monotonic() + 30.0 + _required_linux_support(policy, deadline) + base = Path.home() / ".truememory" + base.mkdir(mode=0o700, parents=True, exist_ok=True) + directory = _open_directory(base, uid=os.getuid()) + if stat.S_IMODE(os.fstat(directory).st_mode) != 0o700: + os.close(directory) + raise EnforcementUnavailable("Existing startup directory must be private") + fcntl = importlib.import_module("fcntl") + lock = os.open("model_server.start.lock", os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW | os.O_CLOEXEC, 0o600, dir_fd=directory) + try: + fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB) + claim_path = base / "model_server.start.json" + try: + old = _strict_json(_read_path(claim_path, bound=4096)) + except FileNotFoundError: + old = None + if old is not None: + _validate_claim(old, pending=True) + previous = old.get("child") or old.get("launcher") + try: + _, birth = _process_identity(previous["pid"]) + except FileNotFoundError: + pass + else: + if birth == previous.get("born"): + raise EnforcementUnavailable("Existing launch ownership is still live or unresolved") + bind = os.open("model_server.lock", os.O_RDWR | os.O_CREAT | os.O_NOFOLLOW | os.O_CLOEXEC, 0o600, dir_fd=directory) + try: + fcntl.flock(bind, fcntl.LOCK_EX | fcntl.LOCK_NB) + finally: + os.close(bind) + generation = secrets.token_hex(16) + _, candidate = _process_identity(os.getpid()) + parent_born = _verified_psutil_birth(os.getpid(), candidate) + claim = {"generation": generation, "launcher": {"pid": os.getpid(), "born": parent_born}, "child": None} + _write_console_claim(claim_path, claim) + handle = launch_owned_linux(policy, generation=generation, python_executable=os.path.abspath(sys.executable), deadline=deadline) + child_born = _verified_psutil_birth(handle.pid, handle.record.identity.birth_time) + claim["child"] = {"pid": handle.pid, "born": child_born} + _write_console_claim(claim_path, claim) + handle.release_after_claim(claim, deadline=deadline) + finally: + os.close(lock) + os.close(directory) + # The foreground console retains ownership through exit; no live stop or + # finalizer cleanup is substituted for terminal evidence. + while not select.select([handle.record.pidfd], [], [], 1.0)[0]: + pass + stop_owned_exact(handle, deadline=time.monotonic() + 30.0) + + +_NATIVE_STARTUP_U64 = (1 << 64) - 1 +_NATIVE_STARTUP_U32 = (1 << 32) - 1 +_NATIVE_STARTUP_LIBRARY = "/usr/lib/x86_64-linux-gnu/libsystemd.so.0.32.0" +_NATIVE_STARTUP_LIBRARY_SHA = "9d70722be19814a7710be5f8df42fb1a36566a4038ae789d63e948c2b4b65ca1" +_NATIVE_STARTUP_LIBRARY_STAT = (64513, 23495170, 33188, 0, 0, 807936, 1749652895000000000, 1771515970026234838, 1) +_NATIVE_STARTUP_MATCH = "type='signal',sender='org.freedesktop.DBus',path='/org/freedesktop/DBus',interface='org.freedesktop.DBus',member='NameOwnerChanged',arg0='org.freedesktop.systemd1'" +_NATIVE_STARTUP_VERSION = r"249(?:\.[0-9]+)*(?:[-+~][A-Za-z0-9][A-Za-z0-9.+~_-]*)?" +_NATIVE_STARTUP_CODES = frozenset(( + "invalid_input", "invalid_owner", "capacity_exhausted", "sequence_exhausted", "stale_sequence", + "busy", "forked_owner", "wrong_thread", "deadline_expired", "backend_unavailable", + "backend_pin_mismatch", "backend_abi_unproven", "endpoint_unavailable", "endpoint_pin_mismatch", + "peer_auth_failed", "native_call_failed", "transport_disconnected", "manager_owner_changed", + "manager_auth_failed", "subscription_refused", "malformed_evidence", "evidence_overflow", + "callback_failed", "native_resource_drift", "cleanup_unresolved", "already_closed", + "already_released", "loader_override_present", "clock_regressed", "cleanup_deadline_invalid", + "cleanup_counter_exhausted", "cleanup_attempts_exhausted", "native_string_unterminated", + "native_pointer_unproven", "native_release_unobserved", +)) +_NATIVE_STARTUP_PHASES = ("prepared", "authenticating", "subscribed", "terminal_unknown", "closing_precreate", "closed_precreate", "released") +_NATIVE_STARTUP_RECORDS: dict[int, object] = {} +_NATIVE_STARTUP_LOCK = threading.RLock() +_NATIVE_STARTUP_CREATOR_PID = os.getpid() +_NATIVE_STARTUP_NEXT_OWNER_ID = 1 +_NATIVE_STARTUP_OPERATION: object | None = None + + +def _native_startup_fail(code: str) -> None: + if type(code) is not str or code not in _NATIVE_STARTUP_CODES: + code = "invalid_input" + raise EnforcementUnavailable("native_startup:" + code) + + +def _native_startup_fields(value: object, cls: type) -> None: + if type(value) is not cls: + _native_startup_fail("invalid_input") + try: + for name in cls.__dataclass_fields__: + object.__getattribute__(value, name) + except (AttributeError, TypeError): + _native_startup_fail("invalid_input") + + +def _native_startup_integer(value: object, *, positive: bool = False) -> int: + if type(value) is not int or value < (1 if positive else 0) or value > _NATIVE_STARTUP_U64: + _native_startup_fail("invalid_input") + return value + + +def _native_startup_float(value: object) -> float: + if type(value) is not float or not math.isfinite(value): + _native_startup_fail("invalid_input") + return value + + +def _native_startup_text(value: object, *, bound: int, empty: bool = False, ascii_only: bool = False) -> str: + if type(value) is not str or "\x00" in value or (not value and not empty): + _native_startup_fail("invalid_input") + try: + encoded = value.encode("ascii" if ascii_only else "utf-8", "strict") + except UnicodeError: + _native_startup_fail("invalid_input") + if len(encoded) > bound: + _native_startup_fail("invalid_input") + return value + + +def _native_startup_sequence_pair(sequence: int, *, cleanup: bool = False) -> tuple[int, int]: + _native_startup_integer(sequence) + if type(cleanup) is not bool: + _native_startup_fail("invalid_input") + if sequence > _NATIVE_STARTUP_U64 - 2: + _native_startup_fail("cleanup_counter_exhausted" if cleanup else "sequence_exhausted") + return sequence + 1, sequence + 2 + + +@dataclass(frozen=True, slots=True) +class StartupNativeBackendPin: + version: int + canonical_path: str + sha256: str + stat_pin: tuple[int, int, int, int, int, int, int, int, int] + + def __post_init__(self) -> None: + _native_startup_validate_pin(self) + + +@dataclass(frozen=True, slots=True) +class StartupNativeRequest: + version: int + policy: LinuxDaemonLaunchPolicy + generation: str + python_executable: str + backend_pin: StartupNativeBackendPin + + def __post_init__(self) -> None: + _native_startup_validate_request(self) + + +@dataclass(frozen=True, slots=True) +class NativeStartupOwner: + version: int + owner_id: int + creator_pid: int + + def __post_init__(self) -> None: + _native_startup_validate_owner(self) + + +@dataclass(frozen=True, slots=True) +class NativeStartupSnapshot: + version: int + owner: NativeStartupOwner + request: StartupNativeRequest + original_deadline: float + last_observed_now: float + operation_sequence: int + cleanup_sequence: int + cleanup_deadline: float | None + cleanup_attempted: bool + cleanup_outcome: str | None + phase: str + context: StartupBusContext | None + peer_credentials: tuple[int, int, int] | None + manager_version: str | None + authentication_attempted: bool + owner_match_confirmed: bool + subscription_confirmed: bool + slice_attempted: bool + service_attempted: bool + connection_closed: bool + native_objects_released: bool + reason: str | None + + def __post_init__(self) -> None: + _native_startup_validate_snapshot(self) + + +def _native_startup_validate_pin(pin: object) -> StartupNativeBackendPin: + _native_startup_fields(pin, StartupNativeBackendPin) + if type(pin.version) is not int or pin.version != 1: + _native_startup_fail("invalid_input") + path = _native_startup_text(pin.canonical_path, bound=4096) + if not path.startswith("/") or any(part in ("", ".", "..") for part in path.split("/")[1:]): + _native_startup_fail("invalid_input") + if type(pin.sha256) is not str or re.fullmatch(r"[0-9a-f]{64}", pin.sha256) is None: + _native_startup_fail("invalid_input") + if type(pin.stat_pin) is not tuple or len(pin.stat_pin) != 9: + _native_startup_fail("invalid_input") + for item in pin.stat_pin: + _native_startup_integer(item) + dev, ino, mode, uid, _, size, _, _, links = pin.stat_pin + if not dev or not ino or not size or size > 4194304 or not links or not stat.S_ISREG(mode) or uid != 0 or mode & 0o022: + _native_startup_fail("invalid_input") + return pin + + +def _native_startup_validate_request(request: object) -> StartupNativeRequest: + _native_startup_fields(request, StartupNativeRequest) + if type(request.version) is not int or request.version != 1: + _native_startup_fail("invalid_input") + try: + _validate_policy(request.policy) + except (TypeError, ValueError, AttributeError, EnforcementUnavailable): + _native_startup_fail("invalid_input") + if type(request.generation) is not str or re.fullmatch(r"[0-9a-f]{32}", request.generation) is None: + _native_startup_fail("invalid_input") + path = _native_startup_text(request.python_executable, bound=4096) + if not path.startswith("/") or any(part in ("", ".", "..") for part in path.split("/")[1:]): + _native_startup_fail("invalid_input") + _native_startup_validate_pin(request.backend_pin) + return request + + +def _native_startup_validate_owner(owner: object) -> NativeStartupOwner: + _native_startup_fields(owner, NativeStartupOwner) + if type(owner.version) is not int or owner.version != 1: + _native_startup_fail("invalid_input") + _native_startup_integer(owner.owner_id, positive=True) + _native_startup_integer(owner.creator_pid, positive=True) + return owner + + +def _native_startup_validate_snapshot(state: object) -> NativeStartupSnapshot: + _native_startup_fields(state, NativeStartupSnapshot) + if type(state.version) is not int or state.version != 1: + _native_startup_fail("invalid_input") + _native_startup_validate_owner(state.owner) + _native_startup_validate_request(state.request) + _native_startup_float(state.original_deadline) + _native_startup_float(state.last_observed_now) + _native_startup_integer(state.operation_sequence) + _native_startup_integer(state.cleanup_sequence) + if state.cleanup_deadline is not None: + _native_startup_float(state.cleanup_deadline) + for name in ("cleanup_attempted", "authentication_attempted", "owner_match_confirmed", "subscription_confirmed", "connection_closed", "native_objects_released"): + if type(getattr(state, name)) is not bool: + _native_startup_fail("invalid_input") + for name in ("slice_attempted", "service_attempted"): + if type(getattr(state, name)) is not bool or getattr(state, name): + _native_startup_fail("invalid_input") + if type(state.phase) is not str or state.phase not in _NATIVE_STARTUP_PHASES: + _native_startup_fail("invalid_input") + if state.reason is not None and (type(state.reason) is not str or state.reason not in _NATIVE_STARTUP_CODES): + _native_startup_fail("invalid_input") + if state.cleanup_outcome is not None and (type(state.cleanup_outcome) is not str or state.cleanup_outcome not in ("in_progress", "closed", "partial", "unknown")): + _native_startup_fail("invalid_input") + if state.peer_credentials is not None: + if type(state.peer_credentials) is not tuple or len(state.peer_credentials) != 3: + _native_startup_fail("invalid_input") + for i, value in enumerate(state.peer_credentials): + _native_startup_integer(value, positive=i == 0) + if state.manager_version is not None: + _native_startup_text(state.manager_version, bound=256, ascii_only=True) + if re.fullmatch(_NATIVE_STARTUP_VERSION, state.manager_version, flags=re.ASCII) is None: + _native_startup_fail("invalid_input") + if state.context is not None: + try: + _startup_validate_context(state.context) + except (TypeError, ValueError, AttributeError, EnforcementUnavailable): + _native_startup_fail("invalid_input") + if state.manager_version is None or state.peer_credentials is None: + _native_startup_fail("invalid_input") + if state.subscription_confirmed and (not state.owner_match_confirmed or state.context is None): + _native_startup_fail("invalid_input") + if state.owner_match_confirmed and not state.authentication_attempted: + _native_startup_fail("invalid_input") + if state.phase == "prepared" and (state.operation_sequence != 0 or state.authentication_attempted or state.context is not None or state.peer_credentials is not None or not state.connection_closed or not state.native_objects_released): + _native_startup_fail("invalid_input") + if state.phase == "prepared" and (state.cleanup_sequence != 0 or state.cleanup_attempted or state.reason is not None): + _native_startup_fail("invalid_input") + if state.phase in ("prepared", "authenticating", "subscribed") and state.last_observed_now >= state.original_deadline: + _native_startup_fail("invalid_input") + if state.phase == "authenticating" and (not state.authentication_attempted or state.operation_sequence != 1 or state.cleanup_attempted): + _native_startup_fail("invalid_input") + if state.phase == "subscribed" and (not state.subscription_confirmed or state.connection_closed or state.native_objects_released): + _native_startup_fail("invalid_input") + if state.phase in ("closed_precreate", "released") and (not state.connection_closed or not state.native_objects_released): + _native_startup_fail("invalid_input") + if state.phase == "subscribed" and (state.operation_sequence != 2 or state.cleanup_attempted or state.reason is not None): + _native_startup_fail("invalid_input") + if state.phase == "terminal_unknown" and state.reason is None: + _native_startup_fail("invalid_input") + if not state.authentication_attempted and (state.operation_sequence != 0 or state.owner_match_confirmed or state.subscription_confirmed or state.context is not None or state.peer_credentials is not None or state.manager_version is not None): + _native_startup_fail("invalid_input") + if not state.cleanup_attempted and state.cleanup_sequence != 0: + _native_startup_fail("invalid_input") + if state.phase == "closing_precreate" and (not state.cleanup_attempted or state.cleanup_outcome != "in_progress"): + _native_startup_fail("invalid_input") + if state.phase in ("closed_precreate", "released") and (not state.cleanup_attempted or state.cleanup_outcome != "closed"): + _native_startup_fail("invalid_input") + if state.cleanup_attempted != (state.cleanup_deadline is not None) or state.cleanup_attempted != (state.cleanup_outcome is not None): + _native_startup_fail("invalid_input") + return state + + +def _native_startup_copy_request(request: StartupNativeRequest) -> StartupNativeRequest: + _native_startup_validate_request(request) + pin = request.backend_pin + return StartupNativeRequest(1, _policy_snapshot(request.policy), request.generation, request.python_executable, + StartupNativeBackendPin(1, pin.canonical_path, pin.sha256, tuple(pin.stat_pin))) + + +def _native_startup_request_key(request: StartupNativeRequest) -> tuple: + _native_startup_validate_request(request) + policy = request.policy + return (request.version, policy.version, policy.cap_bytes, policy.swap_bytes, policy.metric, + policy.physical_hard_cap, policy.dynamic_registry_cap, request.generation, + request.python_executable, request.backend_pin.canonical_path, request.backend_pin.sha256, + request.backend_pin.stat_pin) + + +def _native_startup_state_key(state: NativeStartupSnapshot) -> tuple: + _native_startup_validate_snapshot(state) + context = state.context + context_key = None if context is None else (context.version, context.bus_id, context.manager_unique_name, context.client_unique_name, context.manager_uid) + return tuple(getattr(state, name) if name not in ("request", "context") else + (_native_startup_request_key(state.request) if name == "request" else context_key) + for name in NativeStartupSnapshot.__dataclass_fields__) + + +class _NativeStartupId128(ctypes.Union): + _fields_ = (("bytes", ctypes.c_uint8 * 16), ("qwords", ctypes.c_uint64 * 2)) + + +class _NativeStartupReference: + def __init__(self, role: str, out: object) -> None: + self.role = role + self.original_role = role + self.out = out + self.original_out = out + self.pointer: int | None = None + self.state = "acquisition_pending" + self.release_frame: object | None = None + + +class _NativeStartupResources: + def __init__(self) -> None: + self.backend: object | None = None + self.library: object | None = None + self.library_fd: int | None = None + self.directory_fds: list[tuple[int, tuple, str]] = [] + self.socket: object | None = None + self.raw_fd: int | None = None + self.raw_pin: tuple | None = None + self.transfer = "none" + self.fd_release_observed = False + self.bus: _NativeStartupReference | None = None + self.references: list[_NativeStartupReference] = [] + self.filter_binding: object | None = None + self.pending: object | None = None + self.callbacks: list[object] = [] + self.frames: list[object] = [] + self.active_calls = 0 + self.active_callbacks = 0 + self.callback_error: str | None = None + self.events: list[dict] = [] + self.evidence_bytes = 0 + self.callback_count = 0 + self.copy_bytes = 0 + self.iterations = 0 + self.client_name: str | None = None + self.bus_id: str | None = None + self.manager_name: str | None = None + self.manager_uid: int | None = None + self.manager_version: str | None = None + self.peer: tuple[int, int, int] | None = None + self.bus_closed = False + self.last_frame: object | None = None + self.borrowed_message: int | None = None + self.seen_serials: set[tuple[str, int]] = set() + + +class _NativeStartupSeal: + def __init__(self, owner: NativeStartupOwner, request: StartupNativeRequest, deadline: float, resources: _NativeStartupResources) -> None: + self.owner = owner + self.owner_values = (owner.version, owner.owner_id, owner.creator_pid) + self.request = _native_startup_copy_request(request) + self.request_key = _native_startup_request_key(self.request) + self.original_deadline = deadline + self.thread = threading.current_thread() + self.thread_ident = threading.get_ident() + self.native_id = threading.get_native_id() + self.uid = os.getuid() + self.euid = os.geteuid() + self.resources = resources + + +class _NativeStartupRecord: + def __init__(self, seal: _NativeStartupSeal, state: NativeStartupSnapshot, resources: _NativeStartupResources) -> None: + self.seal = seal + self.state = state + self.state_key = _native_startup_state_key(state) + self.resources = resources + self.operation: object | None = None + self.cleanup_operation: object | None = None + self.cleanup_attempts: list[object] = [] + + +class _NativeStartupAuthToken: + def __init__(self, record: _NativeStartupRecord, intent: int, terminal: int, deadline: float) -> None: + self.record = record + self.resources = record.resources + self.seal = record.seal + self.intent = intent + self.terminal = terminal + self.deadline = deadline + self.cleanup = False + self.failed = False + self.anchor = (intent, terminal, deadline, False) + self.outcome: tuple | None = None + + +class _NativeStartupCleanupToken(_NativeStartupAuthToken): + def __init__(self, record: _NativeStartupRecord, intent: int, terminal: int, deadline: float) -> None: + super().__init__(record, intent, terminal, deadline) + self.cleanup = True + self.anchor = (intent, terminal, deadline, True) + + +class _NativeStartupCallFrame: + def __init__(self, name: str, function: object, arguments: tuple, outputs: tuple) -> None: + self.name = name + self.function = function + self.arguments = arguments + self.outputs = outputs + self.entered = False + self.returned = False + self.result: object | None = None + + +class _NativeStartupCallbackBinding: + def __init__(self, record: _NativeStartupRecord, token: _NativeStartupAuthToken, kind: str) -> None: + self.record = record + self.token = token + self.kind = kind + self.cell = ctypes.c_uint64(record.seal.owner.owner_id) + self.userdata = ctypes.c_void_p(ctypes.addressof(self.cell)) + self.callback: object | None = None + self.slot: _NativeStartupReference | None = None + self.anchor = (record, token, record.resources, record.seal, self.userdata.value, kind) + + +class _NativeStartupPendingReply: + def __init__(self, destination: str, result_kind: str, binding: _NativeStartupCallbackBinding) -> None: + self.destination = destination + self.result_kind = result_kind + self.binding = binding + self.cookie: int | None = None + self.reply: dict | None = None + self.message: _NativeStartupReference | None = None + self.slot: _NativeStartupReference | None = None + self.inputs: list[object] = [] + + +def _native_startup_creator() -> int: + pid = os.getpid() + if type(pid) is not int or pid != _NATIVE_STARTUP_CREATOR_PID: + _native_startup_fail("forked_owner") + return pid + + +def _native_startup_validate_record(record: _NativeStartupRecord, *, effect: bool) -> None: + _native_startup_creator() + if type(record) is not _NativeStartupRecord or type(record.seal) is not _NativeStartupSeal: + _native_startup_fail("invalid_owner") + seal = record.seal + _native_startup_validate_owner(seal.owner) + if (seal.owner.version, seal.owner.owner_id, seal.owner.creator_pid) != seal.owner_values or seal.owner.creator_pid != _NATIVE_STARTUP_CREATOR_PID: + _native_startup_fail("invalid_owner") + if _NATIVE_STARTUP_RECORDS.get(seal.owner.owner_id) is not record: + _native_startup_fail("invalid_owner") + if type(record.resources) is not _NativeStartupResources or record.resources is not seal.resources: + _native_startup_fail("native_resource_drift") + if _native_startup_request_key(seal.request) != seal.request_key: + _native_startup_fail("invalid_input") + if _native_startup_state_key(record.state) != record.state_key or record.state.owner is not seal.owner or record.state.original_deadline != seal.original_deadline: + _native_startup_fail("invalid_input") + if _native_startup_request_key(record.state.request) != seal.request_key: + _native_startup_fail("invalid_input") + if type(record.cleanup_attempts) is not list or len(record.cleanup_attempts) > 16: + _native_startup_fail("native_resource_drift") + if record.operation is not None: + _native_startup_validate_token(record, record.operation, cleanup=False) + if record.cleanup_operation is not None: + _native_startup_validate_token(record, record.cleanup_operation, cleanup=True) + if not record.cleanup_attempts or record.cleanup_attempts[-1] is not record.cleanup_operation: + _native_startup_fail("native_resource_drift") + for token in record.cleanup_attempts: + _native_startup_validate_token(record, token, cleanup=True) + resources = record.resources + if type(resources.references) is not list or type(resources.frames) is not list or type(resources.directory_fds) is not list or type(resources.callbacks) is not list or type(resources.events) is not list or type(resources.seen_serials) is not set: + _native_startup_fail("native_resource_drift") + for value in (resources.active_calls, resources.active_callbacks, resources.evidence_bytes, resources.callback_count, resources.copy_bytes, resources.iterations): + _native_startup_integer(value) + if type(resources.fd_release_observed) is not bool or type(resources.bus_closed) is not bool or type(resources.transfer) is not str or resources.transfer not in ("none", "python_owned", "detach_entered", "caller_owned", "attempted_unknown", "bus_owned", "release_entered", "release_unobserved", "released"): + _native_startup_fail("native_resource_drift") + if resources.callback_error is not None and (type(resources.callback_error) is not str or resources.callback_error not in _NATIVE_STARTUP_CODES): + _native_startup_fail("native_resource_drift") + for reference in resources.references: + if type(reference) is not _NativeStartupReference or type(reference.role) is not str or reference.role != reference.original_role or reference.role not in ("bus", "filter", "method_slot", "method_message", "process_message") or type(reference.out) is not ctypes.c_void_p or reference.out is not reference.original_out or type(reference.state) is not str or reference.state not in ("acquisition_pending", "owned", "acquisition_unobserved", "release_entered", "released", "release_unobserved") or (reference.pointer is not None and (type(reference.pointer) is not int or not 0 < reference.pointer <= _NATIVE_STARTUP_U64)): + _native_startup_fail("native_resource_drift") + if effect and (threading.current_thread() is not seal.thread or threading.get_ident() != seal.thread_ident or threading.get_native_id() != seal.native_id): + _native_startup_fail("wrong_thread") + + +def _native_startup_validate_token(record: _NativeStartupRecord, token: object, *, cleanup: bool) -> None: + expected = _NativeStartupCleanupToken if cleanup else _NativeStartupAuthToken + if type(token) is not expected or token.record is not record or token.seal is not record.seal or token.resources is not record.resources: + _native_startup_fail("native_resource_drift") + _native_startup_integer(token.intent, positive=True) + _native_startup_integer(token.terminal, positive=True) + _native_startup_float(token.deadline) + if type(token.cleanup) is not bool or token.cleanup is not cleanup or type(token.failed) is not bool or type(token.anchor) is not tuple or len(token.anchor) != 4 or tuple(type(value) for value in token.anchor) != (int, int, float, bool) or token.anchor != (token.intent, token.terminal, token.deadline, cleanup) or token.terminal != token.intent + 1: + _native_startup_fail("native_resource_drift") + if not cleanup and token.deadline != record.seal.original_deadline: + _native_startup_fail("native_resource_drift") + if token.outcome is not None: + outcome = token.outcome + if type(outcome) is not tuple or len(outcome) != 5 or type(outcome[0]) is not int or outcome[0] != token.terminal or (outcome[1] is not None and (type(outcome[1]) is not str or outcome[1] not in _NATIVE_STARTUP_CODES)) or type(outcome[2]) is not float or not math.isfinite(outcome[2]) or type(outcome[3]) is not bool or type(outcome[4]) is not bool: + _native_startup_fail("native_resource_drift") + + +def _native_startup_enter(owner: NativeStartupOwner | None, *, effect: bool) -> _NativeStartupRecord | None: + global _NATIVE_STARTUP_OPERATION + _native_startup_creator() + if not _NATIVE_STARTUP_LOCK.acquire(blocking=False): + _native_startup_fail("busy") + try: + if _NATIVE_STARTUP_OPERATION is not None: + _native_startup_fail("busy") + record = None + if owner is not None: + _native_startup_validate_owner(owner) + record = _NATIVE_STARTUP_RECORDS.get(owner.owner_id) + if type(record) is not _NativeStartupRecord or record.seal.owner is not owner: + _native_startup_fail("invalid_owner") + try: + _native_startup_validate_record(record, effect=effect) + except (TypeError, ValueError, AttributeError, KeyError, OverflowError): + _native_startup_fail("invalid_input") + if record.resources.active_calls or record.resources.active_callbacks: + _native_startup_fail("busy") + _NATIVE_STARTUP_OPERATION = record if record is not None else object() + return record + finally: + _NATIVE_STARTUP_LOCK.release() + + +def _native_startup_leave(record: _NativeStartupRecord | None) -> None: + global _NATIVE_STARTUP_OPERATION + _native_startup_creator() + if not _NATIVE_STARTUP_LOCK.acquire(blocking=False): + _native_startup_fail("busy") + try: + if record is not None and (record.resources.active_calls or record.resources.active_callbacks): + return + _NATIVE_STARTUP_OPERATION = None + finally: + _NATIVE_STARTUP_LOCK.release() + + +def _native_startup_clock(record: _NativeStartupRecord | None = None) -> float: + now = time.monotonic() + if type(now) is not float or not math.isfinite(now): + _native_startup_fail("clock_regressed") + if record is not None and now < record.state.last_observed_now: + _native_startup_fail("clock_regressed") + return now + + +def _native_startup_commit(record: _NativeStartupRecord, **changes: object) -> None: + values = {name: getattr(record.state, name) for name in NativeStartupSnapshot.__dataclass_fields__} + values.update(changes) + state = NativeStartupSnapshot(**values) + key = _native_startup_state_key(state) + _native_startup_validate_record(record, effect=False) + record.state = state + record.state_key = key + + +def _native_startup_snapshot_copy(record: _NativeStartupRecord) -> NativeStartupSnapshot: + _native_startup_validate_record(record, effect=False) + values = {name: getattr(record.state, name) for name in NativeStartupSnapshot.__dataclass_fields__} + values["owner"] = record.seal.owner + values["request"] = _native_startup_copy_request(record.seal.request) + if values["context"] is not None: + context = values["context"] + values["context"] = StartupBusContext(context.version, context.bus_id, context.manager_unique_name, context.client_unique_name, context.manager_uid) + return NativeStartupSnapshot(**values) + + +def retain_native_startup(request: StartupNativeRequest, *, deadline: float) -> NativeStartupOwner: + global _NATIVE_STARTUP_NEXT_OWNER_ID + _native_startup_creator() + _native_startup_validate_request(request) + _native_startup_float(deadline) + _native_startup_enter(None, effect=False) + try: + now = _native_startup_clock() + if deadline <= now: + _native_startup_fail("deadline_expired") + if len(_NATIVE_STARTUP_RECORDS) >= 16: + _native_startup_fail("capacity_exhausted") + if _NATIVE_STARTUP_NEXT_OWNER_ID > _NATIVE_STARTUP_U64: + _native_startup_fail("sequence_exhausted") + owner = NativeStartupOwner(1, _NATIVE_STARTUP_NEXT_OWNER_ID, _NATIVE_STARTUP_CREATOR_PID) + resources = _NativeStartupResources() + seal = _NativeStartupSeal(owner, request, deadline, resources) + state = NativeStartupSnapshot(1, owner, _native_startup_copy_request(seal.request), deadline, now, 0, 0, + None, False, None, "prepared", None, None, None, False, False, False, + False, False, True, True, None) + record = _NativeStartupRecord(seal, state, resources) + if _native_startup_request_key(request) != seal.request_key: + _native_startup_fail("invalid_input") + _NATIVE_STARTUP_RECORDS[owner.owner_id] = record + _NATIVE_STARTUP_NEXT_OWNER_ID += 1 + return owner + finally: + _native_startup_leave(None) + + +def native_startup_snapshot(owner: NativeStartupOwner) -> NativeStartupSnapshot: + _native_startup_validate_owner(owner) + record = _native_startup_enter(owner, effect=False) + try: + now = _native_startup_clock(record) + _native_startup_validate_record(record, effect=False) + changes: dict[str, object] = {"last_observed_now": now} + if now >= record.seal.original_deadline and record.state.phase in ("prepared", "subscribed"): + changes.update(phase="terminal_unknown", reason="deadline_expired") + _native_startup_commit(record, **changes) + return _native_startup_snapshot_copy(record) + finally: + _native_startup_leave(record) + + +class _NativeStartupBackend: + def __init__(self, library: object) -> None: + pointer = ctypes.c_void_p + out_pointer = ctypes.POINTER(pointer) + out_u64 = ctypes.POINTER(ctypes.c_uint64) + self.handler = ctypes.CFUNCTYPE(ctypes.c_int, pointer, pointer, pointer) + rows = ( + ("sd_bus_new", (out_pointer,), ctypes.c_int), + ("sd_bus_set_fd", (pointer, ctypes.c_int, ctypes.c_int), ctypes.c_int), + ("sd_bus_set_bus_client", (pointer, ctypes.c_int), ctypes.c_int), + ("sd_bus_set_anonymous", (pointer, ctypes.c_int), ctypes.c_int), + ("sd_bus_negotiate_fds", (pointer, ctypes.c_int), ctypes.c_int), + ("sd_bus_set_exit_on_disconnect", (pointer, ctypes.c_int), ctypes.c_int), + ("sd_bus_start", (pointer,), ctypes.c_int), + ("sd_bus_is_ready", (pointer,), ctypes.c_int), + ("sd_bus_process", (pointer, out_pointer), ctypes.c_int), + ("sd_bus_get_fd", (pointer,), ctypes.c_int), + ("sd_bus_get_events", (pointer,), ctypes.c_int), + ("sd_bus_get_timeout", (pointer, out_u64), ctypes.c_int), + ("sd_bus_get_bus_id", (pointer, ctypes.POINTER(_NativeStartupId128)), ctypes.c_int), + ("sd_bus_get_unique_name", (pointer, out_pointer), ctypes.c_int), + ("sd_bus_call_async", (pointer, out_pointer, pointer, self.handler, pointer, ctypes.c_uint64), ctypes.c_int), + ("sd_bus_add_filter", (pointer, out_pointer, self.handler, pointer), ctypes.c_int), + ("sd_bus_message_new_method_call", (pointer, out_pointer, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p, ctypes.c_char_p), ctypes.c_int), + ("sd_bus_message_set_auto_start", (pointer, ctypes.c_int), ctypes.c_int), + ("sd_bus_message_set_allow_interactive_authorization", (pointer, ctypes.c_int), ctypes.c_int), + ("sd_bus_message_append_basic", (pointer, ctypes.c_char, pointer), ctypes.c_int), + ("sd_bus_message_get_cookie", (pointer, out_u64), ctypes.c_int), + ("sd_bus_message_get_reply_cookie", (pointer, out_u64), ctypes.c_int), + ("sd_bus_message_get_sender", (pointer,), pointer), + ("sd_bus_message_get_destination", (pointer,), pointer), + ("sd_bus_message_get_type", (pointer, ctypes.POINTER(ctypes.c_uint8)), ctypes.c_int), + ("sd_bus_message_get_signature", (pointer, ctypes.c_int), pointer), + ("sd_bus_message_get_path", (pointer,), pointer), + ("sd_bus_message_get_interface", (pointer,), pointer), + ("sd_bus_message_get_member", (pointer,), pointer), + ("sd_bus_message_read_basic", (pointer, ctypes.c_char, pointer), ctypes.c_int), + ("sd_bus_message_enter_container", (pointer, ctypes.c_char, ctypes.c_char_p), ctypes.c_int), + ("sd_bus_message_exit_container", (pointer,), ctypes.c_int), + ("sd_bus_message_at_end", (pointer, ctypes.c_int), ctypes.c_int), + ("sd_bus_message_unref", (pointer,), pointer), + ("sd_bus_slot_unref", (pointer,), pointer), + ("sd_bus_close", (pointer,), None), + ("sd_bus_unref", (pointer,), pointer), + ) + self.library = library + self.functions: dict[str, tuple[object, tuple, object, object]] = {} + for name, arguments, result in rows: + function = getattr(library, name) + if not callable(function): + _native_startup_fail("backend_abi_unproven") + function.argtypes = arguments + function.restype = result + self.functions[name] = (function, arguments, result, getattr(function, "errcheck", None)) + for name in ("sd_bus_send", "sd_bus_message_ref", "sd_bus_message_open_container", "sd_bus_message_close_container", "sd_bus_add_match_async"): + if not callable(getattr(library, name)): + _native_startup_fail("backend_abi_unproven") + self.captured = tuple((name, *binding) for name, binding in self.functions.items()) + + +def _native_startup_stat_pin(metadata: object) -> tuple: + return (metadata.st_dev, metadata.st_ino, metadata.st_mode, metadata.st_uid, metadata.st_gid, + metadata.st_size, metadata.st_mtime_ns, metadata.st_ctime_ns, metadata.st_nlink) + + +def _native_startup_loader_names() -> None: + for name in os.environ: + if type(name) is not str: + _native_startup_fail("invalid_input") + if name.startswith("LD_") or name == "GLIBC_TUNABLES": + _native_startup_fail("loader_override_present") + + +def _native_startup_boundary(record: _NativeStartupRecord, token: _NativeStartupAuthToken, *, clock: bool = True) -> float: + _native_startup_validate_record(record, effect=True) + if _NATIVE_STARTUP_OPERATION is not record or token.record is not record or token.seal is not record.seal or token.resources is not record.resources: + _native_startup_fail("native_resource_drift") + current = record.cleanup_operation if token.cleanup else record.operation + if current is not token or type(token) is not (_NativeStartupCleanupToken if token.cleanup else _NativeStartupAuthToken): + _native_startup_fail("native_resource_drift") + _native_startup_integer(token.intent) + _native_startup_integer(token.terminal) + _native_startup_float(token.deadline) + if type(token.cleanup) is not bool or (token.intent, token.terminal, token.deadline, token.cleanup) != token.anchor: + _native_startup_fail("native_resource_drift") + expected = record.state.cleanup_sequence if token.cleanup else record.state.operation_sequence + if expected != token.intent or token.terminal != token.intent + 1 or token.deadline != (record.state.cleanup_deadline if token.cleanup else record.seal.original_deadline): + _native_startup_fail("native_resource_drift") + resources = record.resources + for value in (resources.active_calls, resources.active_callbacks, resources.evidence_bytes, resources.callback_count, resources.copy_bytes, resources.iterations): + _native_startup_integer(value) + if resources.socket is not None and resources.socket is not record.seal.socket: + _native_startup_fail("native_resource_drift") + if resources.bus is not None and resources.bus is not record.seal.bus: + _native_startup_fail("native_resource_drift") + if resources.backend is not None: + _native_startup_validate_backend(record) + if resources.raw_fd is not None and not resources.fd_release_observed: + _native_startup_integer(resources.raw_fd) + if type(resources.raw_pin) is not tuple or len(resources.raw_pin) != 5: + _native_startup_fail("native_resource_drift") + for value in resources.raw_pin: + _native_startup_integer(value) + try: + metadata = os.fstat(resources.raw_fd) + except OSError: + _native_startup_fail("native_resource_drift") + if _native_startup_stat_pin(metadata)[:5] != resources.raw_pin: + _native_startup_fail("native_resource_drift") + if resources.callback_error is not None and not token.cleanup: + _native_startup_fail(resources.callback_error) + if not clock: + return record.state.last_observed_now + now = _native_startup_clock(record) + _native_startup_validate_record(record, effect=True) + if _NATIVE_STARTUP_OPERATION is not record or (record.cleanup_operation if token.cleanup else record.operation) is not token: + _native_startup_fail("native_resource_drift") + if now >= token.deadline: + _native_startup_fail("deadline_expired") + _native_startup_commit(record, last_observed_now=now) + return now + + +def _native_startup_validate_backend(record: _NativeStartupRecord) -> _NativeStartupBackend: + backend = record.resources.backend + if type(backend) is not _NativeStartupBackend or backend is not record.seal.backend or backend.library is not record.resources.library: + _native_startup_fail("native_resource_drift") + if type(backend.functions) is not dict or len(backend.functions) != 37 or type(backend.captured) is not tuple or len(backend.captured) != 37: + _native_startup_fail("backend_abi_unproven") + try: + for name, function, arguments, result, checker in backend.captured: + if type(name) is not str or backend.functions.get(name) != (function, arguments, result, checker) or getattr(backend.library, name) is not function or tuple(getattr(function, "argtypes", ())) != arguments or getattr(function, "restype", object()) is not result or getattr(function, "errcheck", None) is not checker: + _native_startup_fail("backend_abi_unproven") + except (TypeError, ValueError, AttributeError, KeyError): + _native_startup_fail("backend_abi_unproven") + return backend + + +def _native_startup_pointer(reference: _NativeStartupReference) -> ctypes.c_void_p: + if type(reference) is not _NativeStartupReference or type(reference.role) is not str or reference.role != reference.original_role or reference.state != "owned" or type(reference.pointer) is not int or not 0 < reference.pointer <= _NATIVE_STARTUP_U64 or type(reference.out) is not ctypes.c_void_p or reference.out is not reference.original_out or type(reference.out.value) is not int or reference.out.value != reference.pointer: + _native_startup_fail("native_pointer_unproven") + return ctypes.c_void_p(reference.pointer) + + +def _native_startup_call(record: _NativeStartupRecord, token: _NativeStartupAuthToken, name: str, arguments: tuple, + *, acquire: _NativeStartupReference | None = None, + release: _NativeStartupReference | None = None) -> object: + resources = record.resources + backend = _native_startup_validate_backend(record) + if type(name) is not str or name not in backend.functions: + _native_startup_fail("invalid_input") + if token.cleanup and name not in ("sd_bus_close", "sd_bus_message_unref", "sd_bus_slot_unref", "sd_bus_unref"): + _native_startup_fail("invalid_input") + function = backend.functions[name][0] + outputs = () if acquire is None else (acquire,) + frame = _NativeStartupCallFrame(name, function, arguments, outputs) + resources.frames.append(frame) + if acquire is not None: + resources.references.append(acquire) + _native_startup_commit(record, native_objects_released=False) + if release is not None and release.state != "owned": + _native_startup_fail("native_release_unobserved") + _native_startup_boundary(record, token) + if backend.functions[name][0] is not function: + _native_startup_fail("backend_abi_unproven") + if release is not None: + release.state = "release_entered" + release.release_frame = frame + if name == "sd_bus_set_fd": + resources.transfer = "attempted_unknown" + frame.entered = True + resources.active_calls += 1 + try: + result = function(*arguments) + frame.result = result + frame.returned = True + resources.last_frame = frame + if acquire is not None: + pointer = acquire.out.value + valid = type(result) is int and (result == 0 if name in ("sd_bus_new", "sd_bus_message_new_method_call", "sd_bus_add_filter") else result >= 0) + if name == "sd_bus_process": + valid = type(result) is int and result > 0 + if valid and type(pointer) is int and 0 < pointer <= _NATIVE_STARTUP_U64: + acquire.pointer, acquire.state = pointer, "owned" + elif pointer is None and name == "sd_bus_process" and type(result) is int and result >= 0: + acquire.state = "released" + else: + acquire.pointer, acquire.state = pointer, "acquisition_unobserved" + if name == "sd_bus_set_fd": + if type(result) is int and result == 0: + resources.transfer = "bus_owned" + elif type(result) is int and result < 0: + resources.transfer = "caller_owned" + if name == "sd_bus_close" and result is None: + resources.bus_closed = True + if resources.transfer == "bus_owned": + resources.fd_release_observed = True + resources.transfer = "released" + if release is not None: + release.state = "released" if result is None else "release_unobserved" + except BaseException: + if release is not None: + release.state = "release_unobserved" + raise + finally: + # A lost native return cannot authorize reuse or cleanup of its resources. + if frame.returned: + resources.active_calls -= 1 + _native_startup_boundary(record, token) + return result + + +def _native_startup_result(result: object, *, value: int | None = None, nonnegative: bool = False) -> int: + if type(result) is not int or (value is not None and result != value) or (nonnegative and result < 0): + _native_startup_fail("native_call_failed") + return result + + +def _native_startup_open_directory(record: _NativeStartupRecord, token: _NativeStartupAuthToken, path: str, uid: int) -> int: + _native_startup_boundary(record, token) + descriptor = os.open(path, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW | os.O_CLOEXEC) + entry = [descriptor, None, "owned"] + record.resources.directory_fds.append(entry) + metadata = os.fstat(descriptor) + entry[1] = _native_startup_stat_pin(metadata) + if not stat.S_ISDIR(metadata.st_mode) or metadata.st_uid != uid or metadata.st_mode & 0o022: + _native_startup_fail("endpoint_pin_mismatch") + current = os.stat(path, follow_symlinks=False) + if _native_startup_stat_pin(current) != entry[1]: + _native_startup_fail("endpoint_pin_mismatch") + _native_startup_boundary(record, token) + return descriptor + + +def _native_startup_close_directory(record: _NativeStartupRecord, token: _NativeStartupAuthToken, entry: list) -> None: + if entry[2] == "released": + return + if entry[2] != "owned" or entry[1] is None: + _native_startup_fail("cleanup_unresolved") + _native_startup_boundary(record, token) + if _native_startup_stat_pin(os.fstat(entry[0])) != entry[1]: + _native_startup_fail("native_resource_drift") + entry[2] = "release_entered" + os.close(entry[0]) + entry[2] = "released" + _native_startup_boundary(record, token) + + +def _native_startup_load_backend(record: _NativeStartupRecord, token: _NativeStartupAuthToken) -> None: + request = record.seal.request + pin = request.backend_pin + if (pin.canonical_path, pin.sha256, pin.stat_pin) != (_NATIVE_STARTUP_LIBRARY, _NATIVE_STARTUP_LIBRARY_SHA, _NATIVE_STARTUP_LIBRARY_STAT): + _native_startup_fail("backend_pin_mismatch") + _native_startup_loader_names() + if sys.platform != "linux" or sys.byteorder != "little" or ctypes.sizeof(ctypes.c_void_p) != 8 or ctypes.sizeof(ctypes.c_uint64) != 8 or ctypes.sizeof(ctypes.c_uint32) != 4 or ctypes.sizeof(ctypes.c_uint8) != 1 or ctypes.sizeof(_NativeStartupId128) != 16 or ctypes.alignment(_NativeStartupId128) != 8: + _native_startup_fail("backend_abi_unproven") + paths = ("/", "/usr", "/usr/lib", "/usr/lib/x86_64-linux-gnu") + for path in paths: + _native_startup_open_directory(record, token, path, 0) + _native_startup_boundary(record, token) + descriptor = os.open(pin.canonical_path, os.O_RDONLY | os.O_NOFOLLOW | os.O_CLOEXEC) + record.resources.library_fd = descriptor + entry = [descriptor, None, "owned"] + record.resources.directory_fds.append(entry) + entry[1] = _native_startup_stat_pin(os.fstat(descriptor)) + if entry[1] != pin.stat_pin: + _native_startup_fail("backend_pin_mismatch") + digest = hashlib.sha256() + total = 0 + while True: + _native_startup_boundary(record, token) + block = os.read(descriptor, min(65536, 4194305 - total)) + if not block: + break + total += len(block) + if total > 4194304: + _native_startup_fail("backend_pin_mismatch") + digest.update(block) + if total != pin.stat_pin[5] or digest.hexdigest() != pin.sha256 or _native_startup_stat_pin(os.fstat(descriptor)) != pin.stat_pin or _native_startup_stat_pin(os.stat(pin.canonical_path, follow_symlinks=False)) != pin.stat_pin: + _native_startup_fail("backend_pin_mismatch") + _native_startup_loader_names() + _native_startup_boundary(record, token) + library = ctypes.CDLL(pin.canonical_path, mode=os.RTLD_LOCAL | os.RTLD_NOW) + record.resources.library = library + _native_startup_boundary(record, token) + if _native_startup_stat_pin(os.stat(pin.canonical_path, follow_symlinks=False)) != pin.stat_pin: + _native_startup_fail("backend_pin_mismatch") + for directory, path in zip(record.resources.directory_fds[:-1], paths): + if _native_startup_stat_pin(os.fstat(directory[0])) != directory[1] or _native_startup_stat_pin(os.stat(path, follow_symlinks=False)) != directory[1]: + _native_startup_fail("backend_pin_mismatch") + backend = _NativeStartupBackend(library) + record.resources.backend = backend + record.seal.backend = backend + _native_startup_boundary(record, token) + for directory in record.resources.directory_fds: + _native_startup_close_directory(record, token, directory) + + +def _native_startup_poll(record: _NativeStartupRecord, token: _NativeStartupAuthToken, descriptor: int, events: int, timeout: float) -> None: + resources = record.resources + if resources.iterations >= 4096: + _native_startup_fail("evidence_overflow") + resources.iterations += 1 + now = _native_startup_boundary(record, token) + remaining = min(token.deadline - now, timeout) + if not math.isfinite(remaining) or remaining < 0: + _native_startup_fail("deadline_expired") + poller = select.poll() + poller.register(descriptor, events) + returned = poller.poll(min(2147483647, max(0, int(remaining * 1000)))) + _native_startup_boundary(record, token) + for fd, flags in returned: + if fd != descriptor or flags & (select.POLLERR | select.POLLHUP | select.POLLNVAL): + _native_startup_fail("transport_disconnected") + + +def _native_startup_open_endpoint(record: _NativeStartupRecord, token: _NativeStartupAuthToken) -> None: + seal, resources = record.seal, record.resources + if type(seal.uid) is not int or seal.uid <= 0 or seal.uid != seal.euid or os.getuid() != seal.uid or os.geteuid() != seal.euid: + _native_startup_fail("peer_auth_failed") + root = "/run/user/" + str(seal.uid) + runtime = _native_startup_open_directory(record, token, root, seal.uid) + if stat.S_IMODE(os.fstat(runtime).st_mode) != 0o700: + _native_startup_fail("endpoint_pin_mismatch") + endpoint = os.stat("bus", dir_fd=runtime, follow_symlinks=False) + endpoint_pin = _native_startup_stat_pin(endpoint) + if not stat.S_ISSOCK(endpoint.st_mode) or endpoint.st_uid != seal.uid: + _native_startup_fail("endpoint_pin_mismatch") + _native_startup_boundary(record, token) + connection = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM | socket.SOCK_NONBLOCK | socket.SOCK_CLOEXEC) + resources.socket = connection + record.seal.socket = connection + descriptor = connection.fileno() + resources.raw_fd = descriptor + resources.raw_pin = _native_startup_stat_pin(os.fstat(descriptor))[:5] + resources.transfer = "python_owned" + _native_startup_commit(record, connection_closed=False) + _native_startup_boundary(record, token) + result = connection.connect_ex(root + "/bus") + if type(result) is not int or result not in (0, 4, 11, 114, 115): + _native_startup_fail("endpoint_unavailable") + if result: + _native_startup_poll(record, token, descriptor, select.POLLOUT, token.deadline - record.state.last_observed_now) + if connection.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) != 0: + _native_startup_fail("endpoint_unavailable") + raw_peer = connection.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12) + if type(raw_peer) is not bytes or len(raw_peer) != 12: + _native_startup_fail("peer_auth_failed") + peer = struct.unpack("3i", raw_peer) + if peer[0] <= 0 or peer[1] != seal.uid or peer[2] < 0: + _native_startup_fail("peer_auth_failed") + resources.peer = peer + _native_startup_commit(record, peer_credentials=peer) + if _native_startup_stat_pin(os.stat("bus", dir_fd=runtime, follow_symlinks=False)) != endpoint_pin: + _native_startup_fail("endpoint_pin_mismatch") + _native_startup_boundary(record, token) + resources.transfer = "detach_entered" + detached = connection.detach() + if type(detached) is not int or detached != descriptor: + _native_startup_fail("native_resource_drift") + resources.transfer = "caller_owned" + _native_startup_boundary(record, token) + out = ctypes.c_void_p() + bus = _NativeStartupReference("bus", out) + resources.bus = bus + record.seal.bus = bus + _native_startup_result(_native_startup_call(record, token, "sd_bus_new", (ctypes.byref(out),), acquire=bus), value=0) + pointer = _native_startup_pointer(bus) + _native_startup_result(_native_startup_call(record, token, "sd_bus_set_fd", (pointer, descriptor, descriptor)), value=0) + for name, value in (("sd_bus_set_bus_client", 1), ("sd_bus_set_anonymous", 0), ("sd_bus_negotiate_fds", 0), ("sd_bus_set_exit_on_disconnect", 0)): + _native_startup_result(_native_startup_call(record, token, name, (pointer, value)), value=0) + _native_startup_result(_native_startup_call(record, token, "sd_bus_start", (pointer,)), nonnegative=True) + + +def _native_startup_unref(record: _NativeStartupRecord, token: _NativeStartupAuthToken, reference: _NativeStartupReference) -> None: + if reference.state == "released": + return + name = "sd_bus_unref" if reference.role == "bus" else ("sd_bus_slot_unref" if reference.role in ("filter", "method_slot") else "sd_bus_message_unref") + pointer = _native_startup_pointer(reference) + result = _native_startup_call(record, token, name, (pointer,), release=reference) + if result is not None: + _native_startup_fail("native_release_unobserved") + + +def _native_startup_process_once(record: _NativeStartupRecord, token: _NativeStartupAuthToken) -> None: + resources = record.resources + if resources.iterations >= 4096: + _native_startup_fail("evidence_overflow") + resources.iterations += 1 + bus = _native_startup_pointer(resources.bus) + out = ctypes.c_void_p() + reference = _NativeStartupReference("process_message", out) + result = _native_startup_call(record, token, "sd_bus_process", (bus, ctypes.byref(out)), acquire=reference) + _native_startup_result(result, nonnegative=True) + if reference.state == "owned": + _native_startup_unref(record, token, reference) + elif reference.state != "released": + _native_startup_fail("native_pointer_unproven") + if result > 0: + return + descriptor = _native_startup_result(_native_startup_call(record, token, "sd_bus_get_fd", (bus,)), nonnegative=True) + if descriptor != resources.raw_fd: + _native_startup_fail("native_resource_drift") + events = _native_startup_result(_native_startup_call(record, token, "sd_bus_get_events", (bus,)), nonnegative=True) + if events & ~(select.POLLIN | select.POLLOUT | select.POLLPRI | select.POLLERR | select.POLLHUP | select.POLLNVAL): + _native_startup_fail("malformed_evidence") + timeout = ctypes.c_uint64() + timed = _native_startup_result(_native_startup_call(record, token, "sd_bus_get_timeout", (bus, ctypes.byref(timeout)))) + if timed not in (0, 1): + _native_startup_fail("malformed_evidence") + now = _native_startup_boundary(record, token) + seconds = token.deadline - now + if timed == 1 and timeout.value != _NATIVE_STARTUP_U64: + seconds = min(seconds, max(0.0, timeout.value / 1000000.0 - now)) + _native_startup_poll(record, token, descriptor, events, seconds) + + +def _native_startup_timeout_us(record: _NativeStartupRecord, token: _NativeStartupAuthToken) -> int: + now = _native_startup_boundary(record, token) + residual = token.deadline - now + scaled = residual * 1000000.0 + if not math.isfinite(scaled) or scaled > _NATIVE_STARTUP_U64: + _native_startup_fail("invalid_input") + value = math.floor(scaled) + if value < 1: + _native_startup_fail("deadline_expired") + return value + + +def _native_startup_scan_string(record: _NativeStartupRecord, token: _NativeStartupAuthToken, pointer: object, + limit: int, *, nullable: bool = False, empty: bool = False) -> str | None: + frame = record.resources.last_frame + if type(frame) is not _NativeStartupCallFrame or not frame.returned: + _native_startup_fail("native_pointer_unproven") + getters = ("sd_bus_get_unique_name", "sd_bus_message_get_sender", "sd_bus_message_get_destination", "sd_bus_message_get_signature", "sd_bus_message_get_path", "sd_bus_message_get_interface", "sd_bus_message_get_member", "sd_bus_message_read_basic") + if frame.name not in getters: + _native_startup_fail("native_pointer_unproven") + if frame.name in ("sd_bus_get_unique_name", "sd_bus_message_read_basic"): + cell = getattr(frame.arguments[-1], "_obj", None) + observed = cell.value if type(cell) is ctypes.c_void_p else object() + else: + observed = frame.result + if pointer != observed or type(pointer) is not type(observed): + _native_startup_fail("native_pointer_unproven") + if pointer is None: + if nullable: + return None + _native_startup_fail("native_pointer_unproven") + if type(pointer) is not int or not 0 < pointer <= _NATIVE_STARTUP_U64 or type(limit) is not int or not 0 <= limit <= 4096: + _native_startup_fail("native_pointer_unproven") + data = bytearray() + for index in range(limit + 1): + _native_startup_boundary(record, token) + if pointer > _NATIVE_STARTUP_U64 - index: + _native_startup_fail("native_pointer_unproven") + value = ctypes.c_ubyte.from_address(pointer + index).value + if type(value) is not int or not 0 <= value <= 255: + _native_startup_fail("native_pointer_unproven") + if value == 0: + if not data and not empty: + _native_startup_fail("malformed_evidence") + try: + return bytes(data).decode("ascii", "strict") + except UnicodeError: + _native_startup_fail("malformed_evidence") + if index == limit: + _native_startup_fail("native_string_unterminated") + if record.resources.evidence_bytes + record.resources.copy_bytes + 1 > 65536: + _native_startup_fail("evidence_overflow") + record.resources.copy_bytes += 1 + data.append(value) + _native_startup_fail("native_string_unterminated") + + +def _native_startup_cookie(record: _NativeStartupRecord, token: _NativeStartupAuthToken, message: ctypes.c_void_p, *, reply: bool = False) -> int: + out = ctypes.c_uint64() + name = "sd_bus_message_get_reply_cookie" if reply else "sd_bus_message_get_cookie" + _native_startup_result(_native_startup_call(record, token, name, (message, ctypes.byref(out))), value=0) + if not 0 < out.value <= _NATIVE_STARTUP_U32: + _native_startup_fail("malformed_evidence") + return out.value + + +def _native_startup_header_string(record: _NativeStartupRecord, token: _NativeStartupAuthToken, message: ctypes.c_void_p, + name: str, *, nullable: bool = False, empty: bool = False) -> str | None: + arguments = (message, 1) if name == "sd_bus_message_get_signature" else (message,) + pointer = _native_startup_call(record, token, name, arguments) + return _native_startup_scan_string(record, token, pointer, 4096 if name == "sd_bus_message_get_path" else 255, nullable=nullable, empty=empty) + + +def _native_startup_message_type(record: _NativeStartupRecord, token: _NativeStartupAuthToken, message: ctypes.c_void_p) -> int: + out = ctypes.c_uint8() + _native_startup_result(_native_startup_call(record, token, "sd_bus_message_get_type", (message, ctypes.byref(out))), value=0) + if out.value not in (1, 2, 3, 4): + _native_startup_fail("malformed_evidence") + return out.value + + +def _native_startup_read_string(record: _NativeStartupRecord, token: _NativeStartupAuthToken, message: ctypes.c_void_p, + *, limit: int = 255, empty: bool = False) -> str: + out = ctypes.c_void_p() + _native_startup_result(_native_startup_call(record, token, "sd_bus_message_read_basic", (message, b"s", ctypes.byref(out))), value=1) + return _native_startup_scan_string(record, token, out.value, limit, empty=empty) + + +def _native_startup_evidence_size(value: object) -> int: + if value is None: + return 4 + if type(value) is bool: + return 4 if value else 5 + if type(value) is int: + return len(str(value)) + if type(value) is str: + size = 2 + for character in value: + number = ord(character) + size += 2 if character in ('"', "\\", "\b", "\f", "\n", "\r", "\t") else (6 if number < 32 or 127 <= number <= 65535 else (12 if number > 65535 else 1)) + return size + if type(value) is dict: + if any(type(key) is not str for key in value): + _native_startup_fail("malformed_evidence") + return 2 + max(0, len(value) - 1) + sum(_native_startup_evidence_size(key) + 1 + _native_startup_evidence_size(item) for key, item in value.items()) + if type(value) in (tuple, list): + return 2 + max(0, len(value) - 1) + sum(_native_startup_evidence_size(item) for item in value) + _native_startup_fail("malformed_evidence") + + +def _native_startup_evidence(record: _NativeStartupRecord, event: dict) -> None: + size = _native_startup_evidence_size(event) + if len(record.resources.events) >= 64 or record.resources.evidence_bytes + size > 65536: + _native_startup_fail("evidence_overflow") + serial_key = (event["sender"], event["serial"]) + if serial_key in record.resources.seen_serials: + _native_startup_fail("malformed_evidence") + copied = json.loads(json.dumps(event, sort_keys=True, separators=(",", ":"), ensure_ascii=True)) + record.resources.events.append(copied) + record.resources.seen_serials.add(serial_key) + record.resources.evidence_bytes += size + + +def _native_startup_callback_enter(binding: _NativeStartupCallbackBinding, message: object, userdata: object) -> ctypes.c_void_p: + if type(binding) is not _NativeStartupCallbackBinding or type(binding.anchor) is not tuple or len(binding.anchor) != 6: + _native_startup_fail("callback_failed") + record, token = binding.record, binding.token + if (binding.anchor[0] is not record or binding.anchor[1] is not token or binding.anchor[2] is not record.resources or binding.anchor[3] is not record.seal or type(binding.cell) is not ctypes.c_uint64 or type(binding.userdata) is not ctypes.c_void_p or type(binding.anchor[4]) is not int or binding.anchor[4] != binding.userdata.value or binding.anchor[4] != ctypes.addressof(binding.cell) or type(binding.kind) is not str or binding.kind != binding.anchor[5] or binding.kind not in ("filter", "reply")): + _native_startup_fail("callback_failed") + _native_startup_validate_record(record, effect=True) + if userdata != binding.userdata.value or type(userdata) is not int or type(message) is not int or not 0 < message <= _NATIVE_STARTUP_U64: + _native_startup_fail("callback_failed") + if binding not in record.resources.callbacks or record.resources.active_callbacks or not record.resources.active_calls: + _native_startup_fail("callback_failed") + if record.resources.callback_count >= 64 or record.resources.evidence_bytes >= 65536: + _native_startup_fail("evidence_overflow") + record.resources.callback_count += 1 + record.resources.copy_bytes = 0 + record.resources.active_callbacks += 1 + record.resources.borrowed_message = message + try: + _native_startup_boundary(record, token) + return ctypes.c_void_p(message) + except BaseException: + record.resources.borrowed_message = None + record.resources.active_callbacks -= 1 + raise + + +def _native_startup_callback_exit(resources: object, entered: bool) -> None: + try: + if entered and type(resources) is _NativeStartupResources: + resources.borrowed_message = None + if type(resources.active_callbacks) is not int or resources.active_callbacks <= 0: + resources.callback_error = "callback_failed" + else: + resources.active_callbacks -= 1 + except BaseException: + return + + +def _native_startup_latch_callback(resources: object, error: BaseException) -> None: + try: + if type(resources) is _NativeStartupResources: + resources.callback_error = _native_startup_error_code(error, "callback_failed") + except BaseException: + return + + +def _native_startup_error_code(error: BaseException, fallback: str) -> str: + if type(error) is not EnforcementUnavailable: + return fallback + arguments = error.args + if len(arguments) == 1 and type(arguments[0]) is str: + message = arguments[0] + if message.startswith("native_startup:") and message[15:] in _NATIVE_STARTUP_CODES: + return message[15:] + return fallback + + +def _native_startup_filter(binding: _NativeStartupCallbackBinding, message: object, userdata: object, error: object, + *, resources: object) -> int: + entered = False + try: + pointer = _native_startup_callback_enter(binding, message, userdata) + entered = True + record, token = binding.record, binding.token + if _native_startup_message_type(record, token, pointer) != 4: + return 0 + path = _native_startup_header_string(record, token, pointer, "sd_bus_message_get_path", nullable=True) + interface = _native_startup_header_string(record, token, pointer, "sd_bus_message_get_interface", nullable=True) + member = _native_startup_header_string(record, token, pointer, "sd_bus_message_get_member", nullable=True) + if (path, interface, member) != ("/org/freedesktop/DBus", "org.freedesktop.DBus", "NameOwnerChanged"): + return 0 + sender = _native_startup_header_string(record, token, pointer, "sd_bus_message_get_sender") + if sender != "org.freedesktop.DBus": + return 0 + destination = _native_startup_header_string(record, token, pointer, "sd_bus_message_get_destination", nullable=True) + if destination not in (None, record.resources.client_name): + _native_startup_fail("malformed_evidence") + signature = _native_startup_header_string(record, token, pointer, "sd_bus_message_get_signature", empty=True) + if signature != "sss": + _native_startup_fail("malformed_evidence") + serial = _native_startup_cookie(record, token, pointer) + name = _native_startup_read_string(record, token, pointer) + old = _native_startup_read_string(record, token, pointer, empty=True) + new = _native_startup_read_string(record, token, pointer, empty=True) + _native_startup_result(_native_startup_call(record, token, "sd_bus_message_at_end", (pointer, 1)), value=1) + if name != "org.freedesktop.systemd1": + return 0 + _native_startup_evidence(record, {"kind": "owner_change", "sender": sender, "serial": serial, "destination": destination, "old": old, "new": new}) + _native_startup_fail("manager_owner_changed") + except BaseException as caught: + _native_startup_latch_callback(resources, caught) + return -74 + finally: + _native_startup_callback_exit(resources, entered) + + +def _native_startup_reply(binding: _NativeStartupCallbackBinding, message: object, userdata: object, error: object, + *, resources: object) -> int: + entered = False + try: + pointer = _native_startup_callback_enter(binding, message, userdata) + entered = True + record, token = binding.record, binding.token + pending = record.resources.pending + if type(pending) is not _NativeStartupPendingReply or pending.binding is not binding or pending.reply is not None: + _native_startup_fail("malformed_evidence") + kind = _native_startup_message_type(record, token, pointer) + sender = _native_startup_header_string(record, token, pointer, "sd_bus_message_get_sender") + destination = _native_startup_header_string(record, token, pointer, "sd_bus_message_get_destination") + cookie = _native_startup_cookie(record, token, pointer, reply=True) + serial = _native_startup_cookie(record, token, pointer) + if sender != pending.destination or destination != record.resources.client_name or (pending.cookie is not None and cookie != pending.cookie): + _native_startup_fail("malformed_evidence") + if kind != 2: + _native_startup_fail("native_call_failed") + signature = _native_startup_header_string(record, token, pointer, "sd_bus_message_get_signature", empty=True) + expected = {"empty": "", "owner": "s", "uid": "u", "version": "v"}[pending.result_kind] + if signature != expected: + _native_startup_fail("malformed_evidence") + value: object = None + if pending.result_kind == "owner": + value = _native_startup_read_string(record, token, pointer) + if re.fullmatch(r":[0-9]+\.[0-9]+", value, flags=re.ASCII) is None: + _native_startup_fail("manager_auth_failed") + elif pending.result_kind == "uid": + out = ctypes.c_uint32() + _native_startup_result(_native_startup_call(record, token, "sd_bus_message_read_basic", (pointer, b"u", ctypes.byref(out))), value=1) + value = out.value + elif pending.result_kind == "version": + _native_startup_result(_native_startup_call(record, token, "sd_bus_message_enter_container", (pointer, b"v", b"s")), value=1) + value = _native_startup_read_string(record, token, pointer, limit=256) + if re.fullmatch(_NATIVE_STARTUP_VERSION, value, flags=re.ASCII) is None: + _native_startup_fail("manager_auth_failed") + _native_startup_result(_native_startup_call(record, token, "sd_bus_message_exit_container", (pointer,)), value=1) + _native_startup_result(_native_startup_call(record, token, "sd_bus_message_at_end", (pointer, 1)), value=1) + event = {"kind": "reply", "sender": sender, "destination": destination, "serial": serial, "cookie": cookie, "result_kind": pending.result_kind, "value": value} + _native_startup_evidence(record, event) + pending.reply = event + return 1 + except BaseException as caught: + _native_startup_latch_callback(resources, caught) + return -74 + finally: + _native_startup_callback_exit(resources, entered) + + +def _native_startup_install_filter(record: _NativeStartupRecord, token: _NativeStartupAuthToken) -> None: + backend = _native_startup_validate_backend(record) + binding = _NativeStartupCallbackBinding(record, token, "filter") + resources = record.resources + binding.callback = backend.handler(lambda message, userdata, error: _native_startup_filter(binding, message, userdata, error, resources=resources)) + record.resources.callbacks.append(binding) + record.resources.filter_binding = binding + out = ctypes.c_void_p() + slot = _NativeStartupReference("filter", out) + binding.slot = slot + _native_startup_result(_native_startup_call(record, token, "sd_bus_add_filter", (_native_startup_pointer(record.resources.bus), ctypes.byref(out), binding.callback, binding.userdata), acquire=slot), value=0) + _native_startup_pointer(slot) + + +def _native_startup_method(record: _NativeStartupRecord, token: _NativeStartupAuthToken, destination: str, + path: str, interface: str, member: str, inputs: tuple[str, ...], result_kind: str) -> object: + if record.resources.pending is not None: + _native_startup_fail("busy") + backend = _native_startup_validate_backend(record) + binding = _NativeStartupCallbackBinding(record, token, "reply") + resources = record.resources + binding.callback = backend.handler(lambda message, userdata, error: _native_startup_reply(binding, message, userdata, error, resources=resources)) + pending = _NativeStartupPendingReply(destination, result_kind, binding) + record.resources.callbacks.append(binding) + record.resources.pending = pending + out = ctypes.c_void_p() + message = _NativeStartupReference("method_message", out) + pending.message = message + headers = tuple(value.encode("ascii", "strict") for value in (destination, path, interface, member)) + pending.inputs.extend(headers) + _native_startup_result(_native_startup_call(record, token, "sd_bus_message_new_method_call", (_native_startup_pointer(record.resources.bus), ctypes.byref(out), *headers), acquire=message), value=0) + pointer = _native_startup_pointer(message) + for name in ("sd_bus_message_set_auto_start", "sd_bus_message_set_allow_interactive_authorization"): + _native_startup_result(_native_startup_call(record, token, name, (pointer, 0)), value=0) + for value in inputs: + _native_startup_text(value, bound=4096, ascii_only=True) + buffer = ctypes.create_string_buffer(value.encode("ascii", "strict")) + pending.inputs.append(buffer) + _native_startup_result(_native_startup_call(record, token, "sd_bus_message_append_basic", (pointer, b"s", ctypes.cast(buffer, ctypes.c_void_p))), nonnegative=True) + slot_out = ctypes.c_void_p() + slot = _NativeStartupReference("method_slot", slot_out) + pending.slot = binding.slot = slot + timeout = _native_startup_timeout_us(record, token) + _native_startup_result(_native_startup_call(record, token, "sd_bus_call_async", (_native_startup_pointer(record.resources.bus), ctypes.byref(slot_out), pointer, binding.callback, binding.userdata, timeout), acquire=slot), nonnegative=True) + _native_startup_pointer(slot) + pending.cookie = _native_startup_cookie(record, token, pointer) + if pending.reply is not None and pending.reply["cookie"] != pending.cookie: + _native_startup_fail("malformed_evidence") + while pending.reply is None: + _native_startup_process_once(record, token) + _native_startup_boundary(record, token) + value = pending.reply["value"] + _native_startup_unref(record, token, slot) + _native_startup_unref(record, token, message) + record.resources.pending = None + return value + + +def _native_startup_authenticate(record: _NativeStartupRecord, token: _NativeStartupAuthToken) -> None: + _native_startup_load_backend(record, token) + _native_startup_open_endpoint(record, token) + bus = _native_startup_pointer(record.resources.bus) + while True: + ready = _native_startup_result(_native_startup_call(record, token, "sd_bus_is_ready", (bus,))) + if ready not in (0, 1): + _native_startup_fail("malformed_evidence") + if ready: + break + _native_startup_process_once(record, token) + identifier = _NativeStartupId128() + _native_startup_result(_native_startup_call(record, token, "sd_bus_get_bus_id", (bus, ctypes.byref(identifier))), value=0) + record.resources.bus_id = bytes(identifier.bytes).hex() + out = ctypes.c_void_p() + _native_startup_result(_native_startup_call(record, token, "sd_bus_get_unique_name", (bus, ctypes.byref(out))), value=0) + name = _native_startup_scan_string(record, token, out.value, 255) + if re.fullmatch(r":[0-9]+\.[0-9]+", name, flags=re.ASCII) is None: + _native_startup_fail("malformed_evidence") + record.resources.client_name = name + _native_startup_install_filter(record, token) + broker, broker_path = "org.freedesktop.DBus", "/org/freedesktop/DBus" + _native_startup_method(record, token, broker, broker_path, broker, "AddMatch", (_NATIVE_STARTUP_MATCH,), "empty") + _native_startup_commit(record, owner_match_confirmed=True) + manager = _native_startup_method(record, token, broker, broker_path, broker, "GetNameOwner", ("org.freedesktop.systemd1",), "owner") + record.resources.manager_name = manager + uid = _native_startup_method(record, token, broker, broker_path, broker, "GetConnectionUnixUser", (manager,), "uid") + if type(uid) is not int or uid != record.seal.uid or uid != record.seal.euid: + _native_startup_fail("manager_auth_failed") + record.resources.manager_uid = uid + version = _native_startup_method(record, token, manager, "/org/freedesktop/systemd1", "org.freedesktop.DBus.Properties", "Get", ("org.freedesktop.systemd1.Manager", "Version"), "version") + record.resources.manager_version = version + context = StartupBusContext(1, record.resources.bus_id, manager, name, uid) + _native_startup_commit(record, context=context, manager_version=version) + _native_startup_method(record, token, manager, "/org/freedesktop/systemd1", "org.freedesktop.systemd1.Manager", "Subscribe", (), "empty") + _native_startup_commit(record, subscription_confirmed=True) + + +def _native_startup_gone(record: _NativeStartupRecord) -> tuple[bool, bool]: + resources = record.resources + connection = resources.raw_fd is None or resources.fd_release_observed + native = all(reference.state == "released" for reference in resources.references) + native = native and all(entry[2] == "released" for entry in resources.directory_fds) + if resources.bus is not None: + connection = connection and resources.bus_closed + return connection, native + + +def _native_startup_finish(record: _NativeStartupRecord, token: _NativeStartupAuthToken, code: str | None) -> NativeStartupSnapshot: + token.failed = code is not None + now = record.state.last_observed_now + try: + now = _native_startup_clock(record) + _native_startup_validate_record(record, effect=True) + if now >= token.deadline and code is None: + code = "deadline_expired" + except EnforcementUnavailable: + code = "clock_regressed" + now = record.state.last_observed_now + connection, native = _native_startup_gone(record) + changes: dict[str, object] = {"last_observed_now": now, "connection_closed": connection, "native_objects_released": native} + if token.cleanup: + closed = code is None and connection and native + changes.update(cleanup_sequence=token.terminal, phase="closed_precreate" if closed else "terminal_unknown", + cleanup_outcome="closed" if closed else ("unknown" if code in ("clock_regressed", "native_release_unobserved") else "partial"), + reason=record.state.reason if closed else (code or "cleanup_unresolved")) + else: + changes.update(operation_sequence=token.terminal, phase="subscribed" if code is None else "terminal_unknown", reason=code) + _native_startup_commit(record, **changes) + token.outcome = (token.terminal, code, now, connection, native) + return _native_startup_snapshot_copy(record) + + +def authenticate_startup_bus(owner: NativeStartupOwner, *, expected_sequence: int) -> NativeStartupSnapshot: + _native_startup_validate_owner(owner) + _native_startup_integer(expected_sequence) + record = _native_startup_enter(owner, effect=True) + try: + if expected_sequence != record.state.operation_sequence: + _native_startup_fail("stale_sequence") + if record.state.phase != "prepared": + _native_startup_fail("already_closed" if record.state.phase in ("closed_precreate", "released") else "invalid_input") + now = _native_startup_clock(record) + _native_startup_validate_record(record, effect=True) + if now >= record.seal.original_deadline: + _native_startup_fail("deadline_expired") + intent, terminal = _native_startup_sequence_pair(expected_sequence) + token = _NativeStartupAuthToken(record, intent, terminal, record.seal.original_deadline) + record.operation = token + _native_startup_commit(record, phase="authenticating", operation_sequence=intent, last_observed_now=now, authentication_attempted=True) + code = None + try: + _native_startup_authenticate(record, token) + except BaseException as caught: + code = _native_startup_error_code(caught, "native_call_failed") + return _native_startup_finish(record, token, code) + finally: + _native_startup_leave(record) + + +def _native_startup_close_connection(record: _NativeStartupRecord, token: _NativeStartupCleanupToken) -> None: + resources = record.resources + if resources.transfer in ("attempted_unknown", "detach_entered", "release_entered", "release_unobserved"): + _native_startup_fail("cleanup_unresolved") + if resources.bus is not None and not resources.bus_closed: + pointer = _native_startup_pointer(resources.bus) + _native_startup_call(record, token, "sd_bus_close", (pointer,)) + if not resources.bus_closed: + _native_startup_fail("native_release_unobserved") + if resources.fd_release_observed or resources.raw_fd is None: + return + _native_startup_boundary(record, token) + if resources.transfer == "python_owned": + if resources.socket.fileno() != resources.raw_fd: + _native_startup_fail("native_resource_drift") + resources.transfer = "release_entered" + resources.socket.close() + elif resources.transfer == "caller_owned" and (resources.bus is None or resources.bus_closed): + resources.transfer = "release_entered" + os.close(resources.raw_fd) + else: + _native_startup_fail("cleanup_unresolved") + resources.fd_release_observed = True + resources.transfer = "released" + _native_startup_boundary(record, token) + + +def _native_startup_cleanup(record: _NativeStartupRecord, token: _NativeStartupCleanupToken) -> None: + _native_startup_close_connection(record, token) + resources = record.resources + references = [reference for role in ("method_slot", "filter", "method_message", "process_message", "bus") + for reference in resources.references if reference.role == role] + for reference in references: + if reference.state == "released": + continue + if reference.state == "acquisition_pending" and reference.out.value is None: + entered = any(reference in frame.outputs and frame.entered for frame in resources.frames) + if not entered: + reference.state = "released" + continue + if reference.state != "owned": + _native_startup_fail("cleanup_unresolved") + if reference.role != "bus" and resources.bus is not None and not resources.bus_closed: + _native_startup_fail("cleanup_unresolved") + if reference.role == "bus" and any(other.state != "released" for other in resources.references if other is not reference): + _native_startup_fail("cleanup_unresolved") + _native_startup_unref(record, token, reference) + for entry in resources.directory_fds: + _native_startup_close_directory(record, token, entry) + if _native_startup_gone(record) != (True, True): + _native_startup_fail("cleanup_unresolved") + + +def close_precreate_startup(owner: NativeStartupOwner, *, expected_sequence: int, expected_cleanup_sequence: int, + cleanup_deadline: float) -> NativeStartupSnapshot: + _native_startup_validate_owner(owner) + _native_startup_integer(expected_sequence) + _native_startup_integer(expected_cleanup_sequence) + _native_startup_float(cleanup_deadline) + record = _native_startup_enter(owner, effect=True) + try: + if expected_sequence != record.state.operation_sequence or expected_cleanup_sequence != record.state.cleanup_sequence: + _native_startup_fail("stale_sequence") + if record.state.phase in ("closed_precreate", "released"): + _native_startup_fail("already_closed") + entry = _native_startup_clock(record) + _native_startup_validate_record(record, effect=True) + delta = cleanup_deadline - entry + if not 0 < delta <= 2.0: + _native_startup_fail("cleanup_deadline_invalid") + if len(record.cleanup_attempts) >= 16: + _native_startup_fail("cleanup_attempts_exhausted") + intent, terminal = _native_startup_sequence_pair(expected_cleanup_sequence, cleanup=True) + token = _NativeStartupCleanupToken(record, intent, terminal, cleanup_deadline) + record.cleanup_operation = token + record.cleanup_attempts.append(token) + _native_startup_commit(record, cleanup_sequence=intent, cleanup_deadline=cleanup_deadline, cleanup_attempted=True, + cleanup_outcome="in_progress", phase="closing_precreate", last_observed_now=entry) + code = None + try: + _native_startup_cleanup(record, token) + except BaseException as caught: + code = _native_startup_error_code(caught, "cleanup_unresolved") + return _native_startup_finish(record, token, code) + finally: + _native_startup_leave(record) + + +def release_closed_native_startup(owner: NativeStartupOwner, *, expected_sequence: int, expected_cleanup_sequence: int) -> NativeStartupSnapshot: + _native_startup_validate_owner(owner) + _native_startup_integer(expected_sequence) + _native_startup_integer(expected_cleanup_sequence) + record = _native_startup_enter(owner, effect=True) + try: + if expected_sequence != record.state.operation_sequence or expected_cleanup_sequence != record.state.cleanup_sequence: + _native_startup_fail("stale_sequence") + if record.state.phase != "closed_precreate" or _native_startup_gone(record) != (True, True): + _native_startup_fail("cleanup_unresolved") + _native_startup_commit(record, phase="released") + result = _native_startup_snapshot_copy(record) + if _NATIVE_STARTUP_RECORDS.get(owner.owner_id) is not record: + _native_startup_fail("invalid_owner") + del _NATIVE_STARTUP_RECORDS[owner.owner_id] + return result + finally: + _native_startup_leave(record) + + +if __name__ == "__main__": + main() From cac7c2b1c1c4ae9b6c17bd3395f77ec3a9de0595 Mon Sep 17 00:00:00 2001 From: buildingjoshbetter <236675935+buildingjoshbetter@users.noreply.github.com> Date: Mon, 12 Oct 2026 19:20:02 -0500 Subject: [PATCH 07/16] fix: Preserve inert rebuild policy import boundaries --- truememory/tier_switch/worker.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/truememory/tier_switch/worker.py b/truememory/tier_switch/worker.py index 8c697468..02fb6111 100644 --- a/truememory/tier_switch/worker.py +++ b/truememory/tier_switch/worker.py @@ -14,7 +14,7 @@ from contextlib import contextmanager from typing import TYPE_CHECKING, Callable, NamedTuple -from truememory.memory_budget import DEFAULT_LOCAL_MEMORY_POLICY +from truememory.memory_budget import DEFAULT_LOCAL_MEMORY_POLICY as _LOCAL_MEMORY_POLICY from truememory.tier_switch.cache import VectorCacheRegistry from truememory.tier_switch.throttler import DynamicThrottler @@ -23,8 +23,6 @@ from truememory.tier_switch.source import TierSourcePlan from truememory.vector_search import _PreparedEmbeddingLease -_LOCAL_MEMORY_POLICY = DEFAULT_LOCAL_MEMORY_POLICY - log = logging.getLogger(__name__) _HARD_TIMEOUT = 9000 # 2.5 hours @@ -502,7 +500,7 @@ def _process_batch( from truememory.memory_budget import ( ProfileKey, budgeted_encode_with_model_ownership, validate_local_policy, ) - validate_local_policy(globals()["_LOCAL_MEMORY_POLICY"]) + validate_local_policy(_LOCAL_MEMORY_POLICY) texts = [m["content"] for m in batch] ids = [m["id"] for m in batch] @@ -529,7 +527,7 @@ def _process_batch( for m in batch ] if configured_memory: - validate_local_policy(globals()["_LOCAL_MEMORY_POLICY"]) + validate_local_policy(_LOCAL_MEMORY_POLICY) sep_embeddings = budgeted_encode_with_model_ownership( model, sep_texts, ProfileKey("rebuild separation embedding", "local_embedding", input_item_count=len(sep_texts)), From 07c106604e6c87bbbb12c49800fe56533d9f3021 Mon Sep 17 00:00:00 2001 From: buildingjoshbetter <236675935+buildingjoshbetter@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:11:22 -0500 Subject: [PATCH 08/16] fix: Keep native test fixtures portable across hosted runners --- tests/native/mac-native-bootstrap-v1.py | 3 ++- tests/native/mac-native-bootstrap-v2.py | 3 ++- tests/native/mac-native-bootstrap-v4.py | 3 ++- tests/native/mac-native-bootstrap-v6.py | 3 ++- tests/native/mac-native-bootstrap-v7.py | 3 ++- tests/test-source-capture-admission-native-v1.py | 6 +++++- 6 files changed, 15 insertions(+), 6 deletions(-) diff --git a/tests/native/mac-native-bootstrap-v1.py b/tests/native/mac-native-bootstrap-v1.py index f2692ae1..9e53f6f3 100644 --- a/tests/native/mac-native-bootstrap-v1.py +++ b/tests/native/mac-native-bootstrap-v1.py @@ -4,7 +4,6 @@ import hashlib import json import os -import resource import select import signal import stat @@ -82,6 +81,8 @@ def receive(self, maximum: int, seconds: float) -> dict: def arm_limits() -> dict[str, list[int]]: if sys.platform != "darwin" or not sys.flags.isolated or not sys.flags.no_site or not sys.flags.dont_write_bytecode: raise BootstrapError("isolated_mac_child_required") + import resource + os.umask(0o077) sys.dont_write_bytecode = True expected = {"cpu": (2, 3), "core": (0, 0), "nofile": (64, 64), "fsize": (1048576, 1048576)} diff --git a/tests/native/mac-native-bootstrap-v2.py b/tests/native/mac-native-bootstrap-v2.py index 1d3071ec..755d49d7 100644 --- a/tests/native/mac-native-bootstrap-v2.py +++ b/tests/native/mac-native-bootstrap-v2.py @@ -4,7 +4,6 @@ import hashlib import json import os -import resource import select import signal import stat @@ -82,6 +81,8 @@ def receive(self, maximum: int, seconds: float) -> dict: def arm_limits() -> dict[str, list[int]]: if sys.platform != "darwin" or not sys.flags.isolated or not sys.flags.no_site or not sys.flags.dont_write_bytecode: raise BootstrapError("isolated_mac_child_required") + import resource + os.umask(0o077) sys.dont_write_bytecode = True expected = {"cpu": (2, 3), "core": (0, 0), "nofile": (64, 64), "fsize": (1048576, 1048576)} diff --git a/tests/native/mac-native-bootstrap-v4.py b/tests/native/mac-native-bootstrap-v4.py index b3bc1b94..6a19cf5c 100644 --- a/tests/native/mac-native-bootstrap-v4.py +++ b/tests/native/mac-native-bootstrap-v4.py @@ -4,7 +4,6 @@ import hashlib import json import os -import resource import select import signal import stat @@ -82,6 +81,8 @@ def receive(self, maximum: int, seconds: float) -> dict: def arm_limits() -> dict[str, list[int]]: if sys.platform != "darwin" or not sys.flags.isolated or not sys.flags.no_site or not sys.flags.dont_write_bytecode: raise BootstrapError("isolated_mac_child_required") + import resource + os.umask(0o077) sys.dont_write_bytecode = True expected = {"cpu": (2, 3), "core": (0, 0), "nofile": (64, 64), "fsize": (1048576, 1048576)} diff --git a/tests/native/mac-native-bootstrap-v6.py b/tests/native/mac-native-bootstrap-v6.py index 8051687c..8a2daea4 100644 --- a/tests/native/mac-native-bootstrap-v6.py +++ b/tests/native/mac-native-bootstrap-v6.py @@ -5,7 +5,6 @@ import io import json import os -import resource import select import signal import stat @@ -83,6 +82,8 @@ def receive(self, maximum: int, seconds: float) -> dict: def arm_limits() -> dict[str, list[int]]: if sys.platform != "darwin" or not sys.flags.isolated or not sys.flags.no_site or not sys.flags.dont_write_bytecode: raise BootstrapError("isolated_mac_child_required") + import resource + os.umask(0o077) sys.dont_write_bytecode = True expected = {"cpu": (2, 3), "core": (0, 0), "nofile": (64, 64), "fsize": (1048576, 1048576)} diff --git a/tests/native/mac-native-bootstrap-v7.py b/tests/native/mac-native-bootstrap-v7.py index 29f3a41f..10b490a5 100644 --- a/tests/native/mac-native-bootstrap-v7.py +++ b/tests/native/mac-native-bootstrap-v7.py @@ -5,7 +5,6 @@ import io import json import os -import resource import select import signal import stat @@ -83,6 +82,8 @@ def receive(self, maximum: int, seconds: float) -> dict: def arm_limits() -> dict[str, list[int]]: if sys.platform != "darwin" or not sys.flags.isolated or not sys.flags.no_site or not sys.flags.dont_write_bytecode: raise BootstrapError("isolated_mac_child_required") + import resource + os.umask(0o077) sys.dont_write_bytecode = True expected = {"cpu": (2, 3), "core": (0, 0), "nofile": (64, 64), "fsize": (1048576, 1048576)} diff --git a/tests/test-source-capture-admission-native-v1.py b/tests/test-source-capture-admission-native-v1.py index 266734e2..89eca54c 100644 --- a/tests/test-source-capture-admission-native-v1.py +++ b/tests/test-source-capture-admission-native-v1.py @@ -26,7 +26,11 @@ _ANNOUNCEMENT_BYTES = 0 MAX_ANNOUNCEMENT_BYTES = 65536 MAX_REGULAR_FIXTURE_BYTES_PER_CASE = 65536 -EVIDENCE_ROOT = Path("/results/source-capture-admission-native-v1") +EVIDENCE_ROOT = ( + Path("/results/source-capture-admission-native-v1") + if Path("/results").is_dir() + else Path(os.environ.get("TEMP", "/tmp")) / f"truememory-source-capture-admission-native-v1-{os.getpid()}" +) def announce(path: Path, purpose: str) -> None: From 12b65f1e1dc5b3b61fc49dc0d9e5298ba0989d78 Mon Sep 17 00:00:00 2001 From: buildingjoshbetter <236675935+buildingjoshbetter@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:42:30 -0500 Subject: [PATCH 09/16] fix: Disable unused decoder caches in Qwen embedding models --- truememory/embedding_target.py | 1 + truememory/model_server.py | 1 + truememory/vector_search.py | 1 + 3 files changed, 3 insertions(+) diff --git a/truememory/embedding_target.py b/truememory/embedding_target.py index 7ee2be9f..5604881b 100644 --- a/truememory/embedding_target.py +++ b/truememory/embedding_target.py @@ -115,6 +115,7 @@ def admit() -> None: admit() return SentenceTransformer( "Qwen/Qwen3-Embedding-0.6B", truncate_dim=256, + config_kwargs={"use_cache": False}, model_kwargs=model_kwargs, device=device, ) admit() diff --git a/truememory/model_server.py b/truememory/model_server.py index cb56a353..56f018bf 100644 --- a/truememory/model_server.py +++ b/truememory/model_server.py @@ -725,6 +725,7 @@ def _build_embed_model(model_id: str, device: str | None): return SentenceTransformer( "Qwen/Qwen3-Embedding-0.6B", truncate_dim=256, + config_kwargs={"use_cache": False}, model_kwargs=mkwargs or None, device=device, ) diff --git a/truememory/vector_search.py b/truememory/vector_search.py index 6a776b66..9510b5f9 100644 --- a/truememory/vector_search.py +++ b/truememory/vector_search.py @@ -336,6 +336,7 @@ def get_model(): _model = SentenceTransformer( "Qwen/Qwen3-Embedding-0.6B", truncate_dim=256, + config_kwargs={"use_cache": False}, model_kwargs=_mkwargs or None, device=device, ) From 40928bebbbef1f59f00842624f2f423ad252272d Mon Sep 17 00:00:00 2001 From: buildingjoshbetter <236675935+buildingjoshbetter@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:27:36 -0500 Subject: [PATCH 10/16] test: Require disabled Qwen decoder caches in constructor options --- tests/test_issue_577_model_server_oom.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_issue_577_model_server_oom.py b/tests/test_issue_577_model_server_oom.py index 677cb6db..02a31246 100644 --- a/tests/test_issue_577_model_server_oom.py +++ b/tests/test_issue_577_model_server_oom.py @@ -891,6 +891,7 @@ def test_issue_739_all_transformer_factories_preserve_identity_and_device_policy ("bge-small", "BAAI/bge-small-en-v1.5", 384, {}), ("qwen3_256", "Qwen/Qwen3-Embedding-0.6B", 256, { "truncate_dim": 256, + "config_kwargs": {"use_cache": False}, "model_kwargs": {"attn_implementation": "eager"} if sys.platform == "darwin" else None, }), ("synthetic/custom-encoder", "synthetic/custom-encoder", 192, { @@ -935,6 +936,7 @@ def test_issue_739_qwen_platform_options_are_preserved(self) -> None: with self.subTest(platform=platform), patch.object(sys, "platform", platform): self.load("qwen3_256", "cpu") self.assertEqual(self.transformer_calls[0][1], { + "config_kwargs": {"use_cache": False}, "truncate_dim": 256, "model_kwargs": model_kwargs, "device": "cpu", }) From ea37b2a117dd535242ffec52931160d44963d89f Mon Sep 17 00:00:00 2001 From: buildingjoshbetter <236675935+buildingjoshbetter@users.noreply.github.com> Date: Mon, 12 Oct 2026 21:50:44 -0500 Subject: [PATCH 11/16] fix: Create Windows ingestion files with private birth ownership --- tests/ingest/test_stop_hook_mark_guard.py | 26 +- .../native/mac-native-package-bootstrap-v3.py | 2 +- .../native/mac-native-package-bootstrap-v4.py | 2 +- tests/test-ingestion-completion-768.py | 8 +- truememory/_windows_private.py | 256 ++++++++++++++++++ truememory/ingest/completion.py | 22 +- 6 files changed, 304 insertions(+), 12 deletions(-) create mode 100644 truememory/_windows_private.py diff --git a/tests/ingest/test_stop_hook_mark_guard.py b/tests/ingest/test_stop_hook_mark_guard.py index 5ba54e2d..bac59f38 100644 --- a/tests/ingest/test_stop_hook_mark_guard.py +++ b/tests/ingest/test_stop_hook_mark_guard.py @@ -28,13 +28,25 @@ def _allow_tmp_transcripts(tmp_path, monkeypatch): def _make_transcript(tmp_path, n=6): p = tmp_path / "transcript.jsonl" - p.write_text( - "\n".join( - json.dumps({"type": "user", "message": {"content": f"message number {i}"}}) - for i in range(n) - ), - encoding="utf-8", - ) + import os + if os.name == "nt": + from truememory._windows_private import create_private_file + fd = create_private_file(p, os.O_CREAT | os.O_EXCL | os.O_WRONLY) + with os.fdopen(fd, "w", encoding="utf-8") as handle: + handle.write( + "\n".join( + json.dumps({"type": "user", "message": {"content": f"message number {i}"}}) + for i in range(n) + ) + ) + else: + p.write_text( + "\n".join( + json.dumps({"type": "user", "message": {"content": f"message number {i}"}}) + for i in range(n) + ), + encoding="utf-8", + ) return p diff --git a/tests/native/mac-native-package-bootstrap-v3.py b/tests/native/mac-native-package-bootstrap-v3.py index e22eaa0a..8aba0d72 100644 --- a/tests/native/mac-native-package-bootstrap-v3.py +++ b/tests/native/mac-native-package-bootstrap-v3.py @@ -8,7 +8,6 @@ import importlib import json import os -import resource import select import signal import socket @@ -83,6 +82,7 @@ def receive(self, maximum: int, seconds: float) -> dict: def arm_limits() -> dict[str, list[int]]: if sys.platform != "darwin" or not sys.flags.isolated or not sys.flags.no_site or not sys.flags.dont_write_bytecode: raise BootstrapError("isolated_mac_child_required") + import resource os.umask(0o077) sys.dont_write_bytecode = True expected = {"cpu": (2, 3), "core": (0, 0), "nofile": (64, 64), "fsize": (1048576, 1048576)} diff --git a/tests/native/mac-native-package-bootstrap-v4.py b/tests/native/mac-native-package-bootstrap-v4.py index 08a4287b..af4641c1 100644 --- a/tests/native/mac-native-package-bootstrap-v4.py +++ b/tests/native/mac-native-package-bootstrap-v4.py @@ -8,7 +8,6 @@ import importlib import json import os -import resource import select import signal import socket @@ -83,6 +82,7 @@ def receive(self, maximum: int, seconds: float) -> dict: def arm_limits() -> dict[str, list[int]]: if sys.platform != "darwin" or not sys.flags.isolated or not sys.flags.no_site or not sys.flags.dont_write_bytecode: raise BootstrapError("isolated_mac_child_required") + import resource os.umask(0o077) sys.dont_write_bytecode = True expected = {"cpu": (2, 3), "core": (0, 0), "nofile": (64, 64), "fsize": (1048576, 1048576)} diff --git a/tests/test-ingestion-completion-768.py b/tests/test-ingestion-completion-768.py index 9c3bd4e5..423f1532 100644 --- a/tests/test-ingestion-completion-768.py +++ b/tests/test-ingestion-completion-768.py @@ -458,7 +458,13 @@ def setUp(self) -> None: self.root = self.directory / "private-completion" self.source = self.directory / "source.jsonl" self.data = b"\n".join(json.dumps({"role": "user", "content": "synthetic source turn"}).encode() for _ in range(6)) - self.source.write_bytes(self.data) + if os.name == "nt": + from truememory._windows_private import create_private_file + fd = create_private_file(self.source, os.O_CREAT | os.O_EXCL | os.O_WRONLY) + with os.fdopen(fd, "wb") as handle: + handle.write(self.data) + else: + self.source.write_bytes(self.data) self.shared = types.ModuleType("truememory.ingest.hooks._shared") self.shared.is_allowed_transcript = lambda source, host: Path(source).parent == self.directory self.shared.normalize_transcript_host = lambda host: host if host in ("claude", "codex", "gemini", "cursor") else "" diff --git a/truememory/_windows_private.py b/truememory/_windows_private.py new file mode 100644 index 00000000..f5bc7e34 --- /dev/null +++ b/truememory/_windows_private.py @@ -0,0 +1,256 @@ +"""Fresh Windows objects with an explicit current-user owner and private DACL. + +Existing objects are never adopted, repaired or given a new security descriptor. +CreateDirectoryW followed by opening the new directory is not an atomic birth +identity proof; callers must retain their ordinary post-creation ownership checks. +""" +from __future__ import annotations + +import contextlib +import ctypes +import errno +import os +from pathlib import Path +from typing import Iterator + + +class _SecurityDescriptor(ctypes.Structure): + _fields_ = [("revision", ctypes.c_uint8), ("sbz1", ctypes.c_uint8), + ("control", ctypes.c_uint16), ("owner", ctypes.c_void_p), + ("group", ctypes.c_void_p), ("sacl", ctypes.c_void_p), + ("dacl", ctypes.c_void_p)] + + +class _SecurityAttributes(ctypes.Structure): + _fields_ = [("length", ctypes.c_uint32), ("descriptor", ctypes.c_void_p), + ("inherit", ctypes.c_int32)] + + +class _FileTime(ctypes.Structure): + _fields_ = [("low", ctypes.c_uint32), ("high", ctypes.c_uint32)] + + +class _FileInformation(ctypes.Structure): + _fields_ = [("attributes", ctypes.c_uint32), ("creation", _FileTime), + ("access", _FileTime), ("write", _FileTime), + ("volume", ctypes.c_uint32), ("size_high", ctypes.c_uint32), + ("size_low", ctypes.c_uint32), ("links", ctypes.c_uint32), + ("index_high", ctypes.c_uint32), ("index_low", ctypes.c_uint32)] + + +def _libraries() -> tuple[object, object]: + if os.name != "nt": + raise OSError(errno.ENOSYS, "Windows private creation is unavailable") + security = ctypes.WinDLL("advapi32", use_last_error=True) + kernel = ctypes.WinDLL("kernel32", use_last_error=True) + pointer, dword, boolean, handle = ctypes.c_void_p, ctypes.c_uint32, ctypes.c_int32, ctypes.c_void_p + bindings = ( + (security, "OpenProcessToken", [handle, dword, ctypes.POINTER(handle)], boolean), + (security, "GetTokenInformation", [handle, dword, pointer, dword, ctypes.POINTER(dword)], boolean), + (security, "IsValidSid", [pointer], boolean), + (security, "GetLengthSid", [pointer], dword), + (security, "CreateWellKnownSid", [dword, pointer, pointer, ctypes.POINTER(dword)], boolean), + (security, "InitializeSecurityDescriptor", [pointer, dword], boolean), + (security, "SetSecurityDescriptorOwner", [pointer, pointer, boolean], boolean), + (security, "InitializeAcl", [pointer, dword, dword], boolean), + (security, "AddAccessAllowedAceEx", [pointer, dword, dword, dword, pointer], boolean), + (security, "SetSecurityDescriptorDacl", [pointer, boolean, pointer, boolean], boolean), + (security, "SetSecurityDescriptorControl", [pointer, ctypes.c_uint16, ctypes.c_uint16], boolean), + (security, "IsValidSecurityDescriptor", [pointer], boolean), + (kernel, "GetCurrentProcess", [], handle), + (kernel, "CloseHandle", [handle], boolean), + (kernel, "GetHandleInformation", [handle, ctypes.POINTER(dword)], boolean), + (kernel, "GetFileType", [handle], dword), + (kernel, "GetFileInformationByHandle", [handle, ctypes.POINTER(_FileInformation)], boolean), + (kernel, "CreateDirectoryW", [ctypes.c_wchar_p, pointer], boolean), + (kernel, "CreateFileW", [ctypes.c_wchar_p, dword, dword, pointer, dword, dword, handle], handle), + ) + for library, name, arguments, result in bindings: + function = getattr(library, name) + function.argtypes, function.restype = arguments, result + return security, kernel + + +def _error(reason: str, path: Path | None = None) -> OSError: + code = ctypes.get_last_error() + if code in (80, 183): + return FileExistsError(errno.EEXIST, reason, str(path) if path is not None else None) + return ctypes.WinError(code) + + +def _close(kernel: object, handle: int) -> None: + if not kernel.CloseHandle(handle): + raise _error("private_creation_close_failed") + + +def _path(value: str | os.PathLike[str]) -> Path: + path = Path(value) + if (not path.is_absolute() or ".." in path.parts or "\0" in str(path) + or path == path.parent or len(str(path)) > 4096 or len(path.parents) > 64): + raise ValueError("Private birth requires an absolute child path without traversal") + return path + + +def _information(kernel: object, handle: int, *, directory: bool) -> tuple[int, int, int]: + if not directory and kernel.GetFileType(handle) != 1: + raise OSError(errno.EPERM, "Private creation requires a native disk file") + flags = ctypes.c_uint32() + if not kernel.GetHandleInformation(handle, ctypes.byref(flags)): + raise _error("private_creation_handle_information_failed") + if flags.value & 1: + raise OSError(errno.EPERM, "Private creation handle is inheritable") + info = _FileInformation() + if not kernel.GetFileInformationByHandle(handle, ctypes.byref(info)): + raise _error("private_creation_identity_failed") + if info.attributes & 0x400 or bool(info.attributes & 0x10) != directory: + raise OSError(errno.EPERM, "Private creation requires a plain no-reparse object") + if not directory and info.links != 1: + raise OSError(errno.EPERM, "Private creation requires one regular file link") + return info.volume, info.index_high, info.index_low + + +@contextlib.contextmanager +def _parents(kernel: object, path: Path) -> Iterator[None]: + held: list[int] = [] + try: + for parent in reversed(path.parents): + handle = kernel.CreateFileW(str(parent), 0x20080, 3, None, 3, 0x02200000, None) + if handle in (None, ctypes.c_void_p(-1).value): + raise _error("private_creation_parent_unavailable", parent) + held.append(handle) + _information(kernel, handle, directory=True) + yield + finally: + failure: OSError | None = None + for handle in reversed(held): + try: + _close(kernel, handle) + except OSError as error: + if failure is None: + failure = error + if failure is not None: + raise failure + + +@contextlib.contextmanager +def _birth_security(security: object, kernel: object, *, directory: bool) -> Iterator[_SecurityAttributes]: + token = ctypes.c_void_p() + if not security.OpenProcessToken(kernel.GetCurrentProcess(), 8, ctypes.byref(token)): + raise _error("private_creation_token_unavailable") + try: + needed = ctypes.c_uint32() + ctypes.set_last_error(0) + queried = security.GetTokenInformation(token, 1, None, 0, ctypes.byref(needed)) + if queried or ctypes.get_last_error() != 122 or not ctypes.sizeof(ctypes.c_void_p) + 4 <= needed.value <= 65536: + raise OSError(errno.EPERM, "Private creation token information is unbounded") + buffer = ctypes.create_string_buffer(needed.value) + capacity = len(buffer) + if not security.GetTokenInformation(token, 1, buffer, capacity, ctypes.byref(needed)): + raise _error("private_creation_token_information_failed") + if not 1 <= needed.value <= capacity: + raise OSError(errno.EPERM, "Private creation token information changed size") + user = ctypes.c_void_p.from_buffer(buffer) + start = ctypes.addressof(buffer) + if user.value is None or not start <= user.value <= start + needed.value - 8: + raise OSError(errno.EPERM, "Private creation token SID is outside its buffer") + header = ctypes.string_at(user, 2) + sid_bytes = 8 + 4 * header[1] + if header[0] != 1 or header[1] > 15 or user.value + sid_bytes > start + needed.value: + raise OSError(errno.EPERM, "Private creation token SID is malformed") + if not security.IsValidSid(user) or security.GetLengthSid(user) != sid_bytes: + raise OSError(errno.EPERM, "Private creation token SID is invalid") + well_known_buffers: list[ctypes.Array] = [] + sids = [user] + sizes = [sid_bytes] + for kind in (22, 26): + sid = ctypes.create_string_buffer(68) + size = ctypes.c_uint32(68) + if not security.CreateWellKnownSid(kind, None, sid, ctypes.byref(size)): + raise _error("private_creation_well_known_sid_failed") + if not 8 <= size.value <= 68 or not security.IsValidSid(sid) or security.GetLengthSid(sid) != size.value: + raise OSError(errno.EPERM, "Private creation well-known SID is invalid") + well_known_buffers.append(sid) + sids.append(ctypes.cast(sid, ctypes.c_void_p)) + sizes.append(size.value) + acl = ctypes.create_string_buffer(8 + sum(8 + size for size in sizes)) + descriptor = _SecurityDescriptor() + if not security.InitializeSecurityDescriptor(ctypes.byref(descriptor), 1): + raise _error("private_creation_descriptor_failed") + if not security.SetSecurityDescriptorOwner(ctypes.byref(descriptor), user, False): + raise _error("private_creation_owner_failed") + if not security.InitializeAcl(acl, len(acl), 2): + raise _error("private_creation_acl_failed") + for sid in sids: + if not security.AddAccessAllowedAceEx(acl, 2, 3 if directory else 0, 0x001F01FF, sid): + raise _error("private_creation_ace_failed") + if not security.SetSecurityDescriptorDacl(ctypes.byref(descriptor), True, acl, False): + raise _error("private_creation_dacl_failed") + if not security.SetSecurityDescriptorControl(ctypes.byref(descriptor), 0x1000, 0x1000): + raise _error("private_creation_dacl_protection_failed") + if not security.IsValidSecurityDescriptor(ctypes.byref(descriptor)): + raise OSError(errno.EPERM, "Private creation descriptor is invalid") + attributes = _SecurityAttributes(ctypes.sizeof(_SecurityAttributes), ctypes.addressof(descriptor), False) + # Token SID, well-known SID, ACL and absolute descriptor storage stay live through the native birth. + yield attributes + finally: + if token: + _close(kernel, token) + + +def create_private_directory(value: str | os.PathLike[str]) -> None: + """Create one new directory; an existing name raises FileExistsError.""" + path = _path(value) + security, kernel = _libraries() + with _parents(kernel, path), _birth_security(security, kernel, directory=True) as attributes: + if not kernel.CreateDirectoryW(str(path), ctypes.byref(attributes)): + raise _error("private_directory_create_failed", path) + # This separate open cannot establish an atomic CreateDirectoryW birth identity. + handle = kernel.CreateFileW(str(path), 0x20080, 3, None, 3, 0x02200000, None) + if handle in (None, ctypes.c_void_p(-1).value): + raise _error("private_directory_open_failed", path) + try: + _information(kernel, handle, directory=True) + finally: + _close(kernel, handle) + + +def create_private_file(value: str | os.PathLike[str], flags: int) -> int: + """Create a new file and transfer its no-share-delete handle into one CRT fd.""" + if type(flags) is not int or flags not in (os.O_CREAT | os.O_RDWR, os.O_CREAT | os.O_EXCL | os.O_WRONLY): + raise ValueError("Private birth requires one of the two completion creation flag combinations") + path = _path(value) + security, kernel = _libraries() + import msvcrt + handle = None + fd = None + try: + with _parents(kernel, path), _birth_security(security, kernel, directory=False) as attributes: + access = 0x20080 + if flags & os.O_RDWR: + access |= 0x80000000 | 0x40000000 + elif flags & os.O_WRONLY: + access |= 0x40000000 + else: + access |= 0x80000000 + handle = kernel.CreateFileW(str(path), access, 3, ctypes.byref(attributes), 1, 0x00200080, None) + if handle in (None, ctypes.c_void_p(-1).value): + handle = None + raise _error("private_file_create_failed", path) + identity = _information(kernel, handle, directory=False) + from truememory.ingest.completion import _windows_security + if not _windows_security(handle, private=True): + raise OSError(errno.EPERM, "Private creation failed the original private ownership guard") + transfer = flags & (os.O_WRONLY | os.O_RDWR | os.O_APPEND) + transfer |= os.O_BINARY | os.O_NOINHERIT + fd = msvcrt.open_osfhandle(int(handle), transfer) + handle = None + if _information(kernel, msvcrt.get_osfhandle(fd), directory=False) != identity: + raise OSError(errno.EPERM, "Private file identity changed during fd transfer") + return fd + except BaseException: + if fd is not None: + os.close(fd) + raise + finally: + if handle is not None: + _close(kernel, handle) diff --git a/truememory/ingest/completion.py b/truememory/ingest/completion.py index 2c44593b..981d4e69 100644 --- a/truememory/ingest/completion.py +++ b/truememory/ingest/completion.py @@ -74,7 +74,16 @@ def _private_directory(directory: Path) -> None: # Do not follow an existing link or change permissions of foreign paths. if directory.is_symlink(): raise CompletionError("unsafe_directory") - directory.mkdir(parents=True, mode=0o700, exist_ok=True) + if os.name == "nt": + from truememory._windows_private import create_private_directory + if not directory.parent.exists(): + _private_directory(directory.parent) + try: + create_private_directory(directory) + except FileExistsError: + pass + else: + directory.mkdir(parents=True, mode=0o700, exist_ok=True) for value in (directory.parent, directory): info = value.lstat() if not stat.S_ISDIR(info.st_mode) or value.is_symlink(): @@ -95,7 +104,16 @@ def _account_directory(directory: Path) -> None: def _open_private(path: Path, flags: int) -> int: if path.is_symlink(): raise CompletionError("unsafe_file") - fd = os.open(path, flags | getattr(os, "O_NOFOLLOW", 0), 0o600) + if os.name == "nt" and flags & os.O_CREAT: + from truememory._windows_private import create_private_file + try: + fd = create_private_file(path, flags) + except FileExistsError: + if flags & os.O_EXCL: + raise + fd = os.open(path, (flags & ~(os.O_CREAT | os.O_EXCL)) | getattr(os, "O_NOFOLLOW", 0), 0o600) + else: + fd = os.open(path, flags | getattr(os, "O_NOFOLLOW", 0), 0o600) try: info = os.fstat(fd) if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1: From 6fd04f847f3e135083ce6a926d150a9c4ec0d982 Mon Sep 17 00:00:00 2001 From: buildingjoshbetter <236675935+buildingjoshbetter@users.noreply.github.com> Date: Mon, 12 Oct 2026 22:14:59 -0500 Subject: [PATCH 12/16] test: Isolate POSIX policy fixtures on Windows --- tests/test-linux-preexec-enforcement-297.py | 107 +++++++++++++++ .../test-linux-startup-transport-owner-297.py | 97 +++++++++++++- tests/test-mac-native-acceptance-v1.py | 121 ++++++++++++++++- tests/test-mac-native-acceptance-v2.py | 120 ++++++++++++++++- tests/test-mac-native-acceptance-v4.py | 120 ++++++++++++++++- tests/test-mac-native-acceptance-v6.py | 120 ++++++++++++++++- tests/test-mac-native-acceptance-v7.py | 123 +++++++++++++++++- tests/test-memory-enforcement-native-297.py | 91 ++++++++++++- 8 files changed, 892 insertions(+), 7 deletions(-) diff --git a/tests/test-linux-preexec-enforcement-297.py b/tests/test-linux-preexec-enforcement-297.py index 4fbeed17..074f28da 100644 --- a/tests/test-linux-preexec-enforcement-297.py +++ b/tests/test-linux-preexec-enforcement-297.py @@ -14,6 +14,112 @@ from unittest.mock import patch + +import os as _host_os +import sys as _host_sys +from pathlib import PurePosixPath, PureWindowsPath +from types import ModuleType as _FixtureModule + +_HOST_PATH = Path +_HOST_REPOSITORY = _HOST_PATH(__file__).resolve().parents[1] + + +def _forbidden_target_effect(*args: object, **kwargs: object) -> object: + raise AssertionError("Unmocked synthetic target effect is forbidden") + + +class _TargetPath(PurePosixPath): + def __new__(cls, *args: object): + if args: + first = args[0] + if isinstance(first, _HOST_PATH) or ( + isinstance(first, str) and PureWindowsPath(first).is_absolute() + ): + native = _HOST_PATH(*args) + if (not native.is_absolute() or ".." in native.parts + or not native.is_relative_to(_HOST_REPOSITORY)): + raise AssertionError("Native fixture path outside repository") + return native + return super().__new__(cls, *args) + + lstat = _forbidden_target_effect + stat = _forbidden_target_effect + resolve = _forbidden_target_effect + is_symlink = _forbidden_target_effect + read_bytes = _forbidden_target_effect + read_text = _forbidden_target_effect + write_bytes = _forbidden_target_effect + write_text = _forbidden_target_effect + open = _forbidden_target_effect + exists = _forbidden_target_effect + mkdir = _forbidden_target_effect + unlink = _forbidden_target_effect + + def absolute(self): + if not self.is_absolute(): + raise AssertionError("Relative synthetic path has no native working directory") + return self + + +def _target_os(): + module = _FixtureModule("synthetic_target_os") + module.__dict__.update(vars(_host_os)) + module.name = "posix" + # Linux fixture symbols preserve access comparisons, not native Windows flags. + for name, value in {"O_RDONLY": 0, "O_WRONLY": 1, "O_RDWR": 2, "O_ACCMODE": 3, + "O_CREAT": 64, "O_EXCL": 128, "O_TRUNC": 512, "O_APPEND": 1024, + "O_NONBLOCK": 2048, "O_DIRECTORY": 65536, + "O_NOFOLLOW": 131072, "O_CLOEXEC": 524288}.items(): + setattr(module, name, value) + for name in ("getuid", "geteuid", "getpgid", "killpg", "kill", "pread", "open", + "read", "write", "close", "fstat", "stat", "lstat", "fsync", "fdopen", + "lseek", "umask", "access", "mkdir", "rename", "replace", "unlink", + "rmdir", "listdir", "scandir", "fchmod", "dup", "pipe", "set_blocking"): + setattr(module, name, _forbidden_target_effect) + return module + + +def _target_signal(original): + module = _FixtureModule("synthetic_target_signal") + module.__dict__.update(vars(original)) + module.SIGTERM, module.SIGKILL, module.SIGUSR1 = 15, 9, 10 + for name in ("signal", "alarm", "raise_signal", "pidfd_send_signal"): + setattr(module, name, _forbidden_target_effect) + return module + + +def _target_select(original): + module = _FixtureModule("synthetic_target_select") + module.__dict__.update(vars(original)) + module.poll = module.select = _forbidden_target_effect + return module + + +def _prepare_target_module(module): + if _host_sys.platform != "win32": + return module + module.os = _TARGET_OS + module.Path = _TargetPath + for name in ("RUN_ROOT", "SOURCE_ROOT", "RUNTIME_PACKET"): + if hasattr(module, name): + value = getattr(module, name) + setattr(module, name, _TargetPath(value.as_posix())) + if hasattr(module, "sys"): + isolated = _FixtureModule("synthetic_target_sys") + isolated.__dict__.update(vars(module.sys)) + module.sys = isolated + if hasattr(module, "signal"): + module.signal = _target_signal(module.signal) + if hasattr(module, "select"): + module.select = _target_select(module.select) + return module + + +_TARGET_OS = _target_os() if _host_sys.platform == "win32" else _host_os +if _host_sys.platform == "win32": + Path = _TargetPath + + ROOT = Path(__file__).resolve().parents[1] MODULE_PATH = ROOT / "truememory_memory_bootstrap.py" SPEC = importlib.util.spec_from_file_location("synthetic_linux_preexec_author", MODULE_PATH) @@ -21,6 +127,7 @@ MODULE = importlib.util.module_from_spec(SPEC) sys.modules[SPEC.name] = MODULE SPEC.loader.exec_module(MODULE) +_prepare_target_module(MODULE) def identity(cap: int = 67108864) -> object: diff --git a/tests/test-linux-startup-transport-owner-297.py b/tests/test-linux-startup-transport-owner-297.py index 8236ac04..a5527957 100644 --- a/tests/test-linux-startup-transport-owner-297.py +++ b/tests/test-linux-startup-transport-owner-297.py @@ -11,6 +11,101 @@ from unittest.mock import patch + +import os as _host_os +import sys as _host_sys +from pathlib import PurePosixPath, PureWindowsPath +from types import ModuleType as _FixtureModule + +_HOST_PATH = Path +_HOST_REPOSITORY = _HOST_PATH(__file__).resolve().parents[1] + + +def _forbidden_target_effect(*args: object, **kwargs: object) -> object: + raise AssertionError("Unmocked synthetic target effect is forbidden") + + +class _TargetPath(PurePosixPath): + def __new__(cls, *args: object): + if args: + first = args[0] + if isinstance(first, _HOST_PATH) or ( + isinstance(first, str) and PureWindowsPath(first).is_absolute() + ): + native = _HOST_PATH(*args) + if (not native.is_absolute() or ".." in native.parts + or not native.is_relative_to(_HOST_REPOSITORY)): + raise AssertionError("Native fixture path outside repository") + return native + return super().__new__(cls, *args) + + lstat = _forbidden_target_effect + stat = _forbidden_target_effect + resolve = _forbidden_target_effect + is_symlink = _forbidden_target_effect + read_bytes = _forbidden_target_effect + read_text = _forbidden_target_effect + write_bytes = _forbidden_target_effect + write_text = _forbidden_target_effect + open = _forbidden_target_effect + exists = _forbidden_target_effect + mkdir = _forbidden_target_effect + unlink = _forbidden_target_effect + + def absolute(self): + if not self.is_absolute(): + raise AssertionError("Relative synthetic path has no native working directory") + return self + + +def _target_os(): + module = _FixtureModule("synthetic_target_os") + module.__dict__.update(vars(_host_os)) + module.name = "posix" + # Linux fixture symbols preserve access comparisons, not native Windows flags. + for name, value in {"O_RDONLY": 0, "O_WRONLY": 1, "O_RDWR": 2, "O_ACCMODE": 3, + "O_CREAT": 64, "O_EXCL": 128, "O_TRUNC": 512, "O_APPEND": 1024, + "O_NONBLOCK": 2048, "O_DIRECTORY": 65536, + "O_NOFOLLOW": 131072, "O_CLOEXEC": 524288}.items(): + setattr(module, name, value) + for name in ("getuid", "geteuid", "getpgid", "killpg", "kill", "pread", "open", + "read", "write", "close", "fstat", "stat", "lstat", "fsync", "fdopen", + "lseek", "umask", "access", "mkdir", "rename", "replace", "unlink", + "rmdir", "listdir", "scandir", "fchmod", "dup", "pipe", "set_blocking"): + setattr(module, name, _forbidden_target_effect) + return module + + +def _target_select(original): + module = _FixtureModule("synthetic_target_select") + module.__dict__.update(vars(original)) + module.poll = module.select = _forbidden_target_effect + return module + + +def _prepare_target_module(module): + if _host_sys.platform != "win32": + return module + module.os = _TARGET_OS + module.Path = _TargetPath + for name in ("RUN_ROOT", "SOURCE_ROOT", "RUNTIME_PACKET"): + if hasattr(module, name): + value = getattr(module, name) + setattr(module, name, _TargetPath(value.as_posix())) + if hasattr(module, "sys"): + isolated = _FixtureModule("synthetic_target_sys") + isolated.__dict__.update(vars(module.sys)) + module.sys = isolated + if hasattr(module, "select"): + module.select = _target_select(module.select) + return module + + +_TARGET_OS = _target_os() if _host_sys.platform == "win32" else _host_os +if _host_sys.platform == "win32": + Path = _TargetPath + + def load_source() -> ModuleType: path = Path(__file__).resolve().parents[1] / "truememory_memory_bootstrap.py" name = "truememory_transport_author_source" @@ -18,7 +113,7 @@ def load_source() -> ModuleType: module = importlib.util.module_from_spec(spec) sys.modules[name] = module spec.loader.exec_module(module) - return module + return _prepare_target_module(module) class Function: diff --git a/tests/test-mac-native-acceptance-v1.py b/tests/test-mac-native-acceptance-v1.py index 517de737..279dca29 100644 --- a/tests/test-mac-native-acceptance-v1.py +++ b/tests/test-mac-native-acceptance-v1.py @@ -2,6 +2,7 @@ from __future__ import annotations import copy +from contextlib import ExitStack import os import stat import sys @@ -11,13 +12,128 @@ from unittest import mock + +import os as _host_os +import sys as _host_sys +from pathlib import PurePosixPath, PureWindowsPath +from types import ModuleType as _FixtureModule + +_HOST_PATH = Path +_HOST_REPOSITORY = _HOST_PATH(__file__).resolve().parents[1] + + +def _forbidden_target_effect(*args: object, **kwargs: object) -> object: + raise AssertionError("Unmocked synthetic target effect is forbidden") + + +class _TargetPath(PurePosixPath): + def __new__(cls, *args: object): + if args: + first = args[0] + if isinstance(first, _HOST_PATH) or ( + isinstance(first, str) and PureWindowsPath(first).is_absolute() + ): + native = _HOST_PATH(*args) + if (not native.is_absolute() or ".." in native.parts + or not native.is_relative_to(_HOST_REPOSITORY)): + raise AssertionError("Native fixture path outside repository") + return native + return super().__new__(cls, *args) + + lstat = _forbidden_target_effect + stat = _forbidden_target_effect + resolve = _forbidden_target_effect + is_symlink = _forbidden_target_effect + read_bytes = _forbidden_target_effect + read_text = _forbidden_target_effect + write_bytes = _forbidden_target_effect + write_text = _forbidden_target_effect + open = _forbidden_target_effect + exists = _forbidden_target_effect + mkdir = _forbidden_target_effect + unlink = _forbidden_target_effect + + def absolute(self): + if not self.is_absolute(): + raise AssertionError("Relative synthetic path has no native working directory") + return self + + +def _target_os(): + module = _FixtureModule("synthetic_target_os") + module.__dict__.update(vars(_host_os)) + module.name = "posix" + # Linux fixture symbols preserve access comparisons, not native Windows flags. + for name, value in {"O_RDONLY": 0, "O_WRONLY": 1, "O_RDWR": 2, "O_ACCMODE": 3, + "O_CREAT": 64, "O_EXCL": 128, "O_TRUNC": 512, "O_APPEND": 1024, + "O_NONBLOCK": 2048, "O_DIRECTORY": 65536, + "O_NOFOLLOW": 131072, "O_CLOEXEC": 524288}.items(): + setattr(module, name, value) + for name in ("getuid", "geteuid", "getpgid", "killpg", "kill", "pread", "open", + "read", "write", "close", "fstat", "stat", "lstat", "fsync", "fdopen", + "lseek", "umask", "access", "mkdir", "rename", "replace", "unlink", + "rmdir", "listdir", "scandir", "fchmod", "dup", "pipe", "set_blocking"): + setattr(module, name, _forbidden_target_effect) + return module + + +def _target_signal(original): + module = _FixtureModule("synthetic_target_signal") + module.__dict__.update(vars(original)) + module.SIGTERM, module.SIGKILL, module.SIGUSR1 = 15, 9, 10 + for name in ("signal", "alarm", "raise_signal", "pidfd_send_signal"): + setattr(module, name, _forbidden_target_effect) + return module + + +def _prepare_target_module(module): + if _host_sys.platform != "win32": + return module + module.os = _TARGET_OS + module.Path = _TargetPath + for name in ("RUN_ROOT", "SOURCE_ROOT", "RUNTIME_PACKET"): + if hasattr(module, name): + value = getattr(module, name) + setattr(module, name, _TargetPath(value.as_posix())) + if hasattr(module, "sys"): + isolated = _FixtureModule("synthetic_target_sys") + isolated.__dict__.update(vars(module.sys)) + module.sys = isolated + if hasattr(module, "signal"): + module.signal = _target_signal(module.signal) + return module + + +_TARGET_OS = _target_os() if _host_sys.platform == "win32" else _host_os +if _host_sys.platform == "win32": + Path = _TargetPath + + +def _inert_fcntl(): + module = _FixtureModule("fcntl") + module.F_GETFL, module.LOCK_EX, module.LOCK_NB = 3, 2, 4 + module.fcntl = module.flock = _forbidden_target_effect + return module + + +def _install_inert_fcntl(test): + if _host_sys.platform == "win32": + stack = ExitStack() + test.addCleanup(stack.close) + stack.enter_context(mock.patch.dict(sys.modules, {"fcntl": _inert_fcntl()})) + + +if _host_sys.platform == "win32": + os = _TARGET_OS + + def load_source(name: str, filename: str) -> types.ModuleType: path = Path(__file__).parent / "native" / filename module = types.ModuleType(name) module.__file__ = str(path) sys.modules[name] = module exec(compile(path.read_bytes(), str(path), "exec", dont_inherit=True), module.__dict__) - return module + return _prepare_target_module(module) POLICY = load_source("native_acceptance_policy_test_v1", "mac-native-acceptance-v1.py") @@ -76,6 +192,9 @@ class FakePsutilError(Exception): class PurePolicyTests(unittest.TestCase): + def setUp(self) -> None: + _install_inert_fcntl(self) + def test_fixed_limits_and_proof_scope(self) -> None: self.assertEqual(POLICY.LIMITS, BOOT.LIMITS) self.assertEqual(POLICY.FREE_MIN - POLICY.LIMITS["rss_abort_bytes"], 201326592) diff --git a/tests/test-mac-native-acceptance-v2.py b/tests/test-mac-native-acceptance-v2.py index ff7c4822..51d8fb17 100644 --- a/tests/test-mac-native-acceptance-v2.py +++ b/tests/test-mac-native-acceptance-v2.py @@ -15,13 +15,128 @@ from unittest import mock + +import os as _host_os +import sys as _host_sys +from pathlib import PurePosixPath, PureWindowsPath +from types import ModuleType as _FixtureModule + +_HOST_PATH = Path +_HOST_REPOSITORY = _HOST_PATH(__file__).resolve().parents[1] + + +def _forbidden_target_effect(*args: object, **kwargs: object) -> object: + raise AssertionError("Unmocked synthetic target effect is forbidden") + + +class _TargetPath(PurePosixPath): + def __new__(cls, *args: object): + if args: + first = args[0] + if isinstance(first, _HOST_PATH) or ( + isinstance(first, str) and PureWindowsPath(first).is_absolute() + ): + native = _HOST_PATH(*args) + if (not native.is_absolute() or ".." in native.parts + or not native.is_relative_to(_HOST_REPOSITORY)): + raise AssertionError("Native fixture path outside repository") + return native + return super().__new__(cls, *args) + + lstat = _forbidden_target_effect + stat = _forbidden_target_effect + resolve = _forbidden_target_effect + is_symlink = _forbidden_target_effect + read_bytes = _forbidden_target_effect + read_text = _forbidden_target_effect + write_bytes = _forbidden_target_effect + write_text = _forbidden_target_effect + open = _forbidden_target_effect + exists = _forbidden_target_effect + mkdir = _forbidden_target_effect + unlink = _forbidden_target_effect + + def absolute(self): + if not self.is_absolute(): + raise AssertionError("Relative synthetic path has no native working directory") + return self + + +def _target_os(): + module = _FixtureModule("synthetic_target_os") + module.__dict__.update(vars(_host_os)) + module.name = "posix" + # Linux fixture symbols preserve access comparisons, not native Windows flags. + for name, value in {"O_RDONLY": 0, "O_WRONLY": 1, "O_RDWR": 2, "O_ACCMODE": 3, + "O_CREAT": 64, "O_EXCL": 128, "O_TRUNC": 512, "O_APPEND": 1024, + "O_NONBLOCK": 2048, "O_DIRECTORY": 65536, + "O_NOFOLLOW": 131072, "O_CLOEXEC": 524288}.items(): + setattr(module, name, value) + for name in ("getuid", "geteuid", "getpgid", "killpg", "kill", "pread", "open", + "read", "write", "close", "fstat", "stat", "lstat", "fsync", "fdopen", + "lseek", "umask", "access", "mkdir", "rename", "replace", "unlink", + "rmdir", "listdir", "scandir", "fchmod", "dup", "pipe", "set_blocking"): + setattr(module, name, _forbidden_target_effect) + return module + + +def _target_signal(original): + module = _FixtureModule("synthetic_target_signal") + module.__dict__.update(vars(original)) + module.SIGTERM, module.SIGKILL, module.SIGUSR1 = 15, 9, 10 + for name in ("signal", "alarm", "raise_signal", "pidfd_send_signal"): + setattr(module, name, _forbidden_target_effect) + return module + + +def _prepare_target_module(module): + if _host_sys.platform != "win32": + return module + module.os = _TARGET_OS + module.Path = _TargetPath + for name in ("RUN_ROOT", "SOURCE_ROOT", "RUNTIME_PACKET"): + if hasattr(module, name): + value = getattr(module, name) + setattr(module, name, _TargetPath(value.as_posix())) + if hasattr(module, "sys"): + isolated = _FixtureModule("synthetic_target_sys") + isolated.__dict__.update(vars(module.sys)) + module.sys = isolated + if hasattr(module, "signal"): + module.signal = _target_signal(module.signal) + return module + + +_TARGET_OS = _target_os() if _host_sys.platform == "win32" else _host_os +if _host_sys.platform == "win32": + Path = _TargetPath + + +def _inert_fcntl(): + module = _FixtureModule("fcntl") + module.F_GETFL, module.LOCK_EX, module.LOCK_NB = 3, 2, 4 + module.fcntl = module.flock = _forbidden_target_effect + return module + + +def _install_inert_fcntl(test): + if _host_sys.platform == "win32": + stack = ExitStack() + test.addCleanup(stack.close) + stack.enter_context(mock.patch.dict(sys.modules, {"fcntl": _inert_fcntl()})) + + +if _host_sys.platform == "win32": + os = _TARGET_OS + + def load_source(name: str, filename: str) -> types.ModuleType: path = Path(__file__).parent / "native" / filename module = types.ModuleType(name) module.__file__ = str(path) sys.modules[name] = module exec(compile(path.read_bytes(), str(path), "exec", dont_inherit=True), module.__dict__) - return module + return _prepare_target_module(module) POLICY = load_source("native_acceptance_policy_test_v2", "mac-native-acceptance-v2.py") @@ -108,6 +223,9 @@ def main_scope(version: str = "v3", fixture: dict | None = None) -> Iterator[dic class PurePolicyTests(unittest.TestCase): + def setUp(self) -> None: + _install_inert_fcntl(self) + def test_fixed_limits_and_proof_scope(self) -> None: self.assertEqual(POLICY.LIMITS, BOOT.LIMITS) self.assertEqual(POLICY.FREE_MIN - POLICY.LIMITS["rss_abort_bytes"], 201326592) diff --git a/tests/test-mac-native-acceptance-v4.py b/tests/test-mac-native-acceptance-v4.py index 84fdd4ac..0700420f 100644 --- a/tests/test-mac-native-acceptance-v4.py +++ b/tests/test-mac-native-acceptance-v4.py @@ -16,6 +16,121 @@ from unittest import mock + +import os as _host_os +import sys as _host_sys +from pathlib import PurePosixPath, PureWindowsPath +from types import ModuleType as _FixtureModule + +_HOST_PATH = Path +_HOST_REPOSITORY = _HOST_PATH(__file__).resolve().parents[1] + + +def _forbidden_target_effect(*args: object, **kwargs: object) -> object: + raise AssertionError("Unmocked synthetic target effect is forbidden") + + +class _TargetPath(PurePosixPath): + def __new__(cls, *args: object): + if args: + first = args[0] + if isinstance(first, _HOST_PATH) or ( + isinstance(first, str) and PureWindowsPath(first).is_absolute() + ): + native = _HOST_PATH(*args) + if (not native.is_absolute() or ".." in native.parts + or not native.is_relative_to(_HOST_REPOSITORY)): + raise AssertionError("Native fixture path outside repository") + return native + return super().__new__(cls, *args) + + lstat = _forbidden_target_effect + stat = _forbidden_target_effect + resolve = _forbidden_target_effect + is_symlink = _forbidden_target_effect + read_bytes = _forbidden_target_effect + read_text = _forbidden_target_effect + write_bytes = _forbidden_target_effect + write_text = _forbidden_target_effect + open = _forbidden_target_effect + exists = _forbidden_target_effect + mkdir = _forbidden_target_effect + unlink = _forbidden_target_effect + + def absolute(self): + if not self.is_absolute(): + raise AssertionError("Relative synthetic path has no native working directory") + return self + + +def _target_os(): + module = _FixtureModule("synthetic_target_os") + module.__dict__.update(vars(_host_os)) + module.name = "posix" + # Linux fixture symbols preserve access comparisons, not native Windows flags. + for name, value in {"O_RDONLY": 0, "O_WRONLY": 1, "O_RDWR": 2, "O_ACCMODE": 3, + "O_CREAT": 64, "O_EXCL": 128, "O_TRUNC": 512, "O_APPEND": 1024, + "O_NONBLOCK": 2048, "O_DIRECTORY": 65536, + "O_NOFOLLOW": 131072, "O_CLOEXEC": 524288}.items(): + setattr(module, name, value) + for name in ("getuid", "geteuid", "getpgid", "killpg", "kill", "pread", "open", + "read", "write", "close", "fstat", "stat", "lstat", "fsync", "fdopen", + "lseek", "umask", "access", "mkdir", "rename", "replace", "unlink", + "rmdir", "listdir", "scandir", "fchmod", "dup", "pipe", "set_blocking"): + setattr(module, name, _forbidden_target_effect) + return module + + +def _target_signal(original): + module = _FixtureModule("synthetic_target_signal") + module.__dict__.update(vars(original)) + module.SIGTERM, module.SIGKILL, module.SIGUSR1 = 15, 9, 10 + for name in ("signal", "alarm", "raise_signal", "pidfd_send_signal"): + setattr(module, name, _forbidden_target_effect) + return module + + +def _prepare_target_module(module): + if _host_sys.platform != "win32": + return module + module.os = _TARGET_OS + module.Path = _TargetPath + for name in ("RUN_ROOT", "SOURCE_ROOT", "RUNTIME_PACKET"): + if hasattr(module, name): + value = getattr(module, name) + setattr(module, name, _TargetPath(value.as_posix())) + if hasattr(module, "sys"): + isolated = _FixtureModule("synthetic_target_sys") + isolated.__dict__.update(vars(module.sys)) + module.sys = isolated + if hasattr(module, "signal"): + module.signal = _target_signal(module.signal) + return module + + +_TARGET_OS = _target_os() if _host_sys.platform == "win32" else _host_os +if _host_sys.platform == "win32": + Path = _TargetPath + + +def _inert_fcntl(): + module = _FixtureModule("fcntl") + module.F_GETFL, module.LOCK_EX, module.LOCK_NB = 3, 2, 4 + module.fcntl = module.flock = _forbidden_target_effect + return module + + +def _install_inert_fcntl(test): + if _host_sys.platform == "win32": + stack = ExitStack() + test.addCleanup(stack.close) + stack.enter_context(mock.patch.dict(sys.modules, {"fcntl": _inert_fcntl()})) + + +if _host_sys.platform == "win32": + os = _TARGET_OS + + def load_source(name: str, filename: str) -> types.ModuleType: path = Path(__file__).parent / "native" / filename module = types.ModuleType(name) @@ -25,7 +140,7 @@ def load_source(name: str, filename: str) -> types.ModuleType: compile(path.read_bytes(), str(path), "exec", dont_inherit=True), module.__dict__, ) - return module + return _prepare_target_module(module) POLICY = load_source("native_acceptance_policy_test_v4", "mac-native-acceptance-v4.py") @@ -283,6 +398,9 @@ def main_scope( class PurePolicyTests(unittest.TestCase): + def setUp(self) -> None: + _install_inert_fcntl(self) + def test_directory_sync_live_bound_return_never_closes_or_replaces_prior_binding( self, ) -> None: diff --git a/tests/test-mac-native-acceptance-v6.py b/tests/test-mac-native-acceptance-v6.py index a70d183a..a87e7721 100644 --- a/tests/test-mac-native-acceptance-v6.py +++ b/tests/test-mac-native-acceptance-v6.py @@ -16,6 +16,121 @@ from unittest import mock + +import os as _host_os +import sys as _host_sys +from pathlib import PurePosixPath, PureWindowsPath +from types import ModuleType as _FixtureModule + +_HOST_PATH = Path +_HOST_REPOSITORY = _HOST_PATH(__file__).resolve().parents[1] + + +def _forbidden_target_effect(*args: object, **kwargs: object) -> object: + raise AssertionError("Unmocked synthetic target effect is forbidden") + + +class _TargetPath(PurePosixPath): + def __new__(cls, *args: object): + if args: + first = args[0] + if isinstance(first, _HOST_PATH) or ( + isinstance(first, str) and PureWindowsPath(first).is_absolute() + ): + native = _HOST_PATH(*args) + if (not native.is_absolute() or ".." in native.parts + or not native.is_relative_to(_HOST_REPOSITORY)): + raise AssertionError("Native fixture path outside repository") + return native + return super().__new__(cls, *args) + + lstat = _forbidden_target_effect + stat = _forbidden_target_effect + resolve = _forbidden_target_effect + is_symlink = _forbidden_target_effect + read_bytes = _forbidden_target_effect + read_text = _forbidden_target_effect + write_bytes = _forbidden_target_effect + write_text = _forbidden_target_effect + open = _forbidden_target_effect + exists = _forbidden_target_effect + mkdir = _forbidden_target_effect + unlink = _forbidden_target_effect + + def absolute(self): + if not self.is_absolute(): + raise AssertionError("Relative synthetic path has no native working directory") + return self + + +def _target_os(): + module = _FixtureModule("synthetic_target_os") + module.__dict__.update(vars(_host_os)) + module.name = "posix" + # Linux fixture symbols preserve access comparisons, not native Windows flags. + for name, value in {"O_RDONLY": 0, "O_WRONLY": 1, "O_RDWR": 2, "O_ACCMODE": 3, + "O_CREAT": 64, "O_EXCL": 128, "O_TRUNC": 512, "O_APPEND": 1024, + "O_NONBLOCK": 2048, "O_DIRECTORY": 65536, + "O_NOFOLLOW": 131072, "O_CLOEXEC": 524288}.items(): + setattr(module, name, value) + for name in ("getuid", "geteuid", "getpgid", "killpg", "kill", "pread", "open", + "read", "write", "close", "fstat", "stat", "lstat", "fsync", "fdopen", + "lseek", "umask", "access", "mkdir", "rename", "replace", "unlink", + "rmdir", "listdir", "scandir", "fchmod", "dup", "pipe", "set_blocking"): + setattr(module, name, _forbidden_target_effect) + return module + + +def _target_signal(original): + module = _FixtureModule("synthetic_target_signal") + module.__dict__.update(vars(original)) + module.SIGTERM, module.SIGKILL, module.SIGUSR1 = 15, 9, 10 + for name in ("signal", "alarm", "raise_signal", "pidfd_send_signal"): + setattr(module, name, _forbidden_target_effect) + return module + + +def _prepare_target_module(module): + if _host_sys.platform != "win32": + return module + module.os = _TARGET_OS + module.Path = _TargetPath + for name in ("RUN_ROOT", "SOURCE_ROOT", "RUNTIME_PACKET"): + if hasattr(module, name): + value = getattr(module, name) + setattr(module, name, _TargetPath(value.as_posix())) + if hasattr(module, "sys"): + isolated = _FixtureModule("synthetic_target_sys") + isolated.__dict__.update(vars(module.sys)) + module.sys = isolated + if hasattr(module, "signal"): + module.signal = _target_signal(module.signal) + return module + + +_TARGET_OS = _target_os() if _host_sys.platform == "win32" else _host_os +if _host_sys.platform == "win32": + Path = _TargetPath + + +def _inert_fcntl(): + module = _FixtureModule("fcntl") + module.F_GETFL, module.LOCK_EX, module.LOCK_NB = 3, 2, 4 + module.fcntl = module.flock = _forbidden_target_effect + return module + + +def _install_inert_fcntl(test): + if _host_sys.platform == "win32": + stack = ExitStack() + test.addCleanup(stack.close) + stack.enter_context(mock.patch.dict(sys.modules, {"fcntl": _inert_fcntl()})) + + +if _host_sys.platform == "win32": + os = _TARGET_OS + + def load_source(name: str, filename: str) -> types.ModuleType: path = Path(__file__).parent / "native" / filename module = types.ModuleType(name) @@ -25,7 +140,7 @@ def load_source(name: str, filename: str) -> types.ModuleType: compile(path.read_bytes(), str(path), "exec", dont_inherit=True), module.__dict__, ) - return module + return _prepare_target_module(module) POLICY = load_source("native_acceptance_policy_test_v6", "mac-native-acceptance-v6.py") @@ -449,6 +564,9 @@ def directory_wrapper_frame( class PurePolicyTests(unittest.TestCase): + def setUp(self) -> None: + _install_inert_fcntl(self) + def test_capture_constructor_requires_exact_installed_directory_adapter( self, ) -> None: diff --git a/tests/test-mac-native-acceptance-v7.py b/tests/test-mac-native-acceptance-v7.py index 2e4754c0..af7871d0 100644 --- a/tests/test-mac-native-acceptance-v7.py +++ b/tests/test-mac-native-acceptance-v7.py @@ -16,6 +16,121 @@ from unittest import mock + +import os as _host_os +import sys as _host_sys +from pathlib import PurePosixPath, PureWindowsPath +from types import ModuleType as _FixtureModule + +_HOST_PATH = Path +_HOST_REPOSITORY = _HOST_PATH(__file__).resolve().parents[1] + + +def _forbidden_target_effect(*args: object, **kwargs: object) -> object: + raise AssertionError("Unmocked synthetic target effect is forbidden") + + +class _TargetPath(PurePosixPath): + def __new__(cls, *args: object): + if args: + first = args[0] + if isinstance(first, _HOST_PATH) or ( + isinstance(first, str) and PureWindowsPath(first).is_absolute() + ): + native = _HOST_PATH(*args) + if (not native.is_absolute() or ".." in native.parts + or not native.is_relative_to(_HOST_REPOSITORY)): + raise AssertionError("Native fixture path outside repository") + return native + return super().__new__(cls, *args) + + lstat = _forbidden_target_effect + stat = _forbidden_target_effect + resolve = _forbidden_target_effect + is_symlink = _forbidden_target_effect + read_bytes = _forbidden_target_effect + read_text = _forbidden_target_effect + write_bytes = _forbidden_target_effect + write_text = _forbidden_target_effect + open = _forbidden_target_effect + exists = _forbidden_target_effect + mkdir = _forbidden_target_effect + unlink = _forbidden_target_effect + + def absolute(self): + if not self.is_absolute(): + raise AssertionError("Relative synthetic path has no native working directory") + return self + + +def _target_os(): + module = _FixtureModule("synthetic_target_os") + module.__dict__.update(vars(_host_os)) + module.name = "posix" + # Linux fixture symbols preserve access comparisons, not native Windows flags. + for name, value in {"O_RDONLY": 0, "O_WRONLY": 1, "O_RDWR": 2, "O_ACCMODE": 3, + "O_CREAT": 64, "O_EXCL": 128, "O_TRUNC": 512, "O_APPEND": 1024, + "O_NONBLOCK": 2048, "O_DIRECTORY": 65536, + "O_NOFOLLOW": 131072, "O_CLOEXEC": 524288}.items(): + setattr(module, name, value) + for name in ("getuid", "geteuid", "getpgid", "killpg", "kill", "pread", "open", + "read", "write", "close", "fstat", "stat", "lstat", "fsync", "fdopen", + "lseek", "umask", "access", "mkdir", "rename", "replace", "unlink", + "rmdir", "listdir", "scandir", "fchmod", "dup", "pipe", "set_blocking"): + setattr(module, name, _forbidden_target_effect) + return module + + +def _target_signal(original): + module = _FixtureModule("synthetic_target_signal") + module.__dict__.update(vars(original)) + module.SIGTERM, module.SIGKILL, module.SIGUSR1 = 15, 9, 10 + for name in ("signal", "alarm", "raise_signal", "pidfd_send_signal"): + setattr(module, name, _forbidden_target_effect) + return module + + +def _prepare_target_module(module): + if _host_sys.platform != "win32": + return module + module.os = _TARGET_OS + module.Path = _TargetPath + for name in ("RUN_ROOT", "SOURCE_ROOT", "RUNTIME_PACKET"): + if hasattr(module, name): + value = getattr(module, name) + setattr(module, name, _TargetPath(value.as_posix())) + if hasattr(module, "sys"): + isolated = _FixtureModule("synthetic_target_sys") + isolated.__dict__.update(vars(module.sys)) + module.sys = isolated + if hasattr(module, "signal"): + module.signal = _target_signal(module.signal) + return module + + +_TARGET_OS = _target_os() if _host_sys.platform == "win32" else _host_os +if _host_sys.platform == "win32": + Path = _TargetPath + + +def _inert_fcntl(): + module = _FixtureModule("fcntl") + module.F_GETFL, module.LOCK_EX, module.LOCK_NB = 3, 2, 4 + module.fcntl = module.flock = _forbidden_target_effect + return module + + +def _install_inert_fcntl(test): + if _host_sys.platform == "win32": + stack = ExitStack() + test.addCleanup(stack.close) + stack.enter_context(mock.patch.dict(sys.modules, {"fcntl": _inert_fcntl()})) + + +if _host_sys.platform == "win32": + os = _TARGET_OS + + def load_source(name: str, filename: str) -> types.ModuleType: path = Path(__file__).parent / "native" / filename module = types.ModuleType(name) @@ -25,7 +140,7 @@ def load_source(name: str, filename: str) -> types.ModuleType: compile(path.read_bytes(), str(path), "exec", dont_inherit=True), module.__dict__, ) - return module + return _prepare_target_module(module) POLICY = load_source("native_acceptance_policy_test_v7", "mac-native-acceptance-v7.py") @@ -449,6 +564,9 @@ def directory_wrapper_frame( class PurePolicyTests(unittest.TestCase): + def setUp(self) -> None: + _install_inert_fcntl(self) + def test_capture_constructor_requires_exact_installed_directory_adapter( self, ) -> None: @@ -2039,6 +2157,9 @@ def descriptor_dispatch_scope( class DescriptorDispatchTests(unittest.TestCase): + def setUp(self) -> None: + _install_inert_fcntl(self) + def test_catalog_writers_use_complete_generic_validation(self) -> None: for mode in ("w", "wb"): with ( diff --git a/tests/test-memory-enforcement-native-297.py b/tests/test-memory-enforcement-native-297.py index 4ccb5c69..13b4cb8a 100644 --- a/tests/test-memory-enforcement-native-297.py +++ b/tests/test-memory-enforcement-native-297.py @@ -2,6 +2,7 @@ from __future__ import annotations import ast +from contextlib import ExitStack import importlib.util import json import os @@ -13,6 +14,91 @@ from unittest.mock import patch + +import os as _host_os +import sys as _host_sys +from pathlib import PurePosixPath, PureWindowsPath +from types import ModuleType as _FixtureModule + +_HOST_PATH = Path +_HOST_REPOSITORY = _HOST_PATH(__file__).resolve().parents[1] + + +def _forbidden_target_effect(*args: object, **kwargs: object) -> object: + raise AssertionError("Unmocked synthetic target effect is forbidden") + + +class _TargetPath(PurePosixPath): + def __new__(cls, *args: object): + if args: + first = args[0] + if isinstance(first, _HOST_PATH) or ( + isinstance(first, str) and PureWindowsPath(first).is_absolute() + ): + native = _HOST_PATH(*args) + if (not native.is_absolute() or ".." in native.parts + or not native.is_relative_to(_HOST_REPOSITORY)): + raise AssertionError("Native fixture path outside repository") + return native + return super().__new__(cls, *args) + + lstat = _forbidden_target_effect + stat = _forbidden_target_effect + resolve = _forbidden_target_effect + is_symlink = _forbidden_target_effect + read_bytes = _forbidden_target_effect + read_text = _forbidden_target_effect + write_bytes = _forbidden_target_effect + write_text = _forbidden_target_effect + open = _forbidden_target_effect + exists = _forbidden_target_effect + mkdir = _forbidden_target_effect + unlink = _forbidden_target_effect + + def absolute(self): + if not self.is_absolute(): + raise AssertionError("Relative synthetic path has no native working directory") + return self + + +def _target_os(): + module = _FixtureModule("synthetic_target_os") + module.__dict__.update(vars(_host_os)) + module.name = "posix" + # Linux fixture symbols preserve access comparisons, not native Windows flags. + for name, value in {"O_RDONLY": 0, "O_WRONLY": 1, "O_RDWR": 2, "O_ACCMODE": 3, + "O_CREAT": 64, "O_EXCL": 128, "O_TRUNC": 512, "O_APPEND": 1024, + "O_NONBLOCK": 2048, "O_DIRECTORY": 65536, + "O_NOFOLLOW": 131072, "O_CLOEXEC": 524288}.items(): + setattr(module, name, value) + for name in ("getuid", "geteuid", "getpgid", "killpg", "kill", "pread", "open", + "read", "write", "close", "fstat", "stat", "lstat", "fsync", "fdopen", + "lseek", "umask", "access", "mkdir", "rename", "replace", "unlink", + "rmdir", "listdir", "scandir", "fchmod", "dup", "pipe", "set_blocking"): + setattr(module, name, _forbidden_target_effect) + return module + + +_TARGET_OS = _target_os() if _host_sys.platform == "win32" else _host_os + + +def _prepare_synthetic_bootstrap(test): + if _host_sys.platform != "win32": + return + stack = ExitStack() + test.addCleanup(stack.close) + name = "_memory_bootstrap_synthetic_297" + synthetic = _FixtureModule(name) + synthetic.__file__ = str(_SOURCE) + stack.enter_context(patch.dict(sys.modules, {name: synthetic})) + # Fresh original-source globals keep every existing method-specific mock effective. + exec(compile(_SOURCE.read_bytes(), str(_SOURCE), "exec", dont_inherit=True), synthetic.__dict__) + synthetic.os, synthetic.Path = _TARGET_OS, _TargetPath + stack.enter_context(patch.object(sys.modules[__name__], "bootstrap", synthetic)) + stack.enter_context(patch.object(sys.modules[__name__], "os", _TARGET_OS)) + stack.enter_context(patch.object(sys.modules[__name__], "Path", _TargetPath)) + + _SOURCE = Path(__file__).resolve().parents[1] / "truememory" / "memory_bootstrap.py" _SPEC = importlib.util.spec_from_file_location("_memory_bootstrap_author_297", _SOURCE) assert _SPEC is not None and _SPEC.loader is not None @@ -25,10 +111,13 @@ def expectation() -> object: return bootstrap.LinuxScopeExpectation("/synthetic/owned", 123, 1000, 10, 20, 67108864, 0) -_HAS_SCOPE_FLAGS = all(hasattr(os, name) for name in ("O_DIRECTORY", "O_NOFOLLOW", "O_NONBLOCK")) +_HAS_SCOPE_FLAGS = all(hasattr(_TARGET_OS, name) for name in ("O_DIRECTORY", "O_NOFOLLOW", "O_NONBLOCK")) class SyntheticReadbackControls(unittest.TestCase): + def setUp(self) -> None: + _prepare_synthetic_bootstrap(self) + def test_bootstrap_imports_only_stdlib_and_does_not_activate(self) -> None: tree = ast.parse(_SOURCE.read_bytes()) imports = {node.module.split(".")[0] for node in ast.walk(tree) From 09f99ab0c4903062d87e9aab791416468c122039 Mon Sep 17 00:00:00 2001 From: buildingjoshbetter <236675935+buildingjoshbetter@users.noreply.github.com> Date: Tue, 13 Oct 2026 13:18:41 -0500 Subject: [PATCH 13/16] fix: Stop Windows completion directory recursion at a missing drive root --- .../test-windows-completion-drive-root-768.py | 33 +++++++++++++++++++ truememory/ingest/completion.py | 3 ++ 2 files changed, 36 insertions(+) create mode 100644 tests/test-windows-completion-drive-root-768.py diff --git a/tests/test-windows-completion-drive-root-768.py b/tests/test-windows-completion-drive-root-768.py new file mode 100644 index 00000000..e143a134 --- /dev/null +++ b/tests/test-windows-completion-drive-root-768.py @@ -0,0 +1,33 @@ +"""Windows completion directories refuse a missing drive root without recursing.""" +from __future__ import annotations + +import importlib.util +import os +import string +import sys +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location("windows_completion_drive_root_768", ROOT / "truememory/ingest/completion.py") +assert SPEC is not None and SPEC.loader is not None +COMPLETION = importlib.util.module_from_spec(SPEC) +sys.modules[SPEC.name] = COMPLETION +SPEC.loader.exec_module(COMPLETION) + + +@unittest.skipUnless(os.name == "nt", "drive roots exist only on Windows") +class MissingDriveRoot(unittest.TestCase): + def test_missing_drive_root_is_refused_without_recursion(self) -> None: + free = [letter for letter in reversed(string.ascii_uppercase) if not Path(f"{letter}:\\").exists()] + if not free: + self.skipTest("every drive letter is mapped") + target = Path(f"{free[0]}:\\truememory-missing\\ingest-completion-v1") + with self.assertRaises(COMPLETION.CompletionError) as caught: + COMPLETION._private_directory(target) + self.assertEqual(caught.exception.args, ("unsafe_directory",)) + self.assertFalse(Path(f"{free[0]}:\\").exists()) + + +if __name__ == "__main__": + unittest.main() diff --git a/truememory/ingest/completion.py b/truememory/ingest/completion.py index 981d4e69..46a7a7f7 100644 --- a/truememory/ingest/completion.py +++ b/truememory/ingest/completion.py @@ -77,6 +77,9 @@ def _private_directory(directory: Path) -> None: if os.name == "nt": from truememory._windows_private import create_private_directory if not directory.parent.exists(): + # A missing drive or share root is its own parent; stop instead of recursing. + if directory.parent.parent == directory.parent: + raise CompletionError("unsafe_directory") _private_directory(directory.parent) try: create_private_directory(directory) From 37342c7fa1e3a527154bc0ad305136fe19a7d0c1 Mon Sep 17 00:00:00 2001 From: buildingjoshbetter <236675935+buildingjoshbetter@users.noreply.github.com> Date: Tue, 13 Oct 2026 13:25:32 -0500 Subject: [PATCH 14/16] fix: Treat Claude Code session metadata records as non-conversation --- tests/ingest/test_claude_metadata_records.py | 53 ++++++++++++++++++++ truememory/ingest/transcript.py | 9 ++++ 2 files changed, 62 insertions(+) create mode 100644 tests/ingest/test_claude_metadata_records.py diff --git a/tests/ingest/test_claude_metadata_records.py b/tests/ingest/test_claude_metadata_records.py new file mode 100644 index 00000000..c53174b0 --- /dev/null +++ b/tests/ingest/test_claude_metadata_records.py @@ -0,0 +1,53 @@ +"""Claude Code session metadata records do not make transcript coverage partial.""" +from __future__ import annotations + +import json +from pathlib import Path + +from truememory.ingest.transcript import parse_transcript_outcome + +FIXTURE = Path(__file__).parent / "fixtures" / "sample_real_claude_code_transcript.jsonl" + + +def _write(tmp_path: Path, records: list[dict]) -> Path: + path = tmp_path / "session.jsonl" + path.write_text("".join(json.dumps(record) + "\n" for record in records)) + return path + + +def test_real_format_fixture_is_complete(): + outcome = parse_transcript_outcome(FIXTURE) + assert outcome.status == "complete" + assert outcome.malformed_records == 0 + assert [message.role for message in outcome.messages] == [ + "human", "assistant", "assistant", "tool_result", "human", "assistant"] + + +def test_claude_21_metadata_records_are_not_conversation(tmp_path): + prompt = "Synthetic fixture: the blue widget ships on Tuesday." + records = [ + {"type": "permission-mode", "permissionMode": "default"}, + {"type": "queue-operation", "operation": "enqueue", "content": prompt}, + {"type": "attachment", "attachment": {"type": "hook_success", "hookEvent": "SessionStart", "content": ""}}, + {"type": "attachment", "attachment": {"type": "date", "date": "2026-10-13"}}, + {"type": "atis-latch", "atis": ""}, + {"type": "user", "message": {"role": "user", "content": prompt}}, + {"type": "assistant", "message": {"role": "assistant", "content": [{"type": "text", "text": "Noted."}]}}, + {"type": "last-prompt", "lastPrompt": prompt}, + {"type": "cost-state", "totalCostUSD": 0}, + ] + outcome = parse_transcript_outcome(_write(tmp_path, records)) + assert outcome.status == "complete" + assert outcome.malformed_records == 0 and outcome.unrecognized_records == 0 + assert [(message.role, message.content) for message in outcome.messages] == [ + ("human", prompt), ("assistant", "Noted.")] + + +def test_unlisted_metadata_type_still_counts_as_malformed(tmp_path): + records = [ + {"type": "user", "message": {"role": "user", "content": "Synthetic fixture prompt."}}, + {"type": "future-metadata", "value": 1}, + ] + outcome = parse_transcript_outcome(_write(tmp_path, records)) + assert outcome.status == "partial" + assert outcome.malformed_records == 1 diff --git a/truememory/ingest/transcript.py b/truememory/ingest/transcript.py index 624e419b..76f83a55 100644 --- a/truememory/ingest/transcript.py +++ b/truememory/ingest/transcript.py @@ -1099,6 +1099,15 @@ def _parse_jsonl(text: str) -> list[Message]: "progress", "summary", "system", + # Claude Code 2.1 session metadata. queue-operation and last-prompt repeat + # the prompt that is also written as the user record; the others carry + # host state. Unlisted future types still count as malformed. + "atis-latch", + "attachment", + "cost-state", + "last-prompt", + "permission-mode", + "queue-operation", } From c4f102feff523882533fed5b3f344c1c43ae2f76 Mon Sep 17 00:00:00 2001 From: buildingjoshbetter <236675935+buildingjoshbetter@users.noreply.github.com> Date: Tue, 13 Oct 2026 13:35:57 -0500 Subject: [PATCH 15/16] fix: Skip only known Claude context attachments and recorded queued prompts --- tests/ingest/test_claude_metadata_records.py | 28 +++++++++++ truememory/ingest/transcript.py | 49 ++++++++++++++++++-- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/tests/ingest/test_claude_metadata_records.py b/tests/ingest/test_claude_metadata_records.py index c53174b0..7b17e482 100644 --- a/tests/ingest/test_claude_metadata_records.py +++ b/tests/ingest/test_claude_metadata_records.py @@ -51,3 +51,31 @@ def test_unlisted_metadata_type_still_counts_as_malformed(tmp_path): outcome = parse_transcript_outcome(_write(tmp_path, records)) assert outcome.status == "partial" assert outcome.malformed_records == 1 + + +def test_unknown_attachment_type_is_not_assumed_benign(tmp_path): + records = [ + {"type": "user", "message": {"role": "user", "content": "Synthetic fixture prompt."}}, + {"type": "attachment", "attachment": {"type": "queued_command", "prompt": "Synthetic queued text."}}, + ] + outcome = parse_transcript_outcome(_write(tmp_path, records)) + assert outcome.status == "partial" + assert outcome.malformed_records == 1 + + +def test_attachment_with_top_level_text_is_still_captured(tmp_path): + records = [ + {"type": "user", "message": {"role": "user", "content": "Synthetic fixture prompt."}}, + {"type": "attachment", "attachment": {"type": "date"}, "content": "Synthetic attached text."}, + ] + outcome = parse_transcript_outcome(_write(tmp_path, records)) + assert "Synthetic attached text." in [message.content for message in outcome.messages] + + +def test_unrecorded_queued_prompt_is_still_captured(tmp_path): + records = [ + {"type": "user", "message": {"role": "user", "content": "Synthetic fixture prompt."}}, + {"type": "queue-operation", "operation": "enqueue", "content": "Synthetic prompt never dequeued."}, + ] + outcome = parse_transcript_outcome(_write(tmp_path, records)) + assert "Synthetic prompt never dequeued." in [message.content for message in outcome.messages] diff --git a/truememory/ingest/transcript.py b/truememory/ingest/transcript.py index 76f83a55..5ac0892c 100644 --- a/truememory/ingest/transcript.py +++ b/truememory/ingest/transcript.py @@ -935,6 +935,7 @@ def _parse_transcript_text_outcome(text: str, *, jsonl: bool = False) -> Transcr messages: list[Message] = [] malformed = unrecognized = 0 native_records: list[tuple[int, _CodexRecord]] = [] + prompts = _recorded_prompts(entries) for index, entry in enumerate(entries): if isinstance(entry, str) and allow_strings: messages.append(Message(role="unknown", content=entry)) @@ -954,6 +955,8 @@ def _parse_transcript_text_outcome(text: str, *, jsonl: bool = False) -> Transcr continue if isinstance(top_type, str) and top_type in _NON_CONVERSATION_TYPES: continue + if _claude_context_record(entry, prompts): + continue # The supported older tool-call format has name/input instead of # content. Keep its existing _extract_message behavior intact. if top_type != "tool_use": @@ -1099,17 +1102,53 @@ def _parse_jsonl(text: str) -> list[Message]: "progress", "summary", "system", - # Claude Code 2.1 session metadata. queue-operation and last-prompt repeat - # the prompt that is also written as the user record; the others carry - # host state. Unlisted future types still count as malformed. + # Claude Code 2.1 session metadata without conversation text. last-prompt + # only repeats the leaf user record. Unlisted future types still count as + # malformed. "atis-latch", - "attachment", "cost-state", "last-prompt", "permission-mode", - "queue-operation", } +# Claude Code 2.1 attachment types observed carrying host context, not user +# text. Other attachment types keep their previous parsing and coverage. +_CLAUDE_CONTEXT_ATTACHMENTS = frozenset({ + "agent_listing_delta", "auto_mode", "date", "deferred_tools_delta", + "environment", "hook_success", "model", "prompt_snapshot", + "remote_session_change", "session_context", "skill_listing", + "total_tokens_reminder", +}) + + +def _recorded_prompts(entries: list[object]) -> set[str]: + prompts: set[str] = set() + for entry in entries: + if not isinstance(entry, dict) or entry.get("type") != "user" or not isinstance(entry.get("message"), dict): + continue + content = entry["message"].get("content") + if isinstance(content, str): + prompts.add(content) + elif isinstance(content, list): + texts = [block.get("text") for block in content + if isinstance(block, dict) and block.get("type") == "text" and isinstance(block.get("text"), str)] + prompts.update(texts) + prompts.add("".join(texts)) + return prompts + + +def _claude_context_record(entry: dict, prompts: set[str]) -> bool: + """True when a Claude Code record holds host context or an already recorded prompt.""" + kind = entry.get("type") + if kind == "attachment": + attachment = entry.get("attachment") + return ("content" not in entry and isinstance(attachment, dict) + and attachment.get("type") in _CLAUDE_CONTEXT_ATTACHMENTS) + if kind == "queue-operation": + content = entry.get("content") + return content is None or (isinstance(content, str) and content in prompts) + return False + def _extract_message(entry: dict) -> Message | None: """Extract a Message from a conversation turn dict. From 001e8f1e3b4e8e8c021028d47069577c69994df1 Mon Sep 17 00:00:00 2001 From: buildingjoshbetter <236675935+buildingjoshbetter@users.noreply.github.com> Date: Tue, 13 Oct 2026 13:45:13 -0500 Subject: [PATCH 16/16] fix: Count attachments with non-string types as malformed --- tests/ingest/test_claude_metadata_records.py | 11 +++++++++++ truememory/ingest/transcript.py | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/tests/ingest/test_claude_metadata_records.py b/tests/ingest/test_claude_metadata_records.py index 7b17e482..0a83e43b 100644 --- a/tests/ingest/test_claude_metadata_records.py +++ b/tests/ingest/test_claude_metadata_records.py @@ -79,3 +79,14 @@ def test_unrecorded_queued_prompt_is_still_captured(tmp_path): ] outcome = parse_transcript_outcome(_write(tmp_path, records)) assert "Synthetic prompt never dequeued." in [message.content for message in outcome.messages] + + +def test_attachment_with_unhashable_type_counts_as_malformed(tmp_path): + records = [ + {"type": "user", "message": {"role": "user", "content": "Synthetic fixture prompt."}}, + {"type": "attachment", "attachment": {"type": ["date"]}}, + {"type": "attachment", "attachment": {"type": {"name": "date"}}}, + ] + outcome = parse_transcript_outcome(_write(tmp_path, records)) + assert outcome.status == "partial" + assert outcome.malformed_records == 2 diff --git a/truememory/ingest/transcript.py b/truememory/ingest/transcript.py index 5ac0892c..92c2bbf3 100644 --- a/truememory/ingest/transcript.py +++ b/truememory/ingest/transcript.py @@ -1143,7 +1143,8 @@ def _claude_context_record(entry: dict, prompts: set[str]) -> bool: if kind == "attachment": attachment = entry.get("attachment") return ("content" not in entry and isinstance(attachment, dict) - and attachment.get("type") in _CLAUDE_CONTEXT_ATTACHMENTS) + and isinstance(attachment.get("type"), str) + and attachment["type"] in _CLAUDE_CONTEXT_ATTACHMENTS) if kind == "queue-operation": content = entry.get("content") return content is None or (isinstance(content, str) and content in prompts)