feat: add optional zombie-killer-tray integration (subprocess pattern) - #2
Conversation
T-20260926-368033290 (c) / T-20260926-212716751: adds an OPTIONAL integration with zombie-killer-tray (conservative cleanup of orphaned MCP and language-server processes across agent frameworks, not just Codex), mirroring the existing safe-start-for-codex integration pattern exactly: a git-pinned pip dependency, launched as its own subprocess via `python -m zombie_killer_tray watch ...`, coordinated through the shared zombie_events.jsonl audit log rather than an in-process import. CareCenter's own existing runtime-MCP-reaper (processes.py/watchdog.py) is unchanged -- it stays targeted at Codex-Companion processes specifically; this is a complementary, broader-scope tool, not a replacement. - New src/codex_logdatenbank_wartung/zombie_killer_integration.py: install_zombie_killer_package()/zombie_killer_install_target() (pinned GitHub spec with a local-sibling-checkout override, same as safe-start), launch_zombie_killer_watch() (spawns the watch subprocess with cwd set to a new zombie_killer_state_dir so its audit log lands there, tied to CareCenter's own PID via --parent-pid so it exits when CareCenter does), build_zombie_killer_status() (reads the last cycle event back out of zombie_events.jsonl for a status summary). - config.py: zombie_killer_watch_interval_seconds/zombie_killer_min_age_seconds fields + zombie_killer_state_dir property (CODEX_HOME/zombie-killer-tray). - cli.py: three new subcommands mirroring safe-start's -- zombie-killer-report / zombie-killer-install / zombie-killer-watch. - pyproject.toml: added to the `build` extras, pinned to dev-bricks/zombie-killer-tray@6c8eb2c (that repo's PR #4, src/-packaging, not yet merged -- move this pin to the merge commit once it lands). - README.md/README.de.md/README_de.md: CLI usage lines, env var override doc, credits line (kept README.de.md and README_de.md byte-identical, as their own contract test requires). - THIRD_PARTY_LICENSES.txt: matching SBOM entries for the new dependency. - 10 new tests in tests/test_zombie_killer_integration.py (install target resolution, install success/failure, launch command/cwd/state-dir construction and failure handling, status readback with/without a log). Deliberately NOT done in this PR (scoped out, not forgotten): tray-menu QAction/i18n wiring (tray.py/i18n.py) equivalent to safe-start's "Codex sicher starten" button. That needs interactive verification of the Qt tray this environment can't provide; the CLI-level integration above is the functional core ("eigener Subprozess, gemeinsame State-Dateien") and the GUI wiring can follow as its own reviewable change once someone can click through it. 431/431 tests green (429 baseline unrelated to this change + 10 new; the 3 pre-existing test_maintenance.py/test_orchestrator.py failures on origin/main are unaffected, unchanged before and after), ruff clean. Prepared in a fresh worktree off origin/main (not the existing, stale local `main`/`ticket/carecenter-zombie-killer-20260920` -- see message to team-lead re: that divergence). Not merging -- per the two-model rule, a different model/session should review before merge. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PTbvD41MCVmnQWaobvHfCk
|
Welcome! 👋 Thanks for your first pull request in this repository. A maintainer will review it soon. Please make sure:
Thanks for contributing! |
Resurrected by cherry-picking e7394f6's test_docs_contracts.py (test_llms_has_one_last_checked_source): llms.txt had accumulated two "Last-checked" markers (a header line and a trailing bare line) as later docs-refresh commits added the header without removing the legacy trailing line. That drift predates this branch and was invisible because no test enforced single-source-of-truth here until now. Keeping the header (the format the rest of the recent docs refreshes use) and dropping the trailing duplicate. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PTbvD41MCVmnQWaobvHfCk
|
Per team-lead instruction: recovered 2 unrelated commits found stranded on the local main clone / a pre-existing
Both cherry-picks needed manual conflict resolution (the old commits' base predates 5 newer upstream doc/security commits) — resolved by keeping the current/newer wording and applying only the substantive additions. Also found and fixed a parity break the original commit introduced back on 2026-09-20: it only updated The original branch is preserved, not deleted: local tag 437/437 tests green (434 pre-existing/unrelated-baseline + this PR's own 3 new suites; the 3 known |
zombie-killer-tray#4 (src/-Paketierung) merged as 039b4f2. Updates the pinned dependency in pyproject.toml's build extras, the module-level ZOMBIE_KILLER_PACKAGE_SPEC constant, and the matching THIRD_PARTY_LICENSES.txt entry from the pre-merge branch head (6c8eb2c) to the actual merge commit. Verified: installed the new pin into a clean venv and ran `python -m zombie_killer_tray scan` there successfully before running the full suite. 437/437 tests green (same 3 known-unrelated baseline failures), ruff clean. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PTbvD41MCVmnQWaobvHfCk
|
Pin updated: |
lukisch
left a comment
There was a problem hiding this comment.
Review (merge-reviewer, claude-opus) auf Head 38d3e47: nicht gemergt. Lokal 440 passed / 1 skipped, ruff sauber, Pin 039b4f2 stimmt.
Sicherheitsfix (2f493a9 = gerettetes 1587edf, Patchinhalt gleich), bewertet gegen T-20260816-50:
- Gut:
reap_runtime_mcp_duplicatesverlangt jetztparent_is_deadfür jede Kohorten-Wurzel. Damit ist der Präzedenzfall geschlossen, eine aktive Kohorte mit lebendem app-server wird nicht mehr beendet._parent_is_deadbehandelt PID-Reuse korrekt (Parent jünger als Kind = tot) und bei unbekannten Zeiten fail-safe (nicht tot). Die Parent-Historie dient nur dem Audit. - Blockierend:
taskkill /T /Finreap_runtime_orphans. Der Fix macht aus dem Einzel-Kill stillschweigend einen Baum-Kill (im Commit-Text nicht erwähnt). Alle Nachfahren der Waise werden mitbeendet, ohne dass für sie irgendein Kriterium geprüft wird: kein Parent-tot (ihr Parent, die Waise, lebt ja noch), keine Art, kein Alter, keine CPU-Ruhe (geprüft wird nur die Wurzel). Zudem baut Windows den Baum über aktuelle PPIDs, eine Fremdprozess-Adoption über eine wiederverwendete PID ist also möglich. Das widerspricht „Kill-Kriterium ausschließlich Parent tot“ und dem zombie-killer-Grundsatz „no blanket process-tree kills“. Bitte/Tentfernen. Nachfahren werden im nächsten Zyklus als eigene Waisen mit eigenen Kriterien erfasst. - Hinweis: Die MCP-Marker
" mcp","mcp ","-mcp","_mcp"sind breite Substrings über Pfad und Kommandozeile. Solange Parent-tot, Alter und CPU-Ruhe greifen, ist das innerhalb der Politik. Es vergrößert aber die Kandidatenmenge deutlich; ein Test mit einem Nicht-MCP-Prozess, der „mcp“ nur im Pfad trägt, wäre gut.
Integration zombie-killer-tray, blockierend, empirisch belegt: zombie-killer-watch startet python -m zombie_killer_tray watch --yes ... --parent-pid <eigene PID> und das CLI beendet sich sofort danach. zombie-killer-tray beendet sich, sobald dieser Parent tot ist. Nachgestellt mit Stand 039b4f2 (Aktion scan, gleicher Parent-Mechanismus): Der Watcher ist schon beim Start weg (RuntimeError: tray parent cannot be verified), nach 1/3/8 s lebt er nicht mehr. Die Funktion arbeitet also nie. Die Tests mocken Popen und sehen das nicht. Entweder kein --parent-pid (dann gehört der Watcher dem Aufrufer, und die Prozess-Hygiene braucht einen anderen Lebenszyklus, z. B. Job-Objekt oder eigener Tray) oder eine langlebige CareCenter-Instanz als Parent. Bitte mit einem echten, nicht gemockten Lebensdauer-Test belegen.
Review findings on this PR:
(1) LIFECYCLE BUG (blocking): launch_zombie_killer_watch() passed
--parent-pid <this process's own PID>. zombie-killer-tray's watch_parent
thread kills the watch process as soon as whatever PID it was given for
--parent-pid exits -- and the CLI process that spawns the watcher exits
immediately after Popen returns, so the watcher died within ~1s of being
started, before doing any real work. Reproduced exactly as described:
mocked-Popen tests never caught it because they don't run the real
subprocess lifecycle at all.
Fix: drop --parent-pid entirely. The watcher is now a genuinely detached,
long-lived process; its lifecycle is governed by its own PID file
(<state_dir>/watch.pid, written by launch_zombie_killer_watch) rather than
any parent/child relationship. Added stop_zombie_killer_watch() (reads
the PID file, verifies via psutil that the PID still belongs to
zombie-killer-tray before signalling it -- refuses to touch an unrelated
process that reused the PID) and a new `zombie-killer-stop` CLI
subcommand.
Added a REAL integration test (no mocked Popen) that spawns the actual
zombie-killer-tray package as a subprocess exactly the way production
code does, asserts it is still running 2s after the launching call
returns (the old bug would already have killed it by then), then asserts
zombie-killer-stop actually terminates it. Requires the package
installed (pinned dependency); added the same pip-install step already
used for safe-start-for-codex to CI.
(2) taskkill /T on a single-process orphan kill (blocking): the rescued
watchdog commit (1587edf) changed reap_runtime_orphans' fallback kill
from a single-PID `taskkill /F` to a tree-kill `taskkill /T /F`. Every
PID reaped there was individually vetted (dead parent, min age, two idle
CPU snapshots) -- its descendants were never checked against those same
criteria, so a tree-kill would terminate processes no criterion actually
covers. Reverted to a single-PID kill; a genuinely orphaned descendant
becomes reap-able on its own in a later watchdog cycle once it
independently satisfies the same criteria. (reap_runtime_mcp_duplicates'
own, separate `taskkill /T` for duplicate-cohort launcher trees is
unrelated and untouched -- that one is gated by dead-parent + duplicate-
signature + cohort criteria on the ROOT before the tree-kill fires, which
is the design the review is asking this path to match, not diverge from.)
Added a negative test for the broad MCP substring markers ("-mcp",
"_mcp", " mcp") in processes.py -- it caught a real false positive:
"acme_mcpayments.exe" and "battle-mcp-launcher.exe" were both
misclassified as MCP servers via a substring match with no word-boundary
check. Replaced the plain substring checks for those three broad markers
with a regex requiring "mcp" to end at an actual boundary (path
separator, dot, space, or end of string) rather than continuing into an
unrelated word -- keeps catching the real "<name>-mcp" server-binary
convention (e.g. "ellmos-controlcenter-mcp/dist/index.js") without the
false positives.
(3) safe-start's [tool.hatch.metadata] fix is in that repo's own PR, not
this one -- CareCenter doesn't have a direct-reference optional extra of
this kind (its build extra already declared a direct git reference for
safe-start-for-codex before this PR and CI already accounted for it).
442/442 tests green (439 pre-existing/unrelated-to-this-PR + this PR's
own 3 new test areas; the 3 known-unrelated test_maintenance.py/
test_orchestrator.py failures on origin/main are unchanged), ruff clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PTbvD41MCVmnQWaobvHfCk
|
Addressed all 3 review findings ( (1) Lifecycle bug — confirmed: (2) Added the requested negative test for the broad MCP markers and it caught a real bug: (3) N/A for this repo — CareCenter already had a direct-reference 442/442 tests green (439 pre-existing + this PR's own areas), ruff clean. |
lukisch
left a comment
There was a problem hiding this comment.
Re-Review (merge-reviewer, claude-opus) auf Head 9100c3b: nicht gemergt. Die Punkte aus dem ersten Review sind im Kern behoben: kein --parent-pid mehr, /T ist entfernt (CareCenter), die MCP-Regex ist grenzbewusst.
Real nachgestellt, und zwar bewusst mit watch ohne --yes (Preview, keine Kills), Launcher als eigener, sofort endender Prozess wie das CLI, zombie-killer-tray 039b4f2:
- Watcher lebt 3 s nach Ende des Launchers → ja.
stop→ok, Watcher beendet → ja. Keine Rest-Prozesse. - Stale PID auf einen fremden, lebenden Prozess →
not-found, nicht beendet → ok.
Blockierend:
3. Stop ist fail-open: Ist psutil nicht importierbar, gibt _is_our_watch_process None zurück, und stop beendet die PID aus der Datei ungeprüft. Nachgestellt: stale PID auf einen fremden Python-Prozess, psutil-Pfad None → stop meldet ok und tötet den Fremdprozess. Das Paket deklariert psutil nicht selbst; wer das Extra später entfernt, hat genau diesen Zustand. Der Test test_stop_reports_already_stopped_for_a_dead_pid legt dieses Verhalten sogar fest. Fix: bei None nicht beenden (fail-closed), Status unverifiable.
4. Doppelstart hinterlässt einen Waisen-Watcher: Ein zweites zombie-killer-watch überschreibt watch.pid, der erste Watcher läuft weiter und ist per stop nicht mehr erreichbar. Nachgestellt: nach dem Stop lebte der erste Watcher noch. Fix: vor dem Start prüfen, ob die PID-Datei auf einen lebenden, verifizierten Watcher zeigt, dann ablehnen oder melden.
5. Der „echte“ Integrationstest macht echte Kills: test_real_watch_subprocess_outlives_its_launcher_and_stop_terminates_it startet watch --yes --interval 3 --min-age 30. Der erste Zyklus läuft sofort und beendet auf der Maschine, die den Test ausführt (Dev-Rechner, CI), reale Waisen ab 30 s Alter. Ich habe ihn deshalb bewusst nicht ausgeführt. Ein Test darf keine realen Prozesse beenden. Fix: launch_zombie_killer_watch(..., apply=False) bzw. ohne --yes im Test (der Lebenszyklus ist derselbe).
6. py -3 als Kandidat: In gefrorenen Builds (sys.frozen, also der ausgelieferte EXE-Fall) ist py -3 der erste Kandidat. Dann steht die PID des py.exe-Launchers in watch.pid, der eigentliche Watcher ist dessen Kind. stop beendet nur den Launcher (die cmdline-Prüfung besteht, weil sie den Modulnamen enthält), der Watcher läuft als Waise weiter. Fix: die echte Watcher-PID bestimmen (Kind von py.exe) oder py als Kandidat für den Watch-Start ausschließen.
Nicht blockierend: Die PID-Verifikation per cmdline-Substring trifft auch einen fremden zombie_killer_tray-Prozess (z. B. den Watcher des Schwesterprojekts). Robuster wäre es, beim Start create_time in die PID-Datei zu schreiben und beim Stop zu vergleichen.
Lokal: 444 passed, 1 skipped (ohne den realen Kill-Test). CI grün.
…test
Re-review findings, all 4 addressed:
(1) stop was fail-open on unverifiable PIDs. _is_our_watch_process()
returned None when psutil was unavailable, and stop treated "not False"
as "safe to kill" -- so a stale, unverifiable PID file could terminate an
unrelated process that happened to reuse the PID. Replaced with
_verify_watch_process(pid, expected_create_time), and stop now refuses
(status="verification-unavailable", process untouched) whenever
verification returns None, never falling through to os.kill(). Declared
psutil explicitly in pyproject.toml's build extra (was only relying on it
arriving transitively via zombie-killer-tray).
Also added the create_time cross-check the review recommended: the PID
file now stores {"pid", "create_time"} (written from the real watcher's
own psutil.Process(pid).create_time() right after spawn), and
verification compares it against the live process's actual create_time --
catches PID reuse even when the cmdline substring check alone might not
(e.g. a same-named relaunch).
(2) A second `zombie-killer-watch` silently overwrote watch.pid, orphaning
the first (still-running) watcher. launch_zombie_killer_watch() now reads
and verifies any existing PID file before spawning: a confirmed-alive
watcher refuses a second start (status="already-running"); an
unverifiable one refuses too, fail-closed, same direction as (1)
(status="verification-unavailable"); only a confirmed-dead/stale entry is
cleared and a fresh watcher started.
(3) The integration test ran the real watch subprocess with --yes,
meaning it could actually reap real, qualifying orphan processes on
whatever machine ran it (including CI). Added apply: bool = True to
launch_zombie_killer_watch() (--yes only when apply is True) and switched
the integration test to apply=False -- proves the lifecycle fix (survives
its launcher, responds to stop) without ever being armed to terminate
anything.
(4) In a frozen (PyInstaller) build, sys.executable is the packaged host
app, not an interpreter, so launching via the `py -3` Python Launcher was
the only remaining candidate -- but py.exe spawns the real interpreter as
its OWN child process (Windows has no exec()), so the PID Popen returned
was the launcher's, not the worker's. Killing the launcher on stop never
touched its child, leaving it running as an unreachable orphan. Replaced
the multi-candidate launch loop with _resolve_watch_python_executable():
not frozen -> sys.executable directly (already a real interpreter); frozen
-> resolve the real path ONCE via each launcher candidate's own
`-c "import sys; print(sys.executable)"` stdout, then spawn the watch
subprocess directly against that resolved path, never through py.exe.
Added a unit test simulating the frozen case with a mocked runner.
449/449 tests green (6 new: preview-mode, 2 double-start-guard directions,
fail-closed-stop, frozen-resolution unit test, plus the already-existing
areas re-verified; same 3 known-unrelated baseline failures unchanged),
ruff clean.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PTbvD41MCVmnQWaobvHfCk
|
All 4 re-review findings addressed ( (1) fail-open stop — (2) double-start — (3) real orphans killed by the test — added (4) launcher-PID orphan in frozen builds — replaced the candidate-loop launch with 449/449 tests green (6 new areas), ruff clean. |
|
Runde-3-Review (merge-reviewer, claude-opus): real nachgestellt mit zombie-killer-tray 039b4f2, Watcher im Preview-Modus. (1) Ohne psutil → verification-unavailable, Fremdprozess lebt weiter. (2) Passende cmdline, aber create_time um 100 s abweichend → not-found, Watcher unberührt. (3) Zweiter Start → already-running. (4) Der reale Integrationstest ruft apply=False auf, im Kommando steht kein --yes, geprüft im Code und per Assertion. (5) Gefrorener Fall (sys.frozen): Aufgelöst wird der echte python.exe (nicht py.exe), die PID-Datei zeigt auf den Watcher selbst, sein einziges Kind ist conhost, das mit ihm endet. Stop beendet sauber, danach keine zombie_killer_tray-Prozesse mehr. Kill-Kriterium bleibt das von zombie-killer-tray (Parent tot). Nicht blockierend: Eine PID-Datei im Altformat (nackte Zahl aus Runde 2) würde in _read_watch_pid_file an AttributeError scheitern. Runde 2 wurde nie gemergt, im Feld existiert das Format also nicht. Merge. Lokal 452 passed / 1 skipped, ruff sauber, CI grün. |
Summary
python -m zombie_killer_tray watch ...), coordinated via the sharedzombie_events.jsonlaudit log rather than an in-process import. CareCenter's own existing runtime-MCP-reaper stays unchanged and Codex-Companion-specific; this is complementary, broader-scope tooling.zombie_killer_integration.py: install/target-resolution,launch_zombie_killer_watch()(spawns the watch subprocess withcwdset to a newzombie_killer_state_dir, tied to CareCenter's own PID via--parent-pid),build_zombie_killer_status()(reads the last cycle back out of the audit log).config.py: new interval/min-age fields +zombie_killer_state_dirproperty.cli.py: three new subcommands mirroring safe-start's —zombie-killer-report/-install/-watch.pyproject.toml: added to thebuildextras, pinned todev-bricks/zombie-killer-tray@6c8eb2c(that repo's PR #4, src/-packaging, not yet merged — move this pin to the merge commit once it lands).Deliberately not in scope here
Tray-menu QAction/i18n wiring (
tray.py/i18n.py) equivalent to safe-start's "Codex sicher starten" button — that needs interactive verification of the Qt tray this environment can't provide. The CLI-level integration above is the functional core ("eigener Subprozess, gemeinsame State-Dateien") the ticket asked for; GUI wiring can follow as its own reviewable change.Also worth a look
Found while preparing this: the local
mainclone and its pre-existingticket/carecenter-zombie-killer-20260920worktree/branch had diverged fromorigin/main— 2 local-only commits sitting on a base that's missing 5 upstream commits (README/CHANGELOG/security-audit work). Not touched here; this PR is built fresh offorigin/mainin a new worktree instead. Flagging so it doesn't get lost — see the corresponding note to the team lead.Test plan
tests/test_zombie_killer_integration.pytest_maintenance.py/test_orchestrator.pytests onorigin/mainare unrelated and unchanged before/after this PR)ruff check .cleanREADME.de.md/README_de.mdstayed byte-identical (own contract test)Not merging — per the two-model rule, a different model/session should review before merge.
🤖 Generated with Claude Code
https://claude.ai/code/session_01PTbvD41MCVmnQWaobvHfCk