Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions launchd/README.md
Original file line number Diff line number Diff line change
@@ -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.
54 changes: 54 additions & 0 deletions launchd/com.local.codex-tab-status.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.local.codex-tab-status</string>

<!--
Codex tab status signal producer.

Long-running daemon that writes JSON signal files to
~/.cache/claude-tab-status/codex-<rollout-uuid>.json describing each
live codex session's state. The upstream iTerm2 adapter
(claude_tab_status.py) consumes these files and renders the same
⚡ / 💤 prefix it already shows for claude tabs.

KeepAlive=true: the daemon is a long-running poll loop, not an
interval-driven one-shot. If it crashes, launchd respawns it.
Pattern mirrors com.mikebook.conductor-tick (home-lab launchd/),
minus the StartInterval (this is daemonized, not tick-fired).

Install via launchd/install-codex-tab-status-launchd.sh.
-->

<key>ProgramArguments</key>
<array>
<string>__PYTHON3__</string>
<string>__REPO_ROOT__/scripts/codex_session.py</string>
<string>--daemon</string>
</array>

<key>RunAtLoad</key>
<true/>

<key>KeepAlive</key>
<true/>

<key>StandardOutPath</key>
<string>__HOME__/.cache/codex-tab-status.log</string>
<key>StandardErrorPath</key>
<string>__HOME__/.cache/codex-tab-status.log</string>

<key>WorkingDirectory</key>
<string>__HOME__</string>

<key>EnvironmentVariables</key>
<dict>
<key>HOME</key>
<string>__HOME__</string>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/bin:/bin</string>
</dict>
</dict>
</plist>
66 changes: 66 additions & 0 deletions launchd/install-codex-tab-status-launchd.sh
Original file line number Diff line number Diff line change
@@ -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" \
Comment on lines +41 to +43
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Escape paths before substituting the plist template

When the checkout or home directory contains sed replacement metacharacters (for example a path like /Users/me/R&D/iterm2-tab-status or a volume/folder name containing |), these raw replacements either expand & back to the matched placeholder or break the sed expression, so the installed plist points at a non-existent script/log path or fails to bootstrap. Escape the replacement strings (and XML string content) before writing the LaunchAgent plist.

Useful? React with 👍 / 👎.

"${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"
21 changes: 21 additions & 0 deletions launchd/uninstall-codex-tab-status-launchd.sh
Original file line number Diff line number Diff line change
@@ -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."
Loading