Skip to content

feat(storage): the plugin runs its own PostgreSQL (theseus binaries, Cortex-built pgvector), shared by every surface (#744, #740) - #748

Open
cdeust wants to merge 5 commits into
mainfrom
feat/744-pg0-launcher
Open

cdeust wants to merge 5 commits into
mainfrom
feat/744-pg0-launcher

Conversation

@cdeust

@cdeust cdeust commented Oct 13, 2026 •

Copy link
Copy Markdown
Owner

Closes #744 and #740 (reproducing the locked-down Mac).

Five commits: 6214187b (round 3: theseus binaries replace pg0, ADR-1110), d6f869d8 (D4: Cortex CI compiles pgvector; the release assets the pins name were attested from this exact commit), 52c3156e (round 4), e2909803 (round 5) and the round-6 fix on top. ADR-1110 (wiki page and its mirror) states every decision below.

Round 6: the round-5 review (B5, N8, N9), the red CI job, the orphans

Red on e290980 Green on this head
B5 start() removed the postmaster.pid of a live postmaster Cortex did not start real PG 18.6, postmaster started by hand (reviewer's unlocked_postmaster.py): attach → FATAL: pre-existing shared memory block … is still in use, postmaster_pid_file: None same probe: pid <pid> on port <port> is a live postmaster of <cluster> that Cortex did not start (its start time is the one postmaster.pid records); nothing was removed or signalled: stop it (pg_ctl -D <cluster> stop); the file still names it, the postmaster still runs. Test: test_a_live_postmaster_cortex_did_not_start_is_named_and_left_alone
N8 a stale ready file naming a later-started live PID was dialled reviewer's holder_mismatch.py: B saw: ['dialled'] test_a_ready_file_whose_pid_started_later_is_stale_and_never_dialled: such a file is not ready for Cortex, so nothing is dialled
HOL plugin scan (CI red, Findings met or exceeded the "high" severity threshold) pinned scanner 3.43.1 (wheel sha256 = the action's pin), run locally: PR tree HARDCODED_SECRET (high) at tests_py/infrastructure/test_managed_pg_protocol.py:25, score 96; main: 0 findings, score 100. Introduced by round 5 (_PASSWORD = "topsecret-cluster-password") the test password is secrets.token_hex(24) at run time; scanner on this head: 0 findings, score 100. MIN_SCORE/FAIL_ON untouched
Orphan fakes (4 found by the reviewer: 3 from my mutation runs, 1 from the gate's nested worktree) ps: ppid 1, cwd …/mutr5 and …/zetetic-fail-before-7955 killed (all mine); every fake postmaster now follows a lifeline: the autouse fixture fake_pg_lifeline holds an exclusive flock for the test's life, each fake waits for a shared lock and exits when granted. Measured: pytest SIGKILLed mid-test → its fake exits (kqueue NOTE_EXIT); 26-mutant table and Linux runs: 0 fakes left

How B5/N8 are decided, with no dialling and no clock: a postmaster.pid was written by the live process it names when that process started no later than the file's line 3 (MyStartTime, whole seconds after exec) plus 2 s. The 2 s are 1 s for whole seconds and 1 s for Linux btime, which is whole seconds. Measured kernel start minus a whole-second clock read by the process itself, 20 runs each:

Platform Max
macOS arm64 0.911 s
Linux arm64 −0.190 s
Linux x86_64 under qemu −2.520 s

A process that started later is a reused PID: the file is stale and is removed (only while Cortex holds postmaster.lock). A live writer is a postmaster of this cluster.

  • Deviation from "adopt it if it signs" (preferred in the brief). A live writer that Cortex did not start is named and left alone, not adopted. It holds no postmaster.lock, so the supervisor would have no death event: released() would fire at once and restart-loop. stop() would also return while that postmaster is still shutting down. And stopping a server the user started by hand would itself act on a process Cortex did not start.
  • Who gets dialled. The only unrecorded process ever dialled is a proven writer of a ready file while an unrecorded group holds the lock (N2's orphan). It is adopted only if it signs with the cluster password.
  • N9, now in the ADR Consequences: a group that holds postmaster.lock unrecorded, and neither exits nor gets a proven writer to ready, blocks attach for as long as it lives (D2: no clock decides a start failed).

Round 5 (kept for reference): the round-4 review (B4, N1–N5), red then green

Red on 52c3156 Green on this head
B4 the D2 mutant was killed only by the 300 s pytest-timeout D2 row of the round-4 table: Timeout Popen.poll now raises in test_the_exit_is_read_from_the_pipe_not_from_waitpid: the mutant fails at once with AssertionError: poll() decided the exit (table below, no Timeout line anywhere)
N1 server-signature and nonce checks untested mutants SIG and NONCE: SURVIVED rc 0 ['19 passed'] each test_managed_pg_protocol.py: hostile servers (wrong signature, foreign nonce, trust, cleartext request, malformed server-first) are all refused, the password never travels; SIG and NONCE killed. A malformed server-first message is now a ProtocolError, not a KeyError
N2 orphan whose supervisor died before recording it probe: start() raised ServerError … still runs (it holds postmaster.lock) (no pid, no port) and postmaster.pid still present: False probe: start() -> <port> adopted, postmaster.pid still present: True; real PG 18.6 (the reviewer's killed_supervisor.py, ported): the orphan is adopted and recorded, no lock file is invalid in postgres.log, stopped after the last client. A lock holder that cannot authenticate: ServerError: pid <pid> on port <port> holds postmaster.lock … not adopted, signalled or removed
N3 postmaster death under live clients probe: status still running, client B: port … refused probe: B port … accepts (same port); real PG 18.6 (reviewer's pm_crash.py): B (new client after the crash): connect ok 127.0.0.1:65330 — the port A had
N4 branch could create the release; provenance names a branch publish if: github.repository == 'cdeust/Cortex' publish also requires github.ref == 'refs/heads/main'; docs and release notes verify with --source-ref refs/heads/main
N5 raw traceback from python -m mcp_server and the Codex hook traceback ending in ManagedPgError: … missing; run \claude --init-only`` [cortex-mcp] Cortex not ready: … run \claude --init-only`/[cortex-hook] …`, exit 1, one line

How N2 and N3 work now:

  • N2. start() takes postmaster.lock before touching postmaster.pid (and since round 6 removes the file only when proven stale, see above). If a group holds it while nothing recorded it, Cortex waits on events: either that group exits (Cortex then starts its own), or a proven writer of its postmaster.pid (kernel start time = line 3) reports ready. That writer is the one unrecorded process ever dialled; it is adopted and recorded only if it authenticates with the cluster password.
  • N3, supervisor side. The supervisor waits for two kernel events: the last client releasing clients.lock, or the postmaster's group releasing postmaster.lock. On a death it holds the start lock, reaps the dead child, and starts a new postmaster on the same port when that port is free. A restart that fails writes failed, and the next client starts a new supervisor.
  • N3, client side. A client that reads running checks the recorded postmaster against kernel facts. If it is dead, the client waits for the next change in the root directory, or for the supervisor's exit.
  • Remaining window. Between a postmaster's death and the restart, a client already holding the URL sees connection errors. It recovers when the same port is rebound.

Round 4 (kept for reference): the round-3 blockers, red then green

Red on 6214187 Green on this head
B1 Setup with a managed marker ignored database_url round-3 e1.py: provision called: ['…/data/pg'], Setup fails e1.py: provision called: [], said: managed PostgreSQL not selected: database_url … selects the store; the managed server is not started; test test_a_configured_database_wins_over_a_managed_marker_in_setup
B2 a foreign PID + any listener was adopted, stop() ran pg_ctl stop on it foreign.py: live_postmaster -> (31490, 54222); stopprobe.py: stop() invoked pg_ctl: -D … -m fast -w stop test_start_never_adopts_or_dials_a_foreign_listener, test_stop_never_signals_a_foreign_process (the foreign process is neither signalled nor dialled), test_a_reused_pid_with_a_listener_is_not_ours, test_an_unrecorded_pid_with_a_listener_is_not_ours
B3 python -m mcp_server.doctor crashed on a managed marker BackendMarkerError: the backend marker selects the plugin's managed PostgreSQL server … (traceback) [FAIL] Managed PostgreSQL server … run \claude --init-only`with a fix line, no traceback;test_the_doctor_reports_the_managed_server_instead_of_crashing`
M3 PermissionError mutant survived round-3 mut.py M3 rerun on 6214187: rc 0 ['9 passed'] test_permission_error_is_not_ours_even_with_a_listener kills it (table below)
D3 fallback invisible Setup outcome {"status": "ready", "error": ""} {"status": "ready", "error": "", "sqlite_fallback": "managed PostgreSQL did not start: …"} + [cortex-setup] SQLite fallback: …; SessionStart adds the line SQLite fallback: <cause>

Owner decisions

  • B2 + D2, one invariant. A postmaster is ours only when the kernel proves it: the supervisor spawns bin/postgres -D <cluster> -p <port> as its own child and records {pid, started, port}, started being the kernel's start time of that PID (proc_pidinfo, /proc/<pid>/stat; a zombie has none). Only after that proof is the port dialled (the one unrecorded process ever dialled is the proven writer of a ready postmaster.pid while an unrecorded group holds postmaster.lock, see N2 and round 6 above), and the server must finish SCRAM-SHA-256 with the cluster's password, server signature and nonce included (managed_pg_protocol, stdlib). No pg_ctl any more: readiness is postmaster.pid reporting ready (kqueue / inotify events); failure is end of file on a pipe whose write end only the postmaster's process group holds; stop is SIGINT then a flock on postmaster.lock, granted once the postmaster and every child it forked are gone. No sleep, timeout or attempt cap anywhere; the managed-pg CI job lost its timeout-minutes. No pidfd: it is ENOSYS under qemu-user (measured).
  • D1. The attach code moved into the wheel (mcp_server/infrastructure/managed_pg*.py, renames of scripts/launcher_pg*.py). apply_backend_resolution attaches on a managed marker, so hypermnesia-mcp, the Codex hook entry (mcp_server.hooks.entry, what runtime.py executes — runtime.py itself needed no change) and cortex-doctor use the same locks and supervisor as the launcher. effective_backend stays pure (postgresql, starts nothing). Tests: test_a_codex_hook_attaches_to_the_managed_server, test_claude_and_codex_on_one_home_share_one_server; the e2e checks direct_launch_same_url.
  • D3. sqlite_fallback in <key>.setup.json (kept if a later step fails) and the SessionStart line, only while the store is SQLite.
  • D4. .github/workflows/pgvector.yml + scripts/pgvector_build.py: pgvector v0.8.7 (commit f37c13f6 verified; 0.8.5 → 0.8.7 for the IVFFlat build buffer overflow fixed upstream), built with PGXS against the pinned theseus 18.6 tree on macos-15, macos-15-intel, ubuntu-22.04 and ubuntu-22.04-arm, OPTFLAGS="", load-proved with postgres --single, glibc ≤ 2.34 checked, deterministic archive, actions/attest-build-provenance, published once as prerelease pgvector-v0.8.7-pg18.6.0 (run 39017072528; an existing release is never modified). The four pinned digests are those of the published assets. Since round 5 the publish job runs only on refs/heads/main; this release predates that rule and was created from the PR branch (attestations name feat/744-pg0-launcher, commits d6f869d8 and 52c3156e, identical digests: the build is reproducible). The main push after the merge rebuilds, attests from main and prints release exists: nothing replaced, after which gh attestation verify <archive> --repo cdeust/Cortex --source-ref refs/heads/main passes for the same digests (not verifiable before the merge).
  • D5. Cluster root ~/.claude/methodology/managed-pg (data, password, state, locks, logs); binaries stay in <plugin data>/pg/dist. Uninstalling the plugin never deletes memories; has_earlier_state ignores managed-pg.

Mutation table on this head (b7baad55; one guard at a time, targeted tests, restored after; no Timeout line, no fake left after any mutant)

Mutation Killed by
M1 marker written despite a failed provision test_a_server_that_does_not_start_selects_nothing
M2a / M2b option mapping skipped test_setup_with_the_hook_option_is_not_eligible / test_the_hook_option_wins_over_a_managed_marker_and_starts_nothing
M3 PermissionError read as alive test_permission_error_is_not_ours_even_with_a_listener
M4 sha256 not compared test_a_digest_mismatch_is_refused_and_nothing_installed
M5 _init_schema ready despite DDL errors test_one_failed_statement_fails_and_names_it_after_running_all
M6a / M6b recorded identity never matches / start time ignored test_a_recorded_live_postmaster_is_ours / test_a_reused_pid_with_a_listener_is_not_ours
M7 tar member check disabled test_a_link_leaving_the_tree_is_refused
M8 an existing SQLite store ignored test_an_existing_sqlite_store_is_never_switched
B1 Setup reads the managed marker first test_a_configured_database_wins_over_a_managed_marker_in_setup
B2-start / B2-stop whatever postmaster.pid names test_start_never_adopts_or_dials_a_foreign_listener / test_stop_never_signals_a_foreign_process
B3 doctor without the managed check test_the_doctor_reports_the_managed_server_instead_of_crashing
B5 a live writer's postmaster.pid removed test_a_live_postmaster_cortex_did_not_start_is_named_and_left_alone
D1 direct launches do not attach test_a_codex_hook_attaches_to_the_managed_server
D2 exit decided by waitpid test_the_exit_is_read_from_the_pipe_not_from_waitpid (assertion, 28 s)
SIG / NONCE SCRAM checks removed test_a_hostile_server_is_refused_and_never_gets_the_password[badsig] / [badnonce]
N2 postmaster.pid removed before the lock / unrecorded holder never recovered test_an_unrecorded_lock_holder_that_cannot_sign_is_named_and_left_alone / test_an_unrecorded_postmaster_holding_the_lock_is_proven_and_adopted
N3 supervisor gives up / client trusts running test_a_postmaster_that_dies_under_clients_is_started_again / test_a_client_never_takes_a_running_status_whose_postmaster_is_dead
N5 direct launch without the one-line exit test_a_managed_server_that_cannot_run_ends_a_launch_in_one_line[cortex-mcp]
N8 a later-started PID taken as the file's writer test_a_ready_file_whose_pid_started_later_is_stale_and_never_dialled
D3 fallback cause not recorded test_an_eligible_store_left_on_sqlite_records_why
D5 managed root counted as earlier state test_the_managed_root_is_not_earlier_cortex_state

Not claimed (killable only by a clock, as the round-5 review confirmed): a supervisor that silently ignores the death event.

Measured on this head (b7baad55)

  • e2e (managed_pg_e2e.py, sandbox-exec no_homebrew.sb, env -i, fresh HOME): macOS arm64 and x86_64 (Rosetta) passed: true, vector 0.8.7, pg_trgm 1.6, direct_launch_same_url: true, stopped, no postmaster.pid left; Ubuntu 24.04 arm64 container passed: true.
  • Real PG 18.6: B5 as quoted; the reviewer's killed_supervisor.py (N2: the orphan, a proven writer, is adopted and recorded, stopped after the last client) and pm_crash.py (N3: B gets A's port).
  • Lifecycle tests on Linux arm64 and Linux x86_64 under qemu (non-root, no init): 27 passed each, 0 fake postmasters left.
  • Targeted pytest (launcher, setup_db, hooks entry, session-start status, backend marker, managed_pg, bootstrap, pgvector): 572 passed, 1 skipped on this head.
  • ruff, ruff format --check, check_craftsmanship --base origin/main, check_doc_claims, check_version_surfaces, check_ci_gate_complete, check_project_wiki, check_no_deps_invariant: OK; pyright 1.1.414 (uv.lock typecheck env), Linux and Darwin: 0 errors. HOL plugin scanner 3.43.1: 0 findings, score 100.
  • Not run locally: the full suite (CI is the authority).

Reported, not changed (owner's call)

  • N6: without timeout-minutes, a hung managed-pg job ends at GitHub's 360-minute default; no verdict depends on it.
  • N7: mitigated since round 5 (a client checks the recorded postmaster with kernel facts).
  • Residual: after a postmaster death, a client's pending shared-lock thread on supervisor.lock can hold it for an instant when that supervisor exits; a concurrent first attach then fails loudly and the next succeeds.

Known limits (unchanged unless stated)

Linux distributions without libxml2.so.2, OpenSSL 3 or libgssapi_krb5.so.2 stay on SQLite, with the cause shown. Native Windows and musl stay on SQLite. A fresh install that skips claude --init-only keeps SQLite until the importer exists. Old dist/ trees are not collected.

🤖 Generated with Claude Code

https://claude.ai/code/session_01BYyqzx4FVMHasVPZV3uBYq

@cdeust

cdeust commented Oct 13, 2026

Copy link
Copy Markdown
Owner Author

ZETETIC-REVIEW: REQUEST_CHANGES
head: cb06c40

Reviewed against #744's acceptance and the loop's standard. The machinery is complete and measured (pinned asset + sha256, one flock-supervised server per install, TCP 127.0.0.1, generated password, schema through setup_db, loud failure, zero pg0 writes outside $CLAUDE_PLUGIN_DATA, zero pg0 processes after the last session, Homebrew paths removed), but the issue's own target is not met:

  1. macOS without Homebrew does not start. pg0 v0.15.2's postgres, libpq.5.dylib and eight more files load /opt/homebrew/opt/openssl@3 (arm64) or /usr/local/opt/openssl@3 (x86_64) by absolute path (MacOS: libssl / libcrypto are hardcoded to homebrew, fails on systems without homebrew theseus-rs/postgresql-binaries#30, open). Measured in the Reproduce the org-managed work Mac locally (enforced sandbox, no Homebrew, no model downloads) as the acceptance environment #740 environment with Homebrew hidden: dyld: Library not loaded: /opt/homebrew/opt/openssl@3/lib/libssl.3.dylib; x86_64 under Rosetta: /usr/local/opt/openssl@3/lib/libssl.3.dylib. remember→recall is green only where Homebrew's openssl@3 exists.
  2. x86_64 is therefore not covered (asset selection, digest and extraction are; the server does not start).
  3. "Nothing written outside $CLAUDE_PLUGIN_DATA" holds for pg0 only: the session still writes 9 (first start) + 6 (per session) paths under ~/.claude/methodology, pre-existing and unchanged here (Storage: migrate every existing store (SQLite, local and remote PostgreSQL) into pg0 automatically, with no user operation #745 scope).

The fix for 1–2 is an owner decision (relink and re-sign a third-party binary against the uv.lock-pinned OpenSSL that psycopg-binary already ships, wait for a fixed upstream release, or accept a documented Homebrew openssl@3 requirement); options and costs are in the PR body. Not mergeable as a closure of #744 until that decision is made and implemented.

Comment thread scripts/launcher_pg0_supervisor.py Fixed
@cdeust

cdeust commented Oct 16, 2026

Copy link
Copy Markdown
Owner Author

ZETETIC-REVIEW: REQUEST_CHANGES
head: f33db02

  1. Storage: replace Homebrew PostgreSQL with pg0 installed and started by the launcher #744 n'est pas satisfaite. Le PostgreSQL de pg0 v0.15.2 pour macOS charge openssl@3 par un chemin Homebrew absolu ; sur un Mac sans Homebrew, pg0 start échoue (dyld). C'est une régression par rapport à main, où le repli SQLite s'applique (ADR-1104). L'échec devient en plus permanent, car state.json est écrit avant le start (launcher_pg0_supervisor.py:154-156).
  2. La base est antérieure à fix(install): a marketplace install reaches "Cortex ready" with no other step (ADR-1104) #749 : conflits dans 17 fichiers. La PR fait « pg0 sauf configuration » alors que main fait « SQLite sauf configuration » ; l'ordre de sélection est à redéfinir par rapport à ADR-1104.
  3. Avec database_url renseigné, les hooks ignorent CLAUDE_PLUGIN_OPTION_DATABASE_URL et démarrent pg0 pendant que le serveur MCP utilise la base de l'utilisateur : deux bases différentes.
  4. Un postmaster.pid périmé dont le PID a été réutilisé : la PermissionError renvoyée par kill(pid,0) est traitée comme « vivant », ce qui bloque tant que l'utilisateur ne supprime pas le fichier (lignes 89-90, 161-165).
    Mineurs : mot de passe passé en argument de pg0 start -P (visible dans ps) ; un store SQLite déplacé (CORTEX_MEMORY_DB_PATH) est ignoré par la sélection ; _init_schema relègue les erreurs DDL en avertissements.

… binaries; pg0 dropped (#744, #740)

pg0 0.15.2's macOS PostgreSQL loads Homebrew's OpenSSL by absolute path and
dies in dyld on a Mac without Homebrew (reproduced under sandbox-exec).
theseus-rs 18.6.0 bundles OpenSSL (@loader_path); Setup now downloads it and
pgvector_compiled v0.18.320 (sha256-pinned), runs initdb --pwfile and pg_ctl
itself, creates vector, pg_trgm and the schema, and only then writes the
`managed` backend marker (ADR-1110, amends ADR-1104 decision 1).

- A configured database always wins (CLAUDE_PLUGIN_OPTION_DATABASE_URL mapped
  for hooks and Setup); a failed provision leaves SQLite selected.
- Stale postmaster.pid classified by identity (EPERM is not ours; executable
  or, under emulation, the recorded loopback port); removed before a start,
  never on stop.
- No migration: only a store that does not exist yet becomes managed;
  eligibility is rechecked right before the marker is written.
- setup_db._init_schema fails on any failed DDL statement.
- Linux glibc needs the distribution's libxml2.so.2/OpenSSL 3 (else SQLite,
  cause in setup.log); Windows and musl keep SQLite.
- CI: managed-pg job (macOS under no_homebrew.sb, Linux x86_64/aarch64);
  install-e2e checks the first session names the PostgreSQL store.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYyqzx4FVMHasVPZV3uBYq
@cdeust
cdeust force-pushed the feat/744-pg0-launcher branch from f33db02 to 6214187 Compare October 17, 2026 06:28
@cdeust cdeust changed the title feat(storage): the plugin runs its own pg0 PostgreSQL server; reproduce the locked-down Mac (#744, #740) feat(storage): the plugin runs its own PostgreSQL from pinned theseus binaries, pg0 dropped (#744, #740) Oct 17, 2026
@cdeust

cdeust commented Oct 17, 2026

Copy link
Copy Markdown
Owner Author

ZETETIC-REVIEW: REQUEST_CHANGES
head: 6214187

Revue adversariale PR #748 (round 3) — ADR-1110, theseus + pgvector_compiled

Revue en lecture seule d'un worktree détaché .claude/worktrees/review-748 (supprimé en fin de revue). CI de la PR à ce head : tous les jobs verts (Managed PostgreSQL x3, Install E2E x3, Craftsmanship, Type Check, Test 3.10 à 3.13, SQLite, Windows). Les trois bloquants ci-dessous ne sont couverts par aucun test existant : ils ont été trouvés par contre-exemple, pas par la CI.

1. Statut des bloquants du verdict précédent (2026-10-16)

# Bloquant précédent Statut Preuve
1 openssl/dyld + échec permanent (état écrit avant le start) RÉSOLU Ré-exécution par moi du e2e de l'auteur sous sandbox-exec no_homebrew.sb, env -i, HOME neuf, python uv (hors /opt/homebrew) : passed: true, PG 18.6 aarch64, ssl_library: OpenSSL, vector 0.8.5, pg_trgm 1.6, 23 tables, status: stopped, pas de postmaster.pid résiduel (34 s). Mutation M1 (marker écrit malgré l'échec) tuée. Le marker managed n'est écrit qu'après start + extensions + schéma ; un échec écrit seulement provisioned.json: failed.
2 ordre de sélection vs ADR-1104 PARTIEL ADR-1110 amende ADR-1104 proprement (docs + wiki). Lancement : OK. Chemin Setup avec marker déjà managed : viole l'ordre (B1).
3 hooks ignorent CLAUDE_PLUGIN_OPTION_DATABASE_URL PARTIEL map_database_option appliqué en premier dans apply() et dans l'éligibilité Setup (M2a, M2b tuées). Le chemin Setup « déjà managed » l'ignore (B1).
4 postmaster.pid périmé / PermissionError lu comme vivant PARTIEL Pid mort, fichier tronqué, pid réutilisé sans écouteur : OK (M6b tuée). PermissionError : le code est correct mais le mutant survit (N3). Nouveau trou : pid vivant quelconque + port en écoute quelconque est adopté (B2).
mineur mot de passe dans argv RÉSOLU initdb --pwfile, fichier créé O_EXCL 0600, root 0700, JSON d'état 0600 (lu dans le code).
mineur CORTEX_MEMORY_DB_PATH ignoré RÉSOLU sqlite_store_paths couvre DB_PATH et SQLITE_FALLBACK_PATH ; M8 tuée.
mineur _init_schema démotait les erreurs DDL RÉSOLU M5 tuée ; toutes les instructions s'exécutent, toute erreur fait échouer.

2. Table de mutation (une garde à la fois, tests ciblés, chaque mutation annulée ensuite ; git status propre après)

Mutation Fichier Test qui la tue Résultat
M1 marker écrit malgré provision échouée launcher_pg_provision.py test_a_server_that_does_not_start_selects_nothing TUÉE
M2a éligibilité Setup sans mapping de l'option launcher_pg_select.py test_setup_with_the_hook_option_is_not_eligible TUÉE
M2b apply() sans mapping de l'option launcher_backend.py test_the_hook_option_wins_over_a_managed_marker_and_starts_nothing TUÉE
M3 PermissionError => vivant launcher_pg_server.py aucun (9 passed) SURVIT (le test « pid d'un autre utilisateur » passe par le repli sur le port, qui masque la mutation)
M4 sha256 non comparé launcher_pg_assets.py test_a_digest_mismatch_is_refused_and_nothing_installed TUÉE
M5 _init_schema « ready » malgré erreurs DDL setup_db.py test_one_failed_statement_fails_and_names_it_after_running_all TUÉE
M6a identité loopback supprimée launcher_pg_server.py test_an_emulated_postmaster_is_recognised_by_its_listening_port TUÉE
M6b identité = tout pid vivant launcher_pg_server.py test_a_reused_pid_of_this_user_is_not_a_postmaster TUÉE (cas sans écouteur seulement ; le cas avec écouteur n'est pas testé, cf. B2)
M7 contrôle des membres tar désactivé launcher_pg_assets.py test_a_link_leaving_the_tree_is_refused TUÉE
M8 éligibilité ignore un store SQLite existant launcher_pg_select.py test_an_existing_sqlite_store_is_never_switched TUÉE

Suite exécutée : 89 passed, 1 skipped (le skip est le e2e opt-in CORTEX_MANAGED_PG_E2E, remplacé par mon exécution manuelle ci-dessus). python scripts/check_craftsmanship.py : OK. Digests : les 8 sha256 épinglés (4 triples x 2 archives) sont égaux aux digests que GitHub publie aujourd'hui pour ces assets (gh api .../releases/tags/...).

3. Constats

BLOQUANTS

B1 — Setup avec marker managed démarre le serveur managed même quand database_url est configuré (viole ADR-1110 §4a).
launcher_pg_provision._target renvoie (Path(marker["root"]), True, "") dès que le marker est managed, avant tout appel à map_database_option / environment_decides. Contre-exemple (e1.py) : marker managed + CLAUDE_PLUGIN_OPTION_DATABASE_URL=postgresql://u:p@remote/db => provision est appelée ; sur erreur, setup_step renvoie une cause non vide, donc Setup échoue (_fail). Conséquence : l'échappatoire que le message d'erreur du PR recommande (« or set DATABASE_URL ») marche pour les lancements mais pas pour le Setup suivant (mise à jour du plugin, job de maintenance) : l'utilisateur qui a configuré sa propre base voit son Setup échouer, ou télécharger et démarrer un PG qu'il a refusé. Correction : appliquer map_database_option + environment_decides dans _target avant la branche managed (un store configuré gagne, rien de managed ne démarre, message explicite) + test « marker managed + option => aucun provision ». Effet exact d'un Setup failed sur les hooks suivants : non mesuré.

B2 — L'identité par port loopback fait d'un processus étranger « le nôtre » (l'affirmation « cannot make a foreign process ours » est fausse).
live_postmaster : pid vivant de cet utilisateur dont l'exécutable n'est pas bin/postgres + n'importe quel port en écoute => couple accepté ; le port n'est pas lié au pid. Contre-exemples exécutés : (foreign.py) un pid quelconque + un écouteur quelconque => live_postmaster renvoie le couple ; (stopprobe.py, faux pg_ctl qui enregistre ses arguments, un sleep 300 que j'ai lancé, un écouteur) => stop() invoque pg_ctl -D <cluster> -m fast -w stop, et start() renvoie le port étranger sans retirer le postmaster.pid. Un vrai pg_ctl stop signale le pid du fichier (comportement de PostgreSQL, non mesuré ici : je n'ai rien voulu tuer) : un processus étranger de l'utilisateur serait interrompu ; l'adoption est permanente tant que fichier et écouteur existent (status running sur un port étranger, authentification en échec à chaque lancement : verrouillage jusqu'à suppression manuelle). Probabilité faible (réutilisation de pid ET port éphémère en écoute, typiquement après un reboot), mais c'est la famille du bloquant 4 de la revue précédente et la correction est peu coûteuse. Correction : prouver l'identité par le protocole (SSLRequest PostgreSQL, réponse S ou N exigée) ou lier le port au pid (répertoire de données de la ligne 2 de postmaster.pid, écouteur appartenant à ce pid) + test avec écouteur étranger.

B3 — La commande livrée /preflight (python -m mcp_server.doctor) plante avec un traceback pour tout utilisateur d'une installation fraîche managed (régression vs main).
doctor.active_checks appelle effective_backend(os.environ), qui lève BackendMarkerError sur un marker managed sans DATABASE_URL (reproduit : HOME de scratch avec marker managed => traceback « the backend marker selects the plugin's managed PostgreSQL server ... or set DATABASE_URL »). Sur main une installation fraîche reçoit un marker sqlite et le doctor tourne. Le marker managed devenant le cas par défaut sur macOS, l'outil de diagnostic casse précisément chez l'utilisateur qui en a besoin. Le commentaire de doctor (« BackendMarkerError propagates ») visait un marker corrompu, pas un marker valide. Correction : le doctor connaît managed (résolution via le launcher ou rapport de l'état du serveur managed) + test.

NON BLOQUANTS (classés)

  • N1. Repli SQLite bruyant seulement dans setup.log (et dans le journal que scripts/init_plugin.py affiche en direct). setup_step renvoie "" : l'outcome reste ready, init_plugin sort 0, et aucun lecteur de provisioned.json n'existe dans mcp_server/ (grep). Un utilisateur marketplace ou claude --init-only nu voit « SQLite » sans cause ; le docstring de launcher_setup dit lui-même que l'hôte n'affiche rien de Setup. Conforme à la lettre de la décision d'owner (repli SQLite, cause dans setup.log), pas à l'esprit « pas de dégradation silencieuse ». Recommandé : champ dédié dans <key>.setup.json + une ligne SessionStart. Pèse surtout sur le point (b).
  • N2. pg_ctl start -w / stop -w sans -t ni PGCTLTIMEOUT (grep : aucun « timeout » dans scripts/launcher_pg*.py) => l'horloge de 60 s par défaut de pg_ctl décide de l'échec. Un démarrage lent (récupération après crash, qemu) est déclaré en échec alors que le postmaster finit de monter ; le superviseur meurt, le suivant adopte. Le docstring « no timer of ours decides anything » est exact mot pour mot mais trompeur. Décision d'owner (règle « pas de timeout comme vérification »).
  • N3. Le mutant M3 survit : aucun test ne distingue PermissionError=>mort de PermissionError=>vivant, le repli sur le port masque la différence. Test à ajouter avec un écouteur.
  • N4. TOCTOU : _select revérifie l'existence du fichier SQLite, mais un lancement écrit son marker avant que le moteur crée memory.db ; un serveur MCP déjà parti en SQLite continuerait d'écrire dans le fichier après le basculement du marker (store scindé). Fenêtre étroite, non reproduit. Le test existant pré-crée le fichier.
  • N5. Tar : _inside valide noms et cibles de liens sans modéliser les chaînes de liens symboliques internes à l'archive ; la protection réelle est filter="data" (3.10.12+, 3.11.4+, 3.12+), et sur les Python plus anciens le sha256 épinglé (vérifié avant toute extraction : M4) est la seule barrière. Raisonné, pas exécuté. Extraction toujours dans un scratch sous dist/ des données du plugin.
  • N6. Provenance : l'archive pgvector vient du dépôt personnel nicoloboschi/pgvector_compiled (release du 2026-01-31, mutable, sans attestation). Les digests épinglés protègent contre une substitution ultérieure, pas contre le contenu initial ; PRIVACY/README le divulguent.
  • N7. Un marker global (~/.claude/methodology) pointe vers un store qui vit dans les données d'un plugin : désinstaller supprime les mémoires (divulgué dans PRIVACY) et laisse un marker pendant ; attach dit « root is missing » sans indiquer claude --init-only. Le Setup suivant reprovisionne un store vide à la même racine.
  • N8. Non mesurés : latence d'un hook lancé sans serveur MCP vivant (start + stop de PG par hook) ; accessibilité du store managed pour cortex-viz, mcp_server.migrate, benchmarks (port aléatoire par start, mot de passe en fichier, arrêt au dernier client).
  • N9. free_port puis pg_ctl -p : course sur le port, mais l'échec est bruyant et le start suivant retire un nouveau port. Accepté.
  • Vérifié sain : flock (ordre start/supervisor/clients, aucune attente chronométrée de notre côté), état écrit seulement après succès, sha256 avant extraction, mot de passe jamais en argv, droits 0600/0700, extraction confinée aux données du plugin, éligibilité qui protège un store SQLite existant, aucune migration dans cette PR (le marker sqlite du launcher + fichier présent reste inéligible : l'importeur futur peut le basculer).

4. Jugement sur les points ouverts de l'auteur

(a) Marker global lu sans le launcher (mcp_server/__main__.py, hooks/entry.py, Codex runtime.py qui exécute hooks.entry). Pas une régression pour un utilisateur existant : le marker managed n'est écrit que pour un store sans état antérieur (why_not_eligible, M8) après provisionnement réussi. C'est une régression pour un nouvel utilisateur Claude + Codex sur le même HOME (sur main ils partageaient le SQLite créé au premier lancement ; maintenant Codex et hypermnesia-mcp direct échouent bruyamment avec BackendMarkerError tant que DATABASE_URL n'est pas posé). Bruyant, pas de scission de données : NE BLOQUE PAS en soi, sous réserve que CHANGELOG/README Codex le disent (aujourd'hui seul l'ADR le dit). En revanche /preflight (B3) bloque. Décision d'owner : Codex doit-il s'attacher au serveur managed ?
(b) Linux glibc non autonome => SQLite : acceptable pour un déploiement par étapes, non bloquant, mais seulement avec N1 corrigé ; sinon l'utilisateur Ubuntu 25.10 ne saura jamais pourquoi.
(c) Anciens dist/ non nettoyés : non bloquant (croissance disque à chaque déplacement de pin) ; à suivre avec la règle d'hygiène disque de l'owner.
(d) Installation fraîche sans claude --init-only => SQLite : acceptable (cohérent avec l'éligibilité : un store existant n'est jamais basculé) ; l'importeur devra basculer le marker sqlite écrit par le launcher.

5. Décisions d'owner nécessaires

  1. Codex / lancements directs : s'attachent-ils au serveur managed, ou erreur documentée (état actuel) ?
  2. pg_ctl -w avec son délai de 60 s par défaut : accepté, ou -t / PGCTLTIMEOUT très grand + attente événementielle ?
  3. Repli SQLite : suffit-il de setup.log, ou faut-il une ligne visible (outcome Setup + SessionStart) ?
  4. Confiance dans nicoloboschi/pgvector_compiled (binaire d'un dépôt personnel, digest épinglé) ou compilation par la CI de Cortex.
  5. Durée de vie du store managed = celle des données du plugin (désinstallation = perte) alors que le marker est global : acceptable avant l'importeur ?

6. Non vérifié

Signal réel envoyé par pg_ctl stop à un pid étranger (faux pg_ctl utilisé) ; les exécutions Linux (glibc, qemu) et Rosetta x86_64 (preuves de l'auteur et jobs CI seulement ; j'ai rejoué macOS arm64 natif) ; effet d'un Setup failed sur les hooks suivants ; course N4 ; chaînes de liens tar ; Python < 3.10.12 ; latence des hooks ; cortex-viz.

Artefacts : mut.log, mut.py, e1.py, foreign.py, stopprobe.py dans ~/.claude/fleet/evidence/cortex-748-round3/.

cdeust and others added 2 commits October 17, 2026 10:21
… theseus tree (ADR-1110, D4)

Builds on a runner of each of the four triples, proves the library loads
(postgres --single), checks the glibc floor on Linux, attests build
provenance and publishes the archives once as a prerelease of this repo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYyqzx4FVMHasVPZV3uBYq
…ed lifecycle, every surface attaches (ADR-1110)

Blockers of the round-3 review:
- B1: Setup applies database_url / CORTEX_* before reading a managed marker;
  nothing managed is installed or started and Setup says so.
- B2: a postmaster is ours only when postmaster.pid names this data
  directory and the {pid, kernel start time, port} recorded at spawn; only
  then is the port dialled, and the server must complete SCRAM-SHA-256 with
  the cluster password (server signature checked). A foreign listener is
  never adopted, dialled or signalled.
- B3: the doctor attaches to the managed server and reports it as a check.
- M3: PermissionError test with a listener present.

Owner decisions:
- D1: the attach code moves into the wheel (mcp_server/infrastructure/
  managed_pg*.py); apply_backend_resolution attaches on a managed marker, so
  hypermnesia-mcp, the Codex hook entry and cortex-doctor share the server.
- D2: postgres is the supervisor's child; ready = postmaster.pid "ready"
  (kqueue/inotify), failure = end of file on an inherited pipe, stop =
  SIGINT then flock on postmaster.lock held by the whole group. No pg_ctl -w,
  no timer; zombies read as gone; works under qemu (no pidfd).
- D3: sqlite_fallback recorded in <key>.setup.json, shown at SessionStart.
- D4: pins point to the pgvector 0.8.7 archives Cortex CI built and attested
  (release pgvector-v0.8.7-pg18.6.0, run 39017072528).
- D5: cluster root ~/.claude/methodology/managed-pg; binaries stay in the
  plugin data; uninstalling never deletes memories.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYyqzx4FVMHasVPZV3uBYq
@cdeust cdeust changed the title feat(storage): the plugin runs its own PostgreSQL from pinned theseus binaries, pg0 dropped (#744, #740) feat(storage): the plugin runs its own PostgreSQL (theseus binaries, Cortex-built pgvector), shared by every surface (#744, #740) Oct 17, 2026
@cdeust

cdeust commented Oct 17, 2026

Copy link
Copy Markdown
Owner Author

ZETETIC-REVIEW: REQUEST_CHANGES
head: 52c3156

Revue adversariale PR #748 (round 4) — ADR-1110, PostgreSQL géré

Revue en lecture seule, worktree détaché .claude/worktrees/review-748-r4 sur 52c3156 (base df3a447 = origin/main), supprimé en fin de revue. Je n'ai pas attendu la CI (l'orchestrateur la lit). Chaque constat est marqué exécuté (contre-exemple ou mesure lancés par moi sur ce head) ou non vérifié. Sondes et sorties : ~/.claude/fleet/evidence/cortex-748-round4-review/.

Un seul bloquant (B4, numéroté à la suite de B1-B3 du round 3), peu coûteux. Les trois bloquants du round 3 (B1, B2, B3) et M3 sont résolus, avec contre-exemples rejoués ; les cinq décisions d'owner sont implémentées. Ce qui bloque, c'est la règle d'owner « aucun timeout comme vérification » : le mutant D2 n'est tué que par le timeout pytest de 300 s, alors qu'un test événementiel le tue immédiatement (exécuté).

1. Bloquants du round 3, rejoués sur ce head

# Statut Preuve (exécutée)
B1 Setup + marker managed ignorait database_url RÉSOLU e1.py du round 3 (seul changement : fake_provision(paths, *rest), la signature a changé) : provision called: [], said: managed PostgreSQL not selected: database_url or a CORTEX_* backend setting selects the store. Variante CORTEX_MEMORY_STORE_BACKEND=sqlite : idem. Témoin sans rien de configuré : provision called: ['…/data/pg'] (la sonde est sensible).
B2 pid étranger + écouteur adopté ; stop() le signalait RÉSOLU foreign.py/stopprobe.py portés vers managed_pg_server (foreign2.py) : processus étranger de mon utilisateur, qui enregistre chaque SIGINT et chaque connexion, nommé par postmaster.pid avec le bon répertoire de données et le bon port. (a) sans postmaster.json et (b) avec un started faux : live_postmaster → None, stop() ne fait rien, start() lance son propre postmaster sur un autre port, l'étranger n'a vu ni signal ni connexion (foreign_saw: []). (c) avec le started réel du pid étranger : adopté et composé (dialled, puis ConnectionResetError). C'est la limite de confiance résiduelle : il faut écrire postmaster.json (0600, racine 0700), donc être déjà cet utilisateur. Acceptable.
B3 python -m mcp_server.doctor plantait RÉSOLU HOME de scratch, marker managed dont la racine manque : [FAIL] Managed PostgreSQL server … is missing; run \claude --init-only``, code 1, pas de traceback.
M3 (PermissionError lu comme vivant) RÉSOLU Mutant rejoué par moi : tué par test_permission_error_is_not_ours_even_with_a_listener (1 failed). B2-stop rejoué : tué par test_stop_never_signals_a_foreign_process. Base : 14 passed.

e2e rejoué par moi (macOS arm64, sandbox-exec no_homebrew.sb, env -i, HOME neuf, Python uv hors /opt/homebrew) : passed: true en 39 s ; PG 18.6, ssl_library: OpenSSL, vector 0.8.7, pg_trgm 1.6, 23 tables, direct_launch_same_url: true, cluster sous $HOME/.claude/methodology/managed-pg, binaires sous data/pg/dist, stopped, pas de postmaster.pid résiduel. Le vrai postgres garde bien le flock hérité sur postmaster.lock : _stop_and_wait a attendu la sortie dans l'e2e, et dans la sonde N2 lsof montrait neuf processus postgres qui le tenaient, avec un octroi seulement après leur sortie à tous.

2. BLOQUANT

B4 — Le mutant D2 n'est tué que par une horloge (pytest-timeout 300 s), contre la règle d'owner « aucun timeout/attente/budget de temps comme vérification, tests et CI compris ».
test_the_exit_is_read_from_the_pipe_not_from_waitpid remplace Popen.poll par lambda self: None. Face au mutant lambda: proc.poll() is not None or _is_ready(, le test bloque dans watch.wait() ; seul timeout = 300 / timeout_method = "thread" (pyproject, déjà présent sur main) l'arrête. La table de mutation de l'auteur le reconnaît (« the suite's 300 s pytest-timeout guard ends it »). Un mutant tué par une horloge est un verdict décidé par une horloge.
Un test événementiel existe, et je l'ai exécuté (d2_event.py) : remplacer poll par une fonction qui lève AssertionError("poll() decided the exit").

  • Sur le head : ServerError: postgres exited 1 before it was ready: FATAL: lock file exists (passe : le code produit n'appelle jamais poll).
  • Sur le mutant : AssertionError: poll() decided the exit dès le premier appel, sans attente.
    Correction : une ligne dans le test (monkeypatch.setattr(subprocess.Popen, "poll", <fonction qui lève>)), puis rejouer la ligne D2 de la table et la corriger dans le corps de la PR. Le timeout global de 300 s reste un autre sujet (voir §4, point 7), mais aucun mutant revendiqué « tué » ne doit en dépendre.

3. NON BLOQUANTS (classés)

N1 — Signature serveur et nonce SCRAM : code correct, gardes non testées.
Lecture : n,, / c=biws, AuthMessage = client-first-bare,server-first,client-final-without-proof, ClientProof = ClientKey XOR HMAC(H(ClientKey), AuthMessage), ServerSignature = HMAC(HMAC(SaltedPassword,"Server Key"), AuthMessage) comparée par hmac.compare_digest avant d'accepter AuthenticationOk ; tout code d'authentification autre que 10/11/12/0 dans l'ordre est refusé. Conforme à RFC 5802/7677. Exécuté (scram_neg.py, serveurs hostiles) : signature fausse → ProtocolError: the server's SCRAM signature is not this cluster's ; serveur trust (AuthOK direct) → refusé ; demande de mot de passe en clair (code 3) → refusé, mot de passe jamais émis ; nonce qui ne prolonge pas le nôtre → refusé. Mais aucun test du dépôt n'exerce ces gardes (fake_postgres signe toujours juste ; grep : aucun test « signature »/« nonce »). Mutants exécutés : retrait de la vérification de signature → survit (19 passed) ; retrait du contrôle de nonce → survit (19 passed). Le corps de la PR présente « server signature included » comme partie de la preuve d'identité ; un retrait silencieux de cette vérification passerait la suite. Recommandé, dans la même passe que B4 : trois tests de serveur hostile (signature, trust, clair), sur le modèle de scram_neg.py. Mineurs : attrs["s"]/attrs["i"] absents lèvent KeyError, pas ProtocolError ; pas de borne sur i (un serveur hostile peut imposer un PBKDF2 très long) ; pas de SASLprep (sans objet : mot de passe hexadécimal) ; pas de timeout socket, ce que le docstring justifie (on ne compose que le port d'un pid prouvé). Je l'accepte.

N2 — Superviseur tué entre spawn et enregistrement : récupération automatique, mais par accident, et l'ADR dit le contraire (point 4).
Exécuté avec le vrai PG 18.6 (killed_supervisor.py). Un superviseur se tue (SIGKILL) à l'instant où il écrirait postmaster.json ; le postgres orphelin (ppid 1) devient ready. Puis un client s'attache normalement :

  1. ManagedPgError: … still runs (it holds postmaster.lock); stop it, then start Cortex again. Le message ne nomme ni pid ni port : le port n'était que dans le postmaster.pid que start() venait d'effacer.
  2. start() efface le postmaster.pid d'un postmaster vivant (ligne 207, avant d'essayer le lock dans _spawn).
  3. Ce postmaster s'arrête alors de lui-même : LOG: performing immediate shutdown because data directory lock file is invalid. C'est la revérification par minute de PostgreSQL (postmaster.c REL_18_STABLE l. 1779-1786, RecheckDataDirLockFile). Le flock sur postmaster.lock m'a été accordé après 14,8 s ; la durée est une information, l'événement est l'octroi du lock.
  4. Attache suivante : démarrage, crash recovery (redo done), running, puis stopped au dernier client.
    Gravité faible : fenêtre de quelques microsecondes, échec bruyant, aucune action manuelle, pas de perte (WAL). Mais la récupération repose sur un effet de bord non conçu (l'effacement du fichier d'un postmaster vivant) et sur l'horloge interne de PG. L'ADR (§Negative) et le corps de la PR disent au contraire « fails loudly until that process is stopped » / « naming it », ce qui est inexact. Sous-cas raisonné, non exécuté : si start() efface le fichier avant que l'orphelin ne l'ait créé, l'orphelin le crée ensuite, valide, et il faut un échec de plus. Recommandé : tenter le flock non bloquant avant d'effacer, mettre pid et port du postmaster.pid dans le message, et corriger l'ADR selon ce qui a été mesuré.

N3 — Un postmaster mort sous un superviseur vivant n'est jamais remplacé tant qu'un client vit.
Exécuté (pm_crash.py, vrai PG ; je n'ai tué que le postmaster lancé par ma sonde). Client A connecté ; SIGKILL du postmaster. status.json reste {"state": "running", "port": 59142} ; un nouveau client B reçoit cette URL et échoue (OperationalError). Le service ne revient qu'une fois tous les clients sortis (C : connect ok sur un nouveau port). Le superviseur ferme exit_r après ready et ne surveille plus rien. Avec des sessions Claude/Codex longues, la mémoire peut rester indisponible des heures après un OOM-kill (Linux) du postmaster. Bruyant, rare : non bloquant. (Pendant cette sonde, ma table de mutation tournait en parallèle dans le worktree, M3 peut-être appliqué ; la branche EPERM n'intervient pas ici, et le résultat suit la lecture du code.) Recommandé : garder exit_r et, sur EOF, passer status à failed (ou redémarrer).

N4 — D4 : chaîne d'approvisionnement saine. La provenance ne nomme qu'une branche, et une branche peut réserver une release.
Exécuté : la release pgvector-v0.8.7-pg18.6.0 est une prerelease créée par github-actions[bot] sur d6f869d. Les 4 digests des assets publiés = les 4 pins = shasum de mes téléchargements. gh attestation verify --repo cdeust/Cortex --signer-workflow cdeust/Cortex/.github/workflows/pgvector.yml passe pour chacun, avec deux attestations par archive (d6f869d run 39017072528, et 52c3156 run 39042889364). La reconstruction à 52c3156 a produit des sha256 identiques au bit près : le build CI est reproductible. Les 4 pins theseus = digests publiés par theseus.
Déclencheurs : push (toute branche du dépôt) + workflow_dispatch. Aucun pull_request ni pull_request_target, donc une PR de fork ne lance rien ici ; publish est conditionné à github.repository == 'cdeust/Cortex'. Permissions : contents: read par défaut ; id-token/attestations: write limités au build, contents: write au seul publish. persist-credentials: false. Les ${{ }} dans run: sont des valeurs statiques de la matrice : pas d'injection. Pas de risque d'entrée non fiable.
Restes : (i) --source-ref refs/heads/main échoue aujourd'hui (« got refs/heads/feat/744-pg0-launcher »), et la recette documentée (--repo seul) accepte n'importe quelle branche. Le push sur main après merge reconstruira, et vu la reproductibilité il devrait attester les mêmes digests depuis main (non vérifié avant merge). (ii) Toute personne qui peut pousser peut créer la release d'une version future depuis une branche non relue, et « never replace » fige alors ces octets. Ce sont les pins relus qui ferment la porte. Recommandé : publier seulement depuis refs/heads/main (ou par workflow_dispatch sur main), et documenter la vérification avec --source-ref refs/heads/main.

N5 — D1 vérifié, avec deux restes.
Exécuté (d1_concurrent.py, vrai PG). Démarrage à froid simultané des quatre surfaces : __main__ (apply_backend_resolution, ce que fait hypermnesia-mcp à l'import), hook Codex (hooks.entry.prepare_environment), launcher Claude (launcher_backend.apply) et doctor. Elles sont libérées par un même événement (barrière flock, modules préimportés). Résultat : un seul superviseur (supervisors_started: 1), même URL et même pid de superviseur pour les quatre, stopped après le dernier, pas de postmaster.pid résiduel. Deux exécutions : une première, tiède, à cause d'un défaut de ma barrière FIFO (une surface avait démarré le serveur, les trois autres s'y sont attachées : même superviseur, même URL), puis celle-ci, à froid. Ce n'est pas une preuve d'absence de course. L'ordre des locks (start EX avant supervisor EX-nb ; l'enfant hérite des deux OFD ; status écrit avant de libérer start ; supervisor.lock libéré avant clients.lock) est cohérent à la lecture.
Restes : python -m mcp_server et le hook Codex sortent sur un traceback quand le serveur ne peut pas tourner (racine absente, exécuté), avec en dernière ligne le message utile (ManagedPgError … run \claude --init-only`). C'est bruyant et conforme au rejet du catch-and-degrade ; je ne le bloque pas, mais une ligne propre comme celle du launcher serait cohérente avec B3. Latence d'un hook Codex sans serveur MCP vivant (start + stop de PG à chaque hook) : non mesurée (charge machine ~300). Désaccord de versions entre le code du plugin Claude et la roue Codex sur le même postmaster.json` / les mêmes locks : non vérifié.

N6 — Point 7 : timeout-minutes retiré du job managed-pg.
Conforme à la lettre de D2, mais l'horloge ne disparaît pas : GitHub tue un job à 360 min par défaut. Un blocage coûte donc 6 h de runner (dépôt public : pas de facture) au lieu d'un échec rapide. Les jobs pytest gardent timeout = 300 (main), dont dépend aujourd'hui le mutant D2 (B4). Je ne bloque pas sur ce retrait. L'asymétrie est une décision d'owner : soit on assume « un blocage est un défaut, la plateforme a sa borne », partout ; soit on garde une borne de garde explicite, partout, sans jamais lui confier un verdict.

N7 — Statut running périmé. Raisonné, non exécuté : _ensure_supervisor ne remet pas status.json à zéro avant de lancer un superviseur. Si ce superviseur meurt avant d'écrire (import cassé, interpréteur absent) après un superviseur tué en état running, un client lit l'ancien running et son port. Très rare.

Vérifié sain, exécuté : chemin de readiness, où PG réécrit postmaster.pid sur place (pg_pwrite à l'offset 0 dans AddToDataDirLockFile, miscinit.c REL_18 ; donc NOTE_WRITE kqueue sur le fd suivi, et le follow armé avant chaque lecture) ; EOF du tube comme événement de sortie ; arrêt par SIGINT + flock ; aucun sleep/timeout/retry ajouté dans le diff hors .md (grep) ; D3 (champ sqlite_fallback + ligne SessionStart, lus dans le code ; tués par les tests de l'auteur, non rejoués par moi) ; D5 (racine sous ~/.claude/methodology/managed-pg, binaires dans les données du plugin, e2e).

4. Réponses aux points de l'orchestrateur

  1. B1, B2, B3, M3 : résolus, contre-exemples du round 3 rejoués (§1).
  2. D2 : oui, c'est un verdict d'horloge, contraire à la règle d'owner. Oui, un test événementiel le tue, exécuté (B4).
  3. SCRAM correct, et rejette signature, trust, clair et nonce (exécuté) ; gardes non testées (N1). Readiness kqueue/inotify, EOF du tube et flock à l'arrêt : sains (exécuté avec le vrai PG).
  4. Superviseur tué entre spawn et enregistrement : gravité faible, récupération sans action manuelle au deuxième essai, mais par effet de bord ; message et ADR inexacts (N2).
  5. D4 : pas de risque fork/entrée non fiable ; attestations vérifiées ; digests = pins = assets ; build reproductible ; provenance de branche seulement, réservation de release possible par un pousseur (N4).
  6. D1 : même serveur pour les quatre surfaces au démarrage à froid simultané (exécuté une fois) ; traceback brut sur deux surfaces quand le serveur ne peut pas tourner (N5).
  7. timeout-minutes : voir N6 (décision d'owner).

5. Table de mutation (exécutée par moi sur 52c3156, une garde à la fois, git status propre après)

Mutation Test(s) lancé(s) Résultat
Base, head non modifié test_managed_pg_server.py 14 passed
M3 PermissionError => vivant test_permission_error_is_not_ours_even_with_a_listener TUÉE
B2-stop stop() lit read_lockfile au lieu de live_postmaster test_stop_never_signals_a_foreign_process TUÉE
SIG signature serveur SCRAM non vérifiée test_managed_pg_server.py + test_managed_pg_attach.py SURVIT (19 passed)
NONCE nonce serveur non vérifié idem SURVIT (19 passed)
TRUST client qui attend AuthOK sans SCRAM idem tuée, mais trivialement : fake_postgres n'envoie jamais AuthOK directement, donc le refus du mode trust n'est pas testé non plus
D2 (mutant de l'auteur) avec le test événementiel proposé (d2_event.py, hors pytest) — TUÉE immédiatement ; le head passe

Les mutants de l'auteur que je n'ai pas rejoués (M1, M2a/b, M4 à M8, B1, B2-start, B3, D1, D3, D5) : sa table sur 8df17473 / 52c3156, non revérifiée par moi.

6. Non vérifié

Linux (glibc, qemu) et Rosetta x86_64 (preuves de l'auteur seulement ; j'ai rejoué macOS arm64 natif) ; latence des hooks sans serveur vivant ; désaccord de versions plugin/roue Codex ; attestation depuis main (après merge) ; sous-cas N2 où l'orphelin n'a pas encore écrit postmaster.pid ; N7 ; suite complète (CI).

7. Nettoyage

  • Worktree détaché .claude/worktrees/review-748-r4 supprimé (git worktree remove, status propre, absent de git worktree list et du disque). register-worktree l'avait refusé (worktree détaché) : aucune entrée de ledger, aucun hook ne le reprendra.
  • Répertoire temporaire disk-hygiene-xygw9lpe (185 Mo : venv de scratch, archives téléchargées, cluster et HOME de scratch, sondes) enregistré par create-temp, marqué evidence-preserved, puis supprimé par dispose.
  • Ref locale refs/remotes/pr/748 supprimée.
  • Processus : tous ceux que j'ai lancés sont terminés (orphelin postgres 12925 arrêté par PG lui-même, superviseurs et postmasters de scratch arrêtés au dernier client ; ps : 0 restant). Je n'ai tué aucun processus que je n'avais pas lancé.
  • Conservé : ce verdict et ~/.claude/fleet/evidence/cortex-748-round4-review/ (80 Ko : sondes, sorties, rapport e2e, table de mutation). Le worktree de l'auteur feat-744-pg0-launcher n'a pas été touché (sa suppression revient à l'orchestrateur).

…ed orphan adopted, hostile SCRAM servers refused (ADR-1110)

Round-4 review of 52c3156:
- B4: the D2 mutant dies by assertion (Popen.poll raises), not by the
  300 s pytest-timeout.
- N1: hostile-server tests (wrong signature, foreign nonce, trust,
  cleartext, malformed server-first): refused, password never sent; a
  malformed server-first message is a ProtocolError, not a KeyError.
- N2: start() touches postmaster.pid only while it holds postmaster.lock.
  An unrecorded lock holder is waited for on events (exit, or ready), then
  adopted and recorded only if it authenticates; otherwise the error names
  its pid and port and nothing is signalled or removed.
- N3: the supervisor waits on two kernel events (last client, postmaster
  group released); on death it restarts the postmaster under the start lock,
  on the same port when free. A client never takes a running status whose
  recorded postmaster is dead: it waits for the next root-dir change.
- N4: pgvector.yml publishes only from refs/heads/main; verification is
  documented with --source-ref refs/heads/main.
- N5: python -m mcp_server and the Codex hook entry end in one line
  "Cortex not ready: <cause>", exit 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYyqzx4FVMHasVPZV3uBYq
@cdeust

cdeust commented Oct 17, 2026

Copy link
Copy Markdown
Owner Author

ZETETIC-REVIEW: REQUEST_CHANGES
head: e290980

Revue adversariale PR #748 (round 5) — ADR-1110, PostgreSQL géré

Revue en lecture seule, worktree détaché .claude/worktrees/review-748-r5 sur e290980 (fast-forward de 52c3156, base df3a447 = origin/main), supprimé en fin de revue. Je n'ai pas attendu la CI. Chaque constat est marqué exécuté (contre-exemple ou mesure lancés par moi sur ce head, vrai PG 18.6 quand c'est indiqué) ou raisonné / non vérifié. Sondes et sorties : ~/.claude/fleet/evidence/cortex-748-round5-review/.

B4 et les points N1 à N5 du round 4 sont corrigés. Je les ai vérifiés avec mes propres sondes rejouées sur ce head : d2_event.py, killed_supervisor.py (porté à la nouvelle signature de _spawn), pm_crash.py, serveurs SCRAM hostiles et foreign2.py. L'e2e macOS arm64 sous no_homebrew.sb passe (44 s ; PG 18.6, vector 0.8.7, pg_trgm 1.6, direct_launch_same_url, stopped, aucun postmaster.pid restant).

Il reste un bloquant, B5. start() efface encore le postmaster.pid d'un postmaster vivant de ce cluster quand celui-ci ne tient pas postmaster.lock. PostgreSQL s'arrête alors en urgence (exécuté). Le déclencheur est externe et rare. Je le classe bloquant parce que la consigne du round 4 le demande pour cette classe (action destructive sur un processus vivant, même si la récupération passe par un effet de bord), et parce que la correction est peu coûteuse.

1. Points du round 4 rejoués sur ce head

# Statut Preuve (exécutée)
B4 mutant D2 tué seulement par le timeout de 300 s RÉSOLU test_the_exit_is_read_from_the_pipe_not_from_waitpid remplace maintenant Popen.poll par une fonction qui lève. Rejoué : mutant D2 tué, 1 failed in 49.42s, sans timer. Mon d2_event.py sur le head : PASS ServerError … lock file exists.
N1 signature et nonce SCRAM non testés RÉSOLU Rejoué : mutant SIG tué ([badsig-signature is not this cluster's], 44,97 s, sans timer) ; mutant NONCE tué ([badnonce-nonce does not extend ours], sans timer). scram_neg.py sur le head : signature fausse, trust, demande en clair, nonce étranger, tous refusés ; mot de passe jamais émis.
N2 superviseur tué entre spawn et enregistrement RÉSOLU pour ce cas killed_supervisor.py, vrai PG : l'orphelin (ppid 1) devient ready, puis un client s'attache. Il obtient attached 127.0.0.1:57281 (le port de l'orphelin). postmaster.json enregistre alors le pid de l'orphelin, postmaster.pid est conservé, grep -c "lock file is invalid" postgres.log = 0, et le statut est stopped après le dernier client (orphelin arrêté). Mais voir B5 : une autre forme de la même classe subsiste.
N3 postmaster mort sous des clients vivants RÉSOLU pm_crash.py, vrai PG : après SIGKILL du postmaster, le nouveau client B obtient connect ok 127.0.0.1:57295, le même port que A, puis stopped. Nouvelle sonde n3_race.py, vrai PG : un client A attaché, SIGKILL du postmaster, puis quatre clients libérés ensemble par une barrière flock juste après la mort. Les quatre obtiennent la même URL avec ok, il ne reste qu'un postmaster pour ce cluster (pgrep), puis stopped sans postmaster.pid restant.
N4 publication depuis une branche RÉSOLU publish exige github.repository == 'cdeust/Cortex' && github.ref == 'refs/heads/main'. Un workflow_dispatch sur une branche porte github.ref = la branche, donc il est ignoré. Déclencheurs inchangés (push + workflow_dispatch, aucun pull_request/pull_request_target), permissions inchangées (contents: read par défaut ; id-token/attestations: write pour le build, contents: write pour le seul publish). Les branches continuent de produire des attestations, que --source-ref refs/heads/main écarte. Attestation depuis main : non vérifiable avant le merge.
N5 traceback brut RÉSOLU HOME de scratch, marker managed dont la racine manque : python -m mcp_server → [cortex-mcp] Cortex not ready: … run \claude --init-only`, code 1 ; hook Codex → [cortex-hook] Cortex not ready: …`, code 1 ; une seule ligne chacun, sans traceback.
N6 (timeout-minutes) / N7 (statut périmé) décision d'owner / atténué N7 : un client vérifie désormais le postmaster enregistré avec les faits du noyau avant d'accepter running (mutant « client fait confiance à running » tué : 1 failed in 36.42s, sans timer).

Second point de l'orchestrateur sur N2. Un processus étranger du même utilisateur qui tient postmaster.lock et écrit un postmaster.pid ready n'est pas adopté sans SCRAM réussi avec notre mot de passe. Il n'est ni signalé ni effacé : ServerError (exécuté avec holder_mismatch.py, et c'est aussi le test de l'auteur). Mais ce qui est composé, c'est le pid/port du fichier, pas forcément le détenteur du lock (N8).

2. BLOQUANT

B5 — start() efface le postmaster.pid d'un postmaster vivant qui ne tient pas postmaster.lock ; PostgreSQL s'arrête alors en urgence.
Exécuté (unlocked_postmaster.py, vrai PG 18.6, cluster de scratch) :

  1. Je lance à la main bin/postgres -D <cluster> -p 57364 (ce que ferait pg_ctl start ou un outil), attendu ready.
  2. Un client s'attache normalement. live_postmaster → None (rien n'est enregistré). Le flock non bloquant sur postmaster.lock réussit, puisque ce postmaster ne le tient pas. La ligne 276 (paths.cluster / "postmaster.pid").unlink(missing_ok=True) # lock held: no live one efface le fichier du postmaster vivant.
  3. Le postgres de Cortex refuse de démarrer (FATAL: pre-existing shared memory block … is still in use). Le verrou SysV de PG empêche ainsi un double postmaster sur le même répertoire de données. L'attache échoue (ManagedPgError), et le statut affiche postmaster_pid_file: None.
  4. Le postmaster lancé à la main s'arrête de lui-même : LOG: performing immediate shutdown because data directory lock file is invalid. La sortie est observée par kqueue NOTE_EXIT ; 36 s s'écoulent, à titre d'information seulement.

L'invariant « lock tenu ⇒ aucun postmaster vivant » n'est vrai que pour les postmasters que Cortex a lancés. Le corps de la PR (« start() takes postmaster.lock before touching postmaster.pid ») suggère une sûreté qu'il n'a pas pour les autres.
Correction : ne jamais effacer un postmaster.pid dont le pid est vivant pour cet utilisateur sans avoir d'abord prouvé que ce n'est pas le postmaster qui l'a écrit. Deux pistes, à vérifier par l'auteur :

  • (a) Preuve noyau sans enregistrement : la ligne 3 de postmaster.pid (LOCK_FILE_LINE_START_TIME, MyStartTime) vaut le début de vie du processus. Mesuré : 1792268628 contre started() = 1792268628.936399. À traiter : une tolérance de ±1 s (MyStartTime est pris après l'exec), et sous Linux la conversion btime + starttime/CLK_TCK (started() renvoie des ticks depuis le boot).
  • (b) Plus neutre vis-à-vis des plateformes : si le pid du fichier est vivant pour cet utilisateur, _prove sur son port avant d'effacer. S'il signe avec le mot de passe du cluster, c'est notre postmaster vivant : l'adopter et l'enregistrer, ne jamais l'effacer. S'il refuse, le pid a été réutilisé et le fichier est périmé. C'est la même exposition que celle que _recover_unrecorded accepte déjà.
    Les deux pistes ferment aussi N8. Il faut un test de non-régression avec un processus vivant du même utilisateur, nommé par le fichier, qui ne tient pas le lock.

3. NON BLOQUANTS

N8 — Le processus composé n'est pas forcément « l'unique détenteur non enregistré du lock ». Exécuté (holder_mismatch.py, fake) : le processus A tient postmaster.lock ; un postmaster.pid périmé, ready, nomme un processus B vivant de cet utilisateur qui écoute sur le port indiqué. start() compose B (B saw: ['dialled']) et lève ServerError: pid <B> on port … holds postmaster.lock alors que le lock est tenu par A. Rien n'est signalé, aucun secret ne sort (SCRAM : seule une preuve dérivée du mot de passe circule, et le mot de passe a 192 bits aléatoires). Il faut un état du même utilisateur. La phrase du corps de la PR « the one other process ever dialled is an unrecorded holder of postmaster.lock » est donc inexacte, et le message d'erreur peut désigner le mauvais pid. B5 (a) ou (b) le corrige.

N9 — Un détenteur du lock qui ne devient jamais ready et ne sort jamais bloque attach indéfiniment, avec start.lock tenu. Raisonné, non exécuté. Le superviseur attend dans _recover_unrecorded ; tous les clients suivants attendent start.lock. Au round 4 ce cas échouait bruyamment. C'est voulu par D2 (aucune horloge ne décide qu'un démarrage lent a échoué). Déclencheurs réalistes : enfants d'un postmaster tué par SIGKILL qui traînent, en temps borné ; un archive_command bloqué. À écrire dans l'ADR comme conséquence connue.

Mutant non revendiqué (superviseur qui ignore l'événement de mort). Exécuté : select.select([last_client], [], []) contre test_a_postmaster_that_dies_under_clients_is_started_again → Timeout (tué uniquement par le pytest-timeout de 300 s). L'affirmation de l'auteur, « non tuable sans horloge », est exacte pour ce mutant. La régression qu'il représente est un blocage : les clients attendent pour toujours dans await_change, puisque le superviseur vit et que le répertoire ne change plus. Seule une borne de garde (le pytest-timeout déjà présent sur main) rendrait la CI rouge, sans que personne ne l'annonce comme une mise à mort. Acceptable parce que c'est déclaré. Une piste événementielle, pour mémoire : un état intermédiaire observable (par exemple status: restarting écrit avant le redémarrage) donnerait au test un événement positif sur lequel attendre, mais un mutant qui n'écrit rien ne produit toujours aucun événement. La limite est intrinsèque.

Course résiduelle sur supervisor.lock. Raisonné, conforme à la déclaration de l'auteur. Après une mort, await_change laisse un thread en attente d'un flock partagé sur supervisor.lock. À la sortie du superviseur, ce thread le prend un instant puis le rend. Un _ensure_supervisor concurrent (EX non bloquant) échoue alors, lit stopped et lève ManagedPgError ; l'attache suivante réussit. Ces threads s'accumulent, un par réveil, jusqu'à la sortie du superviseur. Bruyant, rare : non bloquant.

Corrections de test pour qemu. Lues : le test client utilise un autre processus vivant comme postmaster « redémarré », parce que qemu-user falsifie /proc/self/stat. Le test N3 attend sur les événements du répertoire racine au lieu de postmaster.lock, pour ne pas faire la course avec le redémarrage. Le code produit ne lit que /proc/<pid> d'autres processus, et le test reste événementiel (file queue entre threads, join sur le raccrochage). Exécution sous qemu : non vérifiée par moi (journaux de l'auteur seulement).

4. Table de mutation de l'auteur (24 lignes) : des mises à mort par horloge ?

Dans la table finale, aucune ligne Timeout sur e290980 ni sur ce110ed5. La seule ligne Timeout est celle déclarée sur 9d6d5613 (mutant N2 lancé contre tout le fichier, où un autre test attend pour toujours). Reciblé sur son test d'assertion, il est tué par FAILED (rejoué par moi : 1 failed in 33.24s, sans timer). Mes relances sur ce head, avec une colonne TIMER pour chaque ligne :

Mutation Test lancé Résultat Horloge ?
Base, head non modifié server + protocol + recovery 22 passed —
D2 poll() décide la sortie (B4) test_the_exit_is_read_from_the_pipe_not_from_waitpid TUÉE (49 s) non
SIG signature non vérifiée (N1) test_managed_pg_protocol.py TUÉE (45 s) non
NONCE nonce non vérifié (N1) test_managed_pg_protocol.py TUÉE (41 s) non
N2 effacement de postmaster.pid avant le lock test_an_unrecorded_lock_holder_that_cannot_sign_is_named_and_left_alone TUÉE (33 s) non
N3 le superviseur abandonne à la mort (break) test_a_postmaster_that_dies_under_clients_is_started_again TUÉE (46 s) non
N3 le client fait confiance à running test_a_client_never_takes_a_running_status_whose_postmaster_is_dead TUÉE (36 s) non
IGNORE le superviseur ignore l'événement de mort (non revendiqué par l'auteur) test_a_postmaster_that_dies_under_clients_is_started_again « tuée » par Timeout oui : c'est pour cela que l'auteur ne la revendique pas

git status propre après chaque mutation. Les autres lignes de la table de l'auteur (M1 à M8, B1 à B3, D1, D3, D5, N5) n'ont pas été rejouées par moi ; aucune n'affiche Timeout dans ses journaux. Aucun mutant que la PR annonce comme tué ne repose sur une horloge.

5. Non vérifié

Linux (glibc, qemu) et Rosetta x86_64 (preuves de l'auteur) ; attestation depuis main (après merge) ; N9 ; course supervisor.lock ; latence des hooks sans serveur vivant ; désaccord de versions plugin/roue Codex ; suite complète (CI).

6. Nettoyage

  • Worktree détaché .claude/worktrees/review-748-r5 supprimé (git worktree remove, status propre ; absent de git worktree list et du disque). Comme au round 4, register-worktree refuse les worktrees détachés : aucune entrée de ledger.
  • Temp disk-hygiene-8wamenba (185 Mo : venv de scratch, binaires PG, cluster et HOME de scratch, sondes) créé par create-temp, marqué evidence-preserved, puis supprimé par dispose. Ref locale refs/remotes/pr/748 supprimée.
  • Processus : tous les miens sont terminés (orphelin adopté arrêté au dernier client ; les deux postmasters lancés à la main sont sortis, le premier par l'arrêt d'urgence de B5, le second sur mon SIGINT ; fakes et threads attendus). ps/lsof : 0 restant. Je n'ai rien tué que je n'avais pas lancé.
  • Non à moi, laissés en place : quatre fake_postgres orphelins (ppid 1) dans /private/var/folders/0c/_rq0tstn19z9j3vtt65phgxr0000gn/T/pytest-of-cdeust/pytest-{365,367,388,397}/…. Les pids 62501 et 10991 datent de 19:46 et 19:54, le 67584 de 21:35 ; leur cwd est /Users/cdeust/.claude/jobs/615d7f94/disk-hygiene-_x6985c5/mutr5, le répertoire de mutation de l'auteur. Le pid 65859 date de 22:11 ; son cwd est …/feat-744-pg0-launcher/.claude/worktrees/zetetic-fail-before-7955. Les tests de récupération qui bloquent ou sont mutés laissent donc des postmasters factices orphelins : à nettoyer par leur propriétaire.
  • Conservé : ce verdict et ~/.claude/fleet/evidence/cortex-748-round5-review/ (88 Ko).

…live postmaster; fakes die with their test (ADR-1110)

Round-5 review of e290980:
- B5: start() removed the postmaster.pid of a live postmaster of the
  cluster that Cortex did not start (it holds no postmaster.lock), and
  PostgreSQL shut it down. Now the file is removed only when proven stale:
  the process it names started later than line 3 (MyStartTime) plus 2 s
  (measured slack, macOS and Linux incl. qemu) — a reused PID. A live writer
  is named with pid and port and left alone (not adopted: without the lock
  there is no exit event to supervise or stop on).
- N8: only a proven writer of a ready postmaster.pid is ever dialled while
  an unrecorded group holds postmaster.lock.
- N9: the ADR states that such a group that neither exits nor gets ready
  blocks attach (D2).
- HOL plugin scan (HARDCODED_SECRET, high, introduced in round 5): the test
  password is generated at run time.
- Orphan fakes: every fake postmaster follows a lifeline lock its test holds
  (fake_pg_lifeline, autouse); it exits on any end of the test process,
  kill and os._exit included.
- pidfile parsing and proofs move to managed_pg_pidfile.py.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BYyqzx4FVMHasVPZV3uBYq

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Storage: replace Homebrew PostgreSQL with pg0 installed and started by the launcher

2 participants