Repository navigation
Conversation
|
ZETETIC-REVIEW: REQUEST_CHANGES 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
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. |
|
ZETETIC-REVIEW: REQUEST_CHANGES
|
… 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
f33db02 to
6214187
Compare
|
ZETETIC-REVIEW: REQUEST_CHANGES Revue adversariale PR #748 (round 3) — ADR-1110, theseus + pgvector_compiledRevue en lecture seule d'un worktree détaché 1. Statut des bloquants du verdict précédent (2026-10-16)
2. Table de mutation (une garde à la fois, tests ciblés, chaque mutation annulée ensuite ;
|
| 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 quescripts/init_plugin.pyaffiche en direct).setup_steprenvoie"": l'outcome resteready,init_pluginsort 0, et aucun lecteur deprovisioned.jsonn'existe dansmcp_server/(grep). Un utilisateur marketplace ouclaude --init-onlynu 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 -wsans-tniPGCTLTIMEOUT(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 :
_selectrevérifie l'existence du fichier SQLite, mais un lancement écrit son marker avant que le moteur créememory.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 :
_insidevalide noms et cibles de liens sans modéliser les chaînes de liens symboliques internes à l'archive ; la protection réelle estfilter="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 sousdist/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 ;attachdit « root is missing » sans indiquerclaude --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_portpuispg_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
sqlitedu 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
- Codex / lancements directs : s'attachent-ils au serveur managed, ou erreur documentée (état actuel) ?
pg_ctl -wavec son délai de 60 s par défaut : accepté, ou-t/PGCTLTIMEOUTtrès grand + attente événementielle ?- Repli SQLite : suffit-il de
setup.log, ou faut-il une ligne visible (outcome Setup + SessionStart) ? - Confiance dans
nicoloboschi/pgvector_compiled(binaire d'un dépôt personnel, digest épinglé) ou compilation par la CI de Cortex. - 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/.
… 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
|
ZETETIC-REVIEW: REQUEST_CHANGES Revue adversariale PR #748 (round 4) — ADR-1110, PostgreSQL géréRevue en lecture seule, worktree détaché 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
e2e rejoué par moi (macOS arm64, 2. BLOQUANTB4 — 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 ».
3. NON BLOQUANTS (classés)N1 — Signature serveur et nonce SCRAM : code correct, gardes non testées. N2 — Superviseur tué entre spawn et enregistrement : récupération automatique, mais par accident, et l'ADR dit le contraire (point 4).
N3 — Un postmaster mort sous un superviseur vivant n'est jamais remplacé tant qu'un client vit. N4 — D4 : chaîne d'approvisionnement saine. La provenance ne nomme qu'une branche, et une branche peut réserver une release. N5 — D1 vérifié, avec deux restes. N6 — Point 7 : N7 — Statut Vérifié sain, exécuté : chemin de readiness, où PG réécrit 4. Réponses aux points de l'orchestrateur
5. Table de mutation (exécutée par moi sur 52c3156, une garde à la fois,
|
| 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-r4supprimé (git worktree remove, status propre, absent degit worktree listet du disque).register-worktreel'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é parcreate-temp, marquéevidence-preserved, puis supprimé pardispose. - Ref locale
refs/remotes/pr/748supprimé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'auteurfeat-744-pg0-launchern'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
|
ZETETIC-REVIEW: REQUEST_CHANGES Revue adversariale PR #748 (round 5) — ADR-1110, PostgreSQL géréRevue en lecture seule, worktree détaché 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 : Il reste un bloquant, B5. 1. Points du round 4 rejoués sur ce head
Second point de l'orchestrateur sur N2. Un processus étranger du même utilisateur qui tient 2. BLOQUANTB5 —
L'invariant « lock tenu ⇒ aucun postmaster vivant » n'est vrai que pour les postmasters que Cortex a lancés. Le corps de la PR («
3. NON BLOQUANTSN8 — Le processus composé n'est pas forcément « l'unique détenteur non enregistré du lock ». Exécuté ( N9 — Un détenteur du lock qui ne devient jamais Mutant non revendiqué (superviseur qui ignore l'événement de mort). Exécuté : Course résiduelle sur Corrections de test pour qemu. Lues : le test client utilise un autre processus vivant comme postmaster « redémarré », parce que qemu-user falsifie 4. Table de mutation de l'auteur (24 lignes) : des mises à mort par horloge ?Dans la table finale, aucune ligne
5. Non vérifiéLinux (glibc, qemu) et Rosetta x86_64 (preuves de l'auteur) ; attestation depuis main (après merge) ; N9 ; course 6. Nettoyage
|
…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
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
start()removed thepostmaster.pidof a live postmaster Cortex did not startunlocked_postmaster.py): attach →FATAL: pre-existing shared memory block … is still in use,postmaster_pid_file: Nonepid <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_alonereadyfile naming a later-started live PID was dialledholder_mismatch.py:B saw: ['dialled']test_a_ready_file_whose_pid_started_later_is_stale_and_never_dialled: such a file is notreadyfor Cortex, so nothing is dialledFindings met or exceeded the "high" severity threshold)HARDCODED_SECRET(high) attests_py/infrastructure/test_managed_pg_protocol.py:25, score 96; main: 0 findings, score 100. Introduced by round 5 (_PASSWORD = "topsecret-cluster-password")secrets.token_hex(24)at run time; scanner on this head: 0 findings, score 100.MIN_SCORE/FAIL_ONuntouchedps: ppid 1, cwd…/mutr5and…/zetetic-fail-before-7955fake_pg_lifelineholds an exclusiveflockfor the test's life, each fake waits for a shared lock and exits when granted. Measured: pytest SIGKILLed mid-test → its fake exits (kqueueNOTE_EXIT); 26-mutant table and Linux runs: 0 fakes leftHow B5/N8 are decided, with no dialling and no clock: a
postmaster.pidwas 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 Linuxbtime, which is whole seconds. Measured kernel start minus a whole-second clock read by the process itself, 20 runs each: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.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.readyfile while an unrecorded group holds the lock (N2's orphan). It is adopted only if it signs with the cluster password.postmaster.lockunrecorded, and neither exits nor gets a proven writer toready, 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
pytest-timeoutTimeoutPopen.pollnow raises intest_the_exit_is_read_from_the_pipe_not_from_waitpid: the mutant fails at once withAssertionError: poll() decided the exit(table below, noTimeoutline anywhere)SURVIVED rc 0 ['19 passed']eachtest_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 aProtocolError, not aKeyErrorstart() raised ServerError … still runs (it holds postmaster.lock)(no pid, no port) andpostmaster.pid still present: Falsestart() -> <port> adopted,postmaster.pid still present: True; real PG 18.6 (the reviewer'skilled_supervisor.py, ported): the orphan is adopted and recorded, nolock file is invalidinpostgres.log,stoppedafter the last client. A lock holder that cannot authenticate:ServerError: pid <pid> on port <port> holds postmaster.lock … not adopted, signalled or removedrunning, client B:port … refusedport … accepts(same port); real PG 18.6 (reviewer'spm_crash.py):B (new client after the crash): connect ok 127.0.0.1:65330— the port A hadif: github.repository == 'cdeust/Cortex'github.ref == 'refs/heads/main'; docs and release notes verify with--source-ref refs/heads/mainpython -m mcp_serverand the Codex hookManagedPgError: … missing; run \claude --init-only``[cortex-mcp] Cortex not ready: … run \claude --init-only`/[cortex-hook] …`, exit 1, one lineHow N2 and N3 work now:
start()takespostmaster.lockbefore touchingpostmaster.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 itspostmaster.pid(kernel start time = line 3) reportsready. That writer is the one unrecorded process ever dialled; it is adopted and recorded only if it authenticates with the cluster password.clients.lock, or the postmaster's group releasingpostmaster.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 writesfailed, and the next client starts a new supervisor.runningchecks 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.Round 4 (kept for reference): the round-3 blockers, red then green
managedmarker ignoreddatabase_urle1.py:provision called: ['…/data/pg'], Setup failse1.py:provision called: [],said: managed PostgreSQL not selected: database_url … selects the store; the managed server is not started; testtest_a_configured_database_wins_over_a_managed_marker_in_setupstop()ranpg_ctl stopon itforeign.py:live_postmaster -> (31490, 54222);stopprobe.py:stop() invoked pg_ctl: -D … -m fast -w stoptest_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_ourspython -m mcp_server.doctorcrashed on a managed markerBackendMarkerError: 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`mut.py M3rerun on 6214187:rc 0 ['9 passed']test_permission_error_is_not_ours_even_with_a_listenerkills it (table below){"status": "ready", "error": ""}{"status": "ready", "error": "", "sqlite_fallback": "managed PostgreSQL did not start: …"}+[cortex-setup] SQLite fallback: …; SessionStart adds the lineSQLite fallback: <cause>Owner decisions
bin/postgres -D <cluster> -p <port>as its own child and records{pid, started, port},startedbeing 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 areadypostmaster.pidwhile an unrecorded group holdspostmaster.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). Nopg_ctlany more: readiness ispostmaster.pidreportingready(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 aflockonpostmaster.lock, granted once the postmaster and every child it forked are gone. No sleep, timeout or attempt cap anywhere; themanaged-pgCI job lost itstimeout-minutes. No pidfd: it is ENOSYS under qemu-user (measured).mcp_server/infrastructure/managed_pg*.py, renames ofscripts/launcher_pg*.py).apply_backend_resolutionattaches on amanagedmarker, sohypermnesia-mcp, the Codex hook entry (mcp_server.hooks.entry, whatruntime.pyexecutes —runtime.pyitself needed no change) andcortex-doctoruse the same locks and supervisor as the launcher.effective_backendstays 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 checksdirect_launch_same_url.sqlite_fallbackin<key>.setup.json(kept if a later step fails) and the SessionStart line, only while the store is SQLite..github/workflows/pgvector.yml+scripts/pgvector_build.py: pgvector v0.8.7 (commitf37c13f6verified; 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 withpostgres --single, glibc ≤ 2.34 checked, deterministic archive,actions/attest-build-provenance, published once as prereleasepgvector-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 onrefs/heads/main; this release predates that rule and was created from the PR branch (attestations namefeat/744-pg0-launcher, commitsd6f869d8and52c3156e, identical digests: the build is reproducible). The main push after the merge rebuilds, attests from main and printsrelease exists: nothing replaced, after whichgh attestation verify <archive> --repo cdeust/Cortex --source-ref refs/heads/mainpasses for the same digests (not verifiable before the merge).~/.claude/methodology/managed-pg(data, password, state, locks, logs); binaries stay in<plugin data>/pg/dist. Uninstalling the plugin never deletes memories;has_earlier_stateignoresmanaged-pg.Mutation table on this head (
b7baad55; one guard at a time, targeted tests, restored after; noTimeoutline, no fake left after any mutant)test_a_server_that_does_not_start_selects_nothingtest_setup_with_the_hook_option_is_not_eligible/test_the_hook_option_wins_over_a_managed_marker_and_starts_nothingtest_permission_error_is_not_ours_even_with_a_listenertest_a_digest_mismatch_is_refused_and_nothing_installed_init_schemaready despite DDL errorstest_one_failed_statement_fails_and_names_it_after_running_alltest_a_recorded_live_postmaster_is_ours/test_a_reused_pid_with_a_listener_is_not_ourstest_a_link_leaving_the_tree_is_refusedtest_an_existing_sqlite_store_is_never_switchedtest_a_configured_database_wins_over_a_managed_marker_in_setuppostmaster.pidnamestest_start_never_adopts_or_dials_a_foreign_listener/test_stop_never_signals_a_foreign_processtest_the_doctor_reports_the_managed_server_instead_of_crashingpostmaster.pidremovedtest_a_live_postmaster_cortex_did_not_start_is_named_and_left_alonetest_a_codex_hook_attaches_to_the_managed_serverwaitpidtest_the_exit_is_read_from_the_pipe_not_from_waitpid(assertion, 28 s)test_a_hostile_server_is_refused_and_never_gets_the_password[badsig]/[badnonce]postmaster.pidremoved before the lock / unrecorded holder never recoveredtest_an_unrecorded_lock_holder_that_cannot_sign_is_named_and_left_alone/test_an_unrecorded_postmaster_holding_the_lock_is_proven_and_adoptedrunningtest_a_postmaster_that_dies_under_clients_is_started_again/test_a_client_never_takes_a_running_status_whose_postmaster_is_deadtest_a_managed_server_that_cannot_run_ends_a_launch_in_one_line[cortex-mcp]test_a_ready_file_whose_pid_started_later_is_stale_and_never_dialledtest_an_eligible_store_left_on_sqlite_records_whytest_the_managed_root_is_not_earlier_cortex_stateNot 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)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, nopostmaster.pidleft; Ubuntu 24.04 arm64 containerpassed: true.killed_supervisor.py(N2: the orphan, a proven writer, is adopted and recorded,stoppedafter the last client) andpm_crash.py(N3: B gets A's port).27 passedeach, 0 fake postmasters left.572 passed, 1 skippedon 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.locktypecheck env), Linux and Darwin: 0 errors. HOL plugin scanner 3.43.1: 0 findings, score 100.Reported, not changed (owner's call)
timeout-minutes, a hungmanaged-pgjob ends at GitHub's 360-minute default; no verdict depends on it.supervisor.lockcan 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 orlibgssapi_krb5.so.2stay on SQLite, with the cause shown. Native Windows and musl stay on SQLite. A fresh install that skipsclaude --init-onlykeeps SQLite until the importer exists. Olddist/trees are not collected.🤖 Generated with Claude Code
https://claude.ai/code/session_01BYyqzx4FVMHasVPZV3uBYq