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 3 commits into
mainfrom
feat/744-pg0-launcher
Open

cdeust wants to merge 3 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).

Three 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), and the round-4 fix on top. ADR-1110 (wiki page and its mirror) is rewritten for every decision below.

Round 4: 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, and the server must finish SCRAM-SHA-256 with the cluster's password, server signature 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; gh attestation verify <archive> --repo cdeust/Cortex passes for each (subject digest = pin, source ref feat/744-pg0-launcher, commit d6f869d8). The workflow also runs when launcher_pg_assets.py changes: a version bump pushed on any branch publishes before review.
  • 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 (one guard at a time, targeted tests, restored after)

Mutation Killed by Result
M1 marker written despite a failed provision test_a_server_that_does_not_start_selects_nothing killed
M2a Setup eligibility skips the option mapping test_setup_with_the_hook_option_is_not_eligible killed
M2b launcher_backend.apply skips the option mapping test_the_hook_option_wins_over_a_managed_marker_and_starts_nothing killed
M3 PermissionError read as alive (survived in round 3) test_permission_error_is_not_ours_even_with_a_listener killed
M4 sha256 not compared test_a_digest_mismatch_is_refused_and_nothing_installed killed
M5 _init_schema ready despite DDL errors test_one_failed_statement_fails_and_names_it_after_running_all killed
M6a kernel identity never matches test_a_recorded_live_postmaster_is_ours killed
M6b start time ignored (a reused PID accepted) test_a_reused_pid_with_a_listener_is_not_ours killed
M7 tar member check disabled test_a_link_leaving_the_tree_is_refused killed
M8 an existing SQLite store ignored test_an_existing_sqlite_store_is_never_switched killed
B1 Setup reads the managed marker before the configured store test_a_configured_database_wins_over_a_managed_marker_in_setup killed
B2-start adopt whatever postmaster.pid names test_start_never_adopts_or_dials_a_foreign_listener killed
B2-stop signal whatever postmaster.pid names test_stop_never_signals_a_foreign_process killed
B3 doctor without the managed check test_the_doctor_reports_the_managed_server_instead_of_crashing killed
D1 direct launches do not attach test_a_codex_hook_attaches_to_the_managed_server killed
D2 exit decided by waitpid, not the pipe test_the_exit_is_read_from_the_pipe_not_from_waitpid killed: the mutant hangs, the suite's 300 s pytest-timeout guard ends it (the failure mode this mutant is about, not an assertion)
D3 fallback cause not recorded test_an_eligible_store_left_on_sqlite_records_why killed
D5 managed root counted as earlier Cortex state test_the_managed_root_is_not_earlier_cortex_state killed

M3, M6a, M6b, B2-start, B2-stop, B3 and D1 were rerun on this head (their tests changed in the last amend); the others on 8df17473, whose product code is this head's. On the first pass the B2 anchors no longer matched the reworked code (PATTERN NOT FOUND), and once re-anchored B2-stop survived because a SIGINT inherited as ignored (the suite ran as a background job) is invisible to a plain sleep-like process; the foreign process now records every SIGINT with an explicit handler, and B2-stop is killed.

Measured on this head

  • e2e (managed_pg_e2e.py, sandbox-exec no_homebrew.sb, env -i, fresh HOME, uv Python outside /opt/homebrew): macOS arm64 and x86_64 (Rosetta) passed: true, cluster_root = $HOME/.claude/methodology/managed-pg, binaries under data/pg/dist, direct_launch_same_url: true, vector 0.8.7, pg_trgm 1.6, status after last client stopped, no postmaster.pid left. Ubuntu 24.04 arm64 container: passed: true, same checks.
  • test_managed_pg_server.py on Linux arm64 and Linux x86_64 under qemu, non-root, in a container without init: all passed.
  • ruff check, ruff format --check, check_craftsmanship.py --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 with the uv.lock typecheck env, Linux and Darwin platforms: 0 errors.
  • Targeted pytest (launcher, setup, hooks entry/session start, backend marker, managed_pg, bootstrap, doctor sweep, CI tests): 709 passed, 1 skipped on 8df17473 (51 min); this head differs only in test_managed_pg_server.py and test_managed_pg_attach.py, rerun here: 19 passed.
  • Three defects found by these local runs and fixed: (a) after the last event, poll() could still say "running" because descriptors close before a process becomes reapable — the exit is now decided by the pipe's end of file (product code, regression test); (b) the fallback test leaked CORTEX_SQLITE_FALLBACK into later hook tests (_ensure_runtime writes os.environ); (c) the test postmaster installed its SIGINT handler late. Plus two test races fixed (port read from status.json after the supervisor stopped; SIGINT inherited as ignored).
  • Not run locally: the full suite (10 283 tests; the machine was at load average ~240, about 10 tests/min). CI is the authority for it.

Known limits (unchanged unless stated)

Linux distributions without libxml2.so.2, OpenSSL 3 or libgssapi_krb5.so.2 stay on SQLite, now 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. A supervisor killed between spawning the postmaster and recording it leaves a group holding postmaster.lock; the next start then fails loudly naming it.

🤖 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).

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