fix(init): suppress hook recovery output (#760)

## Summary
- silence best-effort profile recovery while `headroom init hook ensure`
runs from installed hooks
- suppress both Python-level stdout/stderr and child process
file-descriptor output so SessionStart hooks do not emit invalid JSON
- add a regression test for noisy supervisor recovery failures

## Verification
- `python3 -m py_compile headroom/cli/init.py`
- live local hook probe: `headroom init hook ensure --profile default
--marker headroom-init-codex` exits 0 with empty output
- targeted pytest was not runnable locally because `uv.lock` currently
fails to parse due to an inconsistent GitPython wheel version entry
This commit is contained in:
Chris Yau 2026-06-12 07:59:31 +08:00 committed by GitHub
parent d76a7296df
commit b4395993ae
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 73 additions and 20 deletions

View file

@ -9,6 +9,8 @@ import shlex
import shutil
import subprocess
import sys
from collections.abc import Iterator
from contextlib import contextmanager, redirect_stderr, redirect_stdout
from hashlib import sha1
from pathlib import Path
from typing import Any
@ -552,31 +554,54 @@ def _install_copilot_marketplace() -> None:
)
@contextmanager
def _suppress_hook_output() -> Iterator[None]:
"""Keep best-effort hook recovery from emitting invalid hook output."""
stdout_fd = os.dup(1)
stderr_fd = os.dup(2)
try:
with open(os.devnull, "w", encoding="utf-8") as devnull:
sys.stdout.flush()
sys.stderr.flush()
os.dup2(devnull.fileno(), 1)
os.dup2(devnull.fileno(), 2)
with redirect_stdout(devnull), redirect_stderr(devnull):
yield
finally:
sys.stdout.flush()
sys.stderr.flush()
os.dup2(stdout_fd, 1)
os.dup2(stderr_fd, 2)
os.close(stdout_fd)
os.close(stderr_fd)
def _ensure_profile_running(profile: str) -> None:
manifest = load_manifest(profile)
if manifest is None:
return
if wait_ready(manifest, timeout_seconds=1):
return
try:
with acquire_runtime_start_lock(manifest.profile) as acquired:
if not acquired:
return
if wait_ready(manifest, timeout_seconds=1):
return
if runtime_status(manifest) == "running":
if wait_ready(manifest, timeout_seconds=_STARTUP_READY_TIMEOUT_SECONDS):
with _suppress_hook_output():
if wait_ready(manifest, timeout_seconds=1):
return
try:
with acquire_runtime_start_lock(manifest.profile) as acquired:
if not acquired:
return
stop_runtime(manifest)
if manifest.preset == InstallPreset.PERSISTENT_DOCKER.value:
start_persistent_docker(manifest)
elif manifest.supervisor_kind == SupervisorKind.SERVICE.value:
start_supervisor(manifest)
else:
start_detached_agent(manifest.profile)
wait_ready(manifest, timeout_seconds=45)
except Exception:
return
if wait_ready(manifest, timeout_seconds=1):
return
if runtime_status(manifest) == "running":
if wait_ready(manifest, timeout_seconds=_STARTUP_READY_TIMEOUT_SECONDS):
return
stop_runtime(manifest)
if manifest.preset == InstallPreset.PERSISTENT_DOCKER.value:
start_persistent_docker(manifest)
elif manifest.supervisor_kind == SupervisorKind.SERVICE.value:
start_supervisor(manifest)
else:
start_detached_agent(manifest.profile)
wait_ready(manifest, timeout_seconds=45)
except Exception:
return
def _probe_init_targets(global_scope: bool) -> list[tuple[str, str | None]]:

View file

@ -2,6 +2,7 @@ from __future__ import annotations
import importlib
import json
import os
import sys
import types
from contextlib import contextmanager
@ -842,6 +843,33 @@ def test_ensure_profile_running_covers_runtime_modes(monkeypatch) -> None:
assert ("docker-profile", 45) in wait_calls
def test_ensure_profile_running_suppresses_hook_recovery_output(monkeypatch, capfd) -> None:
init_cli, _ = _load_init_module(monkeypatch)
manifest = SimpleNamespace(
preset=init_cli.InstallPreset.PERSISTENT_TASK.value,
supervisor_kind=init_cli.SupervisorKind.SERVICE.value,
profile="service-profile",
)
monkeypatch.setattr(init_cli, "load_manifest", lambda profile: manifest)
monkeypatch.setattr(init_cli, "wait_ready", lambda manifest, timeout_seconds: False)
def noisy_start_supervisor(manifest) -> None:
print("python stdout")
print("python stderr", file=sys.stderr)
os.write(1, b"fd stdout\n")
os.write(2, b"fd stderr\n")
raise RuntimeError("not permitted")
monkeypatch.setattr(init_cli, "start_supervisor", noisy_start_supervisor)
init_cli._ensure_profile_running("service-profile")
captured = capfd.readouterr()
assert captured.out == ""
assert captured.err == ""
def test_ensure_profile_running_returns_when_ready_or_on_exception(monkeypatch) -> None:
init_cli, _ = _load_init_module(monkeypatch)
manifest = SimpleNamespace(