From 27e4b71a9710958fd28fd33dc12be734e1475543 Mon Sep 17 00:00:00 2001 From: mike Date: Thu, 28 May 2026 21:26:57 -0400 Subject: [PATCH] feat(launchd): durable LaunchAgent for codex_session daemon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors the conductor-tick launchd pattern (home-lab launchd/): - plist template with __REPO_ROOT__/__HOME__/__PYTHON3__ placeholders - install script resolves python3, expands template, bootstrap/enable/verify - uninstall script is idempotent bootout + plist removal - README explains KeepAlive (long-running daemon) vs StartInterval (one-shot every N seconds) — codex_session is the former, conductor- tick is the latter Smoke verified: install bootstraps cleanly, daemon respawns on kill, signal files appear at ~/.cache/claude-tab-status/codex-*.json. --- launchd/README.md | 55 ++++++++++++++++ launchd/com.local.codex-tab-status.plist | 54 +++++++++++++++ launchd/install-codex-tab-status-launchd.sh | 66 +++++++++++++++++++ launchd/uninstall-codex-tab-status-launchd.sh | 21 ++++++ 4 files changed, 196 insertions(+) create mode 100644 launchd/README.md create mode 100644 launchd/com.local.codex-tab-status.plist create mode 100755 launchd/install-codex-tab-status-launchd.sh create mode 100755 launchd/uninstall-codex-tab-status-launchd.sh diff --git a/launchd/README.md b/launchd/README.md new file mode 100644 index 0000000..ad0c899 --- /dev/null +++ b/launchd/README.md @@ -0,0 +1,55 @@ +# launchd — codex tab status daemon + +Durable supervision for `scripts/codex_session.py --daemon` via macOS launchd. +Mirrors the pattern in `~/code/home-lab/launchd/` (conductor-tick). + +## Files + +| File | Purpose | +|---|---| +| `com.local.codex-tab-status.plist` | LaunchAgent template — `__REPO_ROOT__`, `__HOME__`, `__PYTHON3__` substituted at install time | +| `install-codex-tab-status-launchd.sh` | Resolves python3, expands the plist, bootstraps into `gui/$UID`, verifies first tick | +| `uninstall-codex-tab-status-launchd.sh` | Idempotent bootout + plist removal | + +## Install + +```bash +bash launchd/install-codex-tab-status-launchd.sh +``` + +Verifies by: +- Tailing the log at `~/.cache/codex-tab-status.log` +- Listing existing codex signal files at `~/.cache/claude-tab-status/codex-*.json` + +After install, open a codex tab in iTerm2 — its title should pick up `⚡` while +codex is working and `💤` once a turn completes. The upstream iTerm2 adapter +(`claude_tab_status.py`) renders these unchanged. + +## Uninstall + +```bash +bash launchd/uninstall-codex-tab-status-launchd.sh +``` + +Stops the daemon and removes the agent plist. Signal files in +`~/.cache/claude-tab-status/` self-clean via the adapter's PID-liveness check +when the codex processes they describe exit. + +## Manage + +| Action | Command | +|---|---| +| Status | `launchctl print gui/$UID/com.local.codex-tab-status` | +| Logs | `tail -f ~/.cache/codex-tab-status.log` | +| Force restart | `launchctl kickstart -k gui/$UID/com.local.codex-tab-status` | + +## Why `KeepAlive` not `StartInterval` + +`codex_session.py --daemon` is a long-running poll loop (it watches for codex +process start/exit + scans rollout JSONLs continuously). That's a "keep this +running, respawn if it dies" job, not a "fire this every N seconds" job. So +the plist uses `KeepAlive=true` without `StartInterval`. + +Compare conductor-tick (`com.mikebook.conductor-tick`) which IS one-shot +per 120s — that uses `StartInterval=120` without `KeepAlive`. Different +script lifecycles, different launchd primitives. diff --git a/launchd/com.local.codex-tab-status.plist b/launchd/com.local.codex-tab-status.plist new file mode 100644 index 0000000..8368d60 --- /dev/null +++ b/launchd/com.local.codex-tab-status.plist @@ -0,0 +1,54 @@ + + + + + Label + com.local.codex-tab-status + + + + ProgramArguments + + __PYTHON3__ + __REPO_ROOT__/scripts/codex_session.py + --daemon + + + RunAtLoad + + + KeepAlive + + + StandardOutPath + __HOME__/.cache/codex-tab-status.log + StandardErrorPath + __HOME__/.cache/codex-tab-status.log + + WorkingDirectory + __HOME__ + + EnvironmentVariables + + HOME + __HOME__ + PATH + /opt/homebrew/bin:/usr/bin:/bin + + + diff --git a/launchd/install-codex-tab-status-launchd.sh b/launchd/install-codex-tab-status-launchd.sh new file mode 100755 index 0000000..e6f8f8a --- /dev/null +++ b/launchd/install-codex-tab-status-launchd.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# Install com.local.codex-tab-status as a launchd LaunchAgent. +# +# Template-expands the plist into ~/Library/LaunchAgents, bootstraps into +# gui/$UID domain, enables, verifies. Uses modern launchctl bootstrap/ +# enable/print verbs (NOT the deprecated load/unload). +# +# Pattern source: ~/code/home-lab/launchd/install-conductor-tick-launchd.sh +set -euo pipefail + +SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +LAUNCH_AGENTS_DIR="${HOME}/Library/LaunchAgents" +PLIST_NAME="com.local.codex-tab-status.plist" +LABEL="com.local.codex-tab-status" +SRC="${SCRIPT_DIR}/${PLIST_NAME}" +DST="${LAUNCH_AGENTS_DIR}/${PLIST_NAME}" +LOG_PATH="${HOME}/.cache/codex-tab-status.log" + +# Resolve a real python3 — prefer homebrew, fall back to system. The plist +# embeds the resolved path so launchd doesn't need to search PATH at fire. +PYTHON3="$(command -v python3 || true)" +if [[ -x /opt/homebrew/bin/python3 ]]; then + PYTHON3="/opt/homebrew/bin/python3" +fi +if [[ -z "${PYTHON3}" || ! -x "${PYTHON3}" ]]; then + echo "[install] FATAL: no python3 found on PATH" >&2 + exit 1 +fi + +echo "[install] python3: ${PYTHON3}" +echo "[install] repo root: ${REPO_ROOT}" +echo "[install] plist src: ${SRC}" +echo "[install] plist dst: ${DST}" +echo "[install] log path: ${LOG_PATH}" + +mkdir -p "${LAUNCH_AGENTS_DIR}" "${HOME}/.cache" + +# Template-expand placeholders +sed \ + -e "s|__REPO_ROOT__|${REPO_ROOT}|g" \ + -e "s|__HOME__|${HOME}|g" \ + -e "s|__PYTHON3__|${PYTHON3}|g" \ + "${SRC}" > "${DST}" + +echo "[install] bootout (idempotent) then bootstrap into gui/$(id -u)" +launchctl bootout "gui/$(id -u)/${LABEL}" 2>/dev/null || true +launchctl bootstrap "gui/$(id -u)" "${DST}" +launchctl enable "gui/$(id -u)/${LABEL}" + +echo "[install] launchctl print:" +launchctl print "gui/$(id -u)/${LABEL}" 2>/dev/null | sed -n '1,18p' + +echo +echo "[install] waiting 3s for daemon to start..." +sleep 3 +echo "[install] log tail (${LOG_PATH}):" +tail -10 "${LOG_PATH}" 2>/dev/null || echo " (log not yet created — that's fine if no codex tabs are open)" + +echo +echo "[install] signal files in ~/.cache/claude-tab-status/:" +ls -1 "${HOME}/.cache/claude-tab-status/codex-"*.json 2>/dev/null | head -5 \ + || echo " (none yet — open a codex tab in iTerm2 and re-check)" + +echo +echo "[install] done — uninstall with: bash ${SCRIPT_DIR}/uninstall-codex-tab-status-launchd.sh" diff --git a/launchd/uninstall-codex-tab-status-launchd.sh b/launchd/uninstall-codex-tab-status-launchd.sh new file mode 100755 index 0000000..908ade9 --- /dev/null +++ b/launchd/uninstall-codex-tab-status-launchd.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Uninstall com.local.codex-tab-status. +# +# Idempotent: bootout succeeds even if not loaded; rm -f tolerates a missing +# plist. Leaves signal files in ~/.cache/claude-tab-status/ alone — they +# self-clean via the adapter's PID-liveness check when their codex process +# exits. +set -euo pipefail + +LAUNCH_AGENTS_DIR="${HOME}/Library/LaunchAgents" +PLIST_NAME="com.local.codex-tab-status.plist" +LABEL="com.local.codex-tab-status" +DST="${LAUNCH_AGENTS_DIR}/${PLIST_NAME}" + +echo "[uninstall] bootout gui/$(id -u)/${LABEL}" +launchctl bootout "gui/$(id -u)/${LABEL}" 2>/dev/null || true + +echo "[uninstall] rm -f ${DST}" +rm -f "${DST}" + +echo "[uninstall] done — codex tabs will lose their status indicators next time the iTerm2 adapter sweeps."