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
13 changes: 8 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,18 @@ Follow these patterns consistently (aligned with agent-session-analytics):
| Repo | `agent-event-bus` |
| Python package | `agent_event_bus` |
| MCP server name | `agent-event-bus` |
| CLI commands | `agent-event-bus`, `agent-event-bus-cli`, `agent-event-bus-bridge` (bridge is venv-only for now - run via `uv run`) |
| CLI commands | `agent-event-bus`, `agent-event-bus-cli`, `agent-event-bus-bridge` (supervised on macOS via `make install-bridge`; elsewhere run via `uv run`) |
| Resource URI | `agent-event-bus://guide` |
| Data directory | `~/.claude/contrib/agent-event-bus/` |
| Database | `~/.claude/contrib/agent-event-bus/data.db` |
| Log files | `agent-event-bus.log`, `agent-event-bus.err` |
| LaunchAgent | `com.evansenter.agent-event-bus.plist` |
| Log files | `agent-event-bus.log`, `agent-event-bus.err`; bridge: `agent-event-bus-bridge.log`, `agent-event-bus-bridge.err` (the bridge's are launchd's stdout/stderr capture, which launchd TRUNCATES on every restart - it has no append-mode file logger of its own yet) |
| LaunchAgent | `com.evansenter.agent-event-bus.plist`; bridge: `com.evansenter.agent-event-bus-bridge.plist` (separate unit - a bus host need not run a bridge) |
| systemd service | `agent-event-bus.service` |
| Wake spool dir | `~/.claude/contrib/agent-event-bus/wake/` (bridge spool/lock files + `panes.json` + `bridge.singleton.lock` - transient; the DB protection below does NOT extend to it. Safe to hand-clear **while no bridge is running** - clearing it under a live bridge orphans its `bridge.singleton.lock` inode, so a second instance acquires a fresh one) |
| Bridge hook-lock dir | `$XDG_RUNTIME_DIR/agent-event-bus-bridge-<uid>/`, else the system temp dir (`$TMPDIR`, or `/tmp` when unset; macOS: per-user `/var/folders/.../T`) - zero-byte, uid-scoped `hook.<hash>.lock` files, machine-scoped so a same-URL double-start refuses regardless of `$HOME`. Create-and-verified private (not adopted). Safe to remove when no bridge is running |

**Environment variables**: `AGENT_EVENT_BUS_*` prefix (e.g., `_DB`, `_LOG`, `_ERR`, `_URL`, `_AUTH_DISABLED`, `_ICON`, `_TESTING`, `_SESSION_ID`; bridge: `_BRIDGE_PORT`, `_BRIDGE_BACKEND`, `_BRIDGE_COOLDOWN`, `_BRIDGE_SECRET`, `_BRIDGE_HOOK_URL`, `_BRIDGE_BIND`, `_BRIDGE_ALLOWED_HOSTS`, `_WAKE_DIR`)
**Environment variables**: `AGENT_EVENT_BUS_*` prefix (e.g., `_DB`, `_LOG`, `_ERR`, `_URL`, `_AUTH_DISABLED`, `_ICON`, `_TESTING`, `_SESSION_ID`; bridge: `_BRIDGE_PORT`, `_BRIDGE_BACKEND`, `_BRIDGE_COOLDOWN`, `_BRIDGE_SECRET`, `_BRIDGE_HOOK_URL`, `_BRIDGE_BIND`, `_BRIDGE_ALLOWED_HOSTS`, `_BRIDGE_LOG`, `_BRIDGE_ERR`, `_WAKE_DIR`).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Suggestion] _BRIDGE_LOG and _BRIDGE_ERR are listed alongside _BRIDGE_PORT, _BRIDGE_BACKEND, and the rest, but they are a different kind of variable: every other _BRIDGE_* name is read by bridge.py at runtime, whereas these two are read only by scripts/install-bridge-launchagent.sh:16-17 and baked into the plist at install time. Setting them in the environment of a running bridge does nothing.

The Operations section already carries exactly this warning for the bus _LOG / _ERR pair (they must be in the environment of make install-server itself); the bridge pair needs the same note against make install-bridge.

Related: the installer runs mkdir -p "$DATA_DIR" only, so an AGENT_EVENT_BUS_BRIDGE_LOG pointing outside the data directory leaves launchd unable to open StandardOutPath. A mkdir -p "$(dirname "$BRIDGE_LOG_FILE")" would cover it.

One consulted variable is deliberately outside the prefix: **`CLAUDE_CODE_SESSION_ID`**, which the CLI reads as a session-attribution fallback when `--session-id` and `AGENT_EVENT_BUS_SESSION_ID` are both absent (#137).

---

Expand Down Expand Up @@ -65,10 +66,12 @@ make uninstall # Remove everything (preserves DB)
make dev # Install with dev dependencies
make check # Format + lint + test
make restart # Lightweight service restart (no dependency sync)
make install-bridge # Supervise the RFC #122 bridge (macOS LaunchAgent, idempotent)
make uninstall-bridge # Stop supervising it (leaves the bus, DB, and wake/ alone)
./scripts/dev.sh # Dev mode (foreground, auto-reload)
```

**When to restart**: Code changes to `server.py`, `storage.py`, `helpers.py` require `make install-server` (or `make restart`). `guide.md` is read fresh each request. Dev mode auto-reloads. The bridge is a separate process with no install target - after `bridge.py` changes, restart whatever runs `uv run agent-event-bus-bridge`.
**When to restart**: Code changes to `server.py`, `storage.py`, `helpers.py` require `make install-server` (or `make restart`). `guide.md` is read fresh each request. Dev mode auto-reloads. After `bridge.py` changes, restart the bridge: `make install-bridge` if it is supervised (macOS LaunchAgent, idempotent), else whatever runs `uv run agent-event-bus-bridge`.

## Testing

Expand Down
30 changes: 28 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: check fmt lint test clean install-server install-client uninstall dev venv restart logs
.PHONY: check fmt lint test clean install-server install-client uninstall dev venv restart logs install-bridge uninstall-bridge

# Canonical paths (override with matching AGENT_EVENT_BUS_* env vars)
LOG_FILE := $(or $(AGENT_EVENT_BUS_LOG),$(HOME)/.claude/contrib/agent-event-bus/agent-event-bus.log)
Expand Down Expand Up @@ -96,10 +96,36 @@ install-client:
@echo "Add to your shell profile (~/.zshrc, ~/.bashrc, or ~/.extra):"
@echo ' export AGENT_EVENT_BUS_URL="$(REMOTE_URL)"'

# Supervise the RFC #122 bridge (macOS only; idempotent, restarts on crash).
# Separate from install-server on purpose: the bridge is experimental, the bus
# is not, and a bus host does not have to run one. Requires the venv that
# install-server (or `make dev`) creates.
install-bridge:
@if [ "$$(uname)" != "Darwin" ]; then \
echo "install-bridge is macOS-only (LaunchAgent)."; \
echo "On Linux, run the bridge under your own supervisor:"; \
echo " uv run agent-event-bus-bridge"; \
exit 1; \
fi
./scripts/install-bridge-launchagent.sh

# Stop supervising the bridge. Leaves the bus, the database, and wake/ alone.
uninstall-bridge:
@if [ "$$(uname)" != "Darwin" ]; then \
echo "uninstall-bridge is macOS-only (LaunchAgent)."; \
exit 1; \
fi
./scripts/uninstall-bridge-launchagent.sh

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Suggestion] make uninstall (documented in CLAUDE.md as "Remove everything") does not touch the bridge unit, so after make install-bridge; make uninstall the bridge LaunchAgent stays loaded, KeepAlive keeps respawning it, and it backs off forever against a bus that no longer exists — while uninstall-cli.sh has just removed the agent-event-bus-cli that the bridge teardown notes tell you to verify with. Either invoke uninstall-bridge-launchagent.sh from uninstall when the bridge plist is present, or add a line to uninstall output pointing at make uninstall-bridge.


# Uninstall: service + CLI + MCP config
uninstall:
@echo "Uninstalling..."
@if [ "$$(uname)" = "Darwin" ]; then \
if [ -f "$$HOME/Library/LaunchAgents/com.evansenter.agent-event-bus-bridge.plist" ]; then \
echo "Removing bridge LaunchAgent first (KeepAlive would respawn it against a bus that is gone)..."; \
./scripts/uninstall-bridge-launchagent.sh; \
echo ""; \
fi; \
./scripts/uninstall-launchagent.sh; \
else \
./scripts/uninstall-systemd.sh; \
Expand All @@ -125,7 +151,7 @@ restart:
launchctl unload "$$PLIST" 2>/dev/null || true; \
launchctl load "$$PLIST"; \
sleep 1; \
if launchctl list | grep -q "com.evansenter.agent-event-bus"; then \
if launchctl list | grep -q "com.evansenter.agent-event-bus$$"; then \
echo "Service restarted successfully"; \
else \
echo "Error: Service failed to start. Check $(ERR_FILE)"; \
Expand Down
75 changes: 75 additions & 0 deletions scripts/com.evansenter.agent-event-bus-bridge.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?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.evansenter.agent-event-bus-bridge</string>

<!-- Runs as a module, matching the bus unit. bridge.py has the __main__
guard, so this is equivalent to the agent-event-bus-bridge console
script without depending on the venv's bin/ being on PATH. -->
<key>ProgramArguments</key>
<array>
<string>__VENV_PYTHON__</string>
<string>-m</string>
<string>agent_event_bus.bridge</string>
</array>

<key>WorkingDirectory</key>
<string>__PROJECT_DIR__</string>

<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
<key>PYTHONPATH</key>
<string>__PROJECT_DIR__/src</string>
<!-- Pinned rather than inherited. launchd does not run a shell, so a
profile that exports a different backend cannot reach this
process - but being explicit means the unit describes what runs
instead of leaving it to whatever the default happens to be.
tmux would additionally need wake/panes.json maintained by
something session-side; spool has no such dependency. -->
<key>AGENT_EVENT_BUS_BRIDGE_BACKEND</key>
<string>spool</string>
<!-- AGENT_EVENT_BUS_URL is deliberately NOT set: unset means the
bridge's own default (http://127.0.0.1:8080/mcp), which is
correct on the machine hosting the bus. Set it here only if the
bus moves off this box - and then the hook URL stops being
loopback, which makes AGENT_EVENT_BUS_BRIDGE_SECRET mandatory
(validate_config refuses an exposed listener without one). -->
</dict>

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

<!-- Restart on crash. Safe with the singleton lock: flock is released
when the dead process's fd closes, so the replacement acquires it
cleanly rather than refusing to start. The startup sweep also
reclaims the webhook row the dead instance left registered. -->
<key>KeepAlive</key>
<true/>

<!-- launchd's default respawn throttle, stated explicitly because it is
load-bearing here: it bounds how fast a crash loop rewrites the log
below, and how fast a failing bridge re-registers against the bus. -->
<key>ThrottleInterval</key>
<integer>10</integer>

<!-- The bridge logs via logging.basicConfig, which is stderr-only, so
this file IS the bridge's log - there is no separate append-mode
file logger like the bus has. launchd TRUNCATES these on every
process start, so a crash loop overwrites the evidence of earlier
iterations; ThrottleInterval above is what keeps the surviving
window useful. Giving the bridge its own append-mode file handler
is the follow-up that removes this caveat. -->
<key>StandardOutPath</key>
<string>__BRIDGE_LOG_FILE__</string>

<key>StandardErrorPath</key>
<string>__BRIDGE_ERR_FILE__</string>

<key>ProcessType</key>
<string>Background</string>
</dict>
</plist>
4 changes: 2 additions & 2 deletions scripts/dev.sh
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ SERVICE_WAS_RUNNING=false

# Stop service if running (to free port 8080)
if [[ "$(uname)" == "Darwin" ]]; then
LABEL="com.evansenter.agent-event-bus"
LABEL="com.evansenter.agent-event-bus" # matched anchored: the bridge label is a superstring
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
if launchctl list 2>/dev/null | grep -q "$LABEL"; then
if launchctl list 2>/dev/null | grep -q "$LABEL$"; then
echo "Stopping LaunchAgent for dev mode..."
launchctl unload "$PLIST" 2>/dev/null
SERVICE_WAS_RUNNING=true
Expand Down
122 changes: 122 additions & 0 deletions scripts/install-bridge-launchagent.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#!/bin/bash
# Install the RFC #122 bridge as a macOS LaunchAgent (auto-starts on login,
# restarts on crash). The bus has its own unit - this supervises only the
# webhook->injection bridge, which until now had no install target at all.

set -e

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
VENV_PYTHON="$PROJECT_DIR/.venv/bin/python"
PLIST_TEMPLATE="$SCRIPT_DIR/com.evansenter.agent-event-bus-bridge.plist"
PLIST_DEST="$HOME/Library/LaunchAgents/com.evansenter.agent-event-bus-bridge.plist"
LABEL="com.evansenter.agent-event-bus-bridge"

DATA_DIR="$HOME/.claude/contrib/agent-event-bus"
BRIDGE_LOG_FILE="${AGENT_EVENT_BUS_BRIDGE_LOG:-$DATA_DIR/agent-event-bus-bridge.log}"
BRIDGE_ERR_FILE="${AGENT_EVENT_BUS_BRIDGE_ERR:-$DATA_DIR/agent-event-bus-bridge.err}"

if [[ ! -f "$VENV_PYTHON" ]]; then
echo "Error: Virtual environment not found at $PROJECT_DIR/.venv"
echo "Run: make install-server (or: uv sync)"
exit 1
fi

# Preflight the import rather than running `uv sync` here. A stale venv missing
# a bridge dependency would otherwise become an import crash-loop under
# KeepAlive - the one case where the log-truncation caveat bites hardest, since
# every respawn wipes the previous traceback. Checking beats syncing: `uv sync
# --no-dev` (what install-server runs) would silently strip pytest/ruff from a
# venv someone just set up with `make dev`.
if ! PYTHONPATH="$PROJECT_DIR/src" "$VENV_PYTHON" -c "import agent_event_bus.bridge" 2>/tmp/bridge-import-check.$$; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Suggestion] The preflight imports agent_event_bus.bridge, but uvicorn is imported lazily inside main() (src/agent_event_bus/bridge.py:2129), not at module scope — so the one dependency whose absence would produce exactly the KeepAlive import-crash-loop this check exists to prevent is the one it cannot see. starlette / anyio / requests are module-level and are covered.

In practice uvicorn>=0.30.0 is a direct entry in [project.dependencies], so a venv that can import starlette almost certainly has it — which is why this is a Suggestion rather than a defect. Widening the check costs nothing: add , uvicorn to the -c import list.

echo "Error: the venv cannot import agent_event_bus.bridge:"
sed 's/^/ /' /tmp/bridge-import-check.$$

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Suggestion] /tmp/bridge-import-check.$$ is a predictable path in a world-writable sticky directory, and the 2> redirect follows symlinks — a pre-planted /tmp/bridge-import-check.<pid> symlink would be truncated as the installing user. The file also leaks if the script is interrupted between creation and the rm -f.

mktemp plus a trap ... EXIT handles both, and lines up with the $TMPDIR-aware convention CLAUDE.md already documents for the bridge hook-lock dir.

rm -f /tmp/bridge-import-check.$$
echo "Run: make dev (or: uv sync)"
exit 1
fi
rm -f /tmp/bridge-import-check.$$

# The bridge is useless without a bus to register against. It would retry with
# backoff rather than die (register_with_retry), so this is a warning and not a
# hard failure - but starting a bridge on a box with no bus is almost always a
# mistake worth naming at install time rather than discovering in the log.
if ! launchctl list | grep -q "com.evansenter.agent-event-bus$"; then

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Important] This check is correctly anchored with $ — but the three pre-existing probes for the bus label are not, and the new label is a superstring of the old one:

  • Makefile:149 (make restart) — launchctl list | grep -q "com.evansenter.agent-event-bus"
  • scripts/install-launchagent.sh:29,48grep -q "$LABEL", LABEL="com.evansenter.agent-event-bus"
  • scripts/dev.sh:15 — same unanchored $LABEL

launchctl list prints the label at end-of-line, so a loaded com.evansenter.agent-event-bus-bridge row matches all of them.

Fails when: the bridge LaunchAgent is loaded and the buss launchctl loaddoes not leave a job registered (invalid plist after a badsedsubstitution,Load failed: 5, or the bus plist absent) → make restartandinstall-launchagent.sh:48match the *bridge* row, printService restarted successfully/Agent Event Bus installed and running!, fire the success notification, and exit 0 while the bus is down — the exact failure those checks exist to catch. Same root cause in dev.sh: on a bridge-only box it prints Stopping LaunchAgent for dev mode..., sets SERVICE_WAS_RUNNING=true, and on exit launchctl load`s a bus that was never running (or errors if the plist does not exist).

Fix is the $ you already used here, applied to those three sites.

echo "Warning: the bus LaunchAgent does not appear to be loaded."
echo " The bridge will start and retry registration with backoff,"
echo " but it cannot deliver anything until the bus is up."
echo " Install it with: make install-server"
echo ""
fi

mkdir -p "$HOME/Library/LaunchAgents"
mkdir -p "$DATA_DIR"

if launchctl list | grep -q "$LABEL$"; then
echo "Stopping existing bridge service..."
launchctl unload "$PLIST_DEST" 2>/dev/null || true
fi

echo "Installing bridge LaunchAgent..."
sed -e "s|__VENV_PYTHON__|$VENV_PYTHON|g" \
-e "s|__PROJECT_DIR__|$PROJECT_DIR|g" \
-e "s|__BRIDGE_LOG_FILE__|$BRIDGE_LOG_FILE|g" \
-e "s|__BRIDGE_ERR_FILE__|$BRIDGE_ERR_FILE|g" \
"$PLIST_TEMPLATE" > "$PLIST_DEST"

echo "Starting bridge..."
launchctl load "$PLIST_DEST"

# POLL rather than sleep a fixed interval. On a re-install over a live bridge,
# `launchctl unload` returns as soon as SIGTERM is delivered, so the outgoing
# process can still hold the singleton flock and port 8082 when the replacement
# starts. The replacement then exits on the lock - correctly; that ordering is
# what keeps the outgoing instance's unregister from racing a new registration
# - and KeepAlive only retries after ThrottleInterval (~10s). A flat 2s probe
# lands inside that window and reports a perfectly healthy idempotent
# re-install as a failure. 20s covers the throttle with room to spare.
HEALTH=""
for _ in $(seq 1 40); do
HEALTH="$(curl -fsS --max-time 2 http://127.0.0.1:8082/health 2>/dev/null || true)"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Suggestion] The poll breaks on the first successful /health, which on a re-install over a live bridge is answered by the outgoing process.

launchctl unload returns once SIGTERM is delivered; the old bridge still holds port 8082 through the shielded stop-join-unregister in its lifespan. So iteration 1 gets a 200 from the instance that is about to die, HEALTH is set, the loop breaks, the launchctl list check below (which reports only whether the job is loaded, not whether a process is alive) passes, and the script prints "Bridge installed and running" plus the outgoing registered: value — while the replacement has in fact just exited on the singleton flock and will not exist for another ~ThrottleInterval.

End state is still correct (KeepAlive brings it back), so this is reporting accuracy rather than breakage. But the comment above the loop presents the poll as covering the handoff window, and breaking on the first response cannot tell the two instances apart. Telling them apart needs something instance-specific in /health (a start timestamp, or the pid), or a short unconditional wait past ThrottleInterval before the first probe.

[[ -n "$HEALTH" ]] && break
sleep 0.5
done

if ! launchctl list | grep -q "$LABEL$"; then
echo "Error: bridge failed to start. Check $BRIDGE_ERR_FILE"
exit 1
fi

echo ""
echo "Bridge installed and running."
# Labelled by what each file actually receives: bridge.py logs via
# logging.basicConfig with no stream=, which defaults to STDERR, so every
# bridge record lands in the .err file. The .log file gets uvicorn's access
# lines only. Following a "Logs:" pointer at the .log would show a reader no
# bridge messages at all.
echo " Bridge log (records, warnings, errors): $BRIDGE_ERR_FILE"
echo " Access log (uvicorn requests): $BRIDGE_LOG_FILE"
echo " Health: curl -s http://127.0.0.1:8082/health"
echo ""

# registered:false is not a failure here - it means registration is still
# backing off (bus not up yet), which resolves on its own. Report what we see
# rather than asserting success we have not confirmed.
if [[ -n "$HEALTH" ]]; then
echo " /health -> $HEALTH"
case "$HEALTH" in
*'"registered":true'*|*'"registered": true'*)
echo " Registered on the bus." ;;
*)
echo " Not registered yet - retrying with backoff (is the bus up?)." ;;
esac
else
echo " /health did not answer within 20s; check $BRIDGE_ERR_FILE."
fi

echo ""
echo "NOTE: nothing drains wake/<session>.jsonl yet (agent-event-bus#134)."
echo " The bridge will spool actionable DMs durably, but no session is"
echo " woken by them until a drain hook exists."
echo ""
echo "To uninstall: $SCRIPT_DIR/uninstall-bridge-launchagent.sh"
8 changes: 6 additions & 2 deletions scripts/install-launchagent.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
VENV_PYTHON="$PROJECT_DIR/.venv/bin/python"
PLIST_TEMPLATE="$SCRIPT_DIR/com.evansenter.agent-event-bus.plist"
PLIST_DEST="$HOME/Library/LaunchAgents/com.evansenter.agent-event-bus.plist"
# Anchored at end-of-line everywhere it is matched: launchctl list prints the
# label last, and com.evansenter.agent-event-bus-bridge is a SUPERSTRING of
# this one, so an unanchored probe reports the bus healthy whenever only the
# bridge is loaded.
LABEL="com.evansenter.agent-event-bus"

# Resolve paths (respect env var overrides, fall back to canonical defaults)
Expand All @@ -26,7 +30,7 @@ mkdir -p "$HOME/Library/LaunchAgents"
mkdir -p "$HOME/.claude"

# Stop existing service if running
if launchctl list | grep -q "$LABEL"; then
if launchctl list | grep -q "$LABEL$"; then
echo "Stopping existing service..."
launchctl unload "$PLIST_DEST" 2>/dev/null || true
fi
Expand All @@ -46,7 +50,7 @@ launchctl load "$PLIST_DEST"

# Verify it's running
sleep 1
if launchctl list | grep -q "$LABEL"; then
if launchctl list | grep -q "$LABEL$"; then
echo ""
echo "Agent Event Bus installed and running!"
echo " Logs: $LOG_FILE"
Expand Down
32 changes: 32 additions & 0 deletions scripts/uninstall-bridge-launchagent.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/bin/bash
# Remove the bridge LaunchAgent. Leaves the bus, the database, and the wake
# directory alone - this only stops supervising the bridge.

set -e

PLIST_DEST="$HOME/Library/LaunchAgents/com.evansenter.agent-event-bus-bridge.plist"
LABEL="com.evansenter.agent-event-bus-bridge"

if launchctl list | grep -q "$LABEL$"; then
echo "Stopping bridge..."
# SIGTERM via unload, so the shutdown path runs: the lifespan's shielded
# stop-join-unregister removes the webhook row rather than leaving the bus
# POSTing at a dead port.
launchctl unload "$PLIST_DEST" 2>/dev/null || true
else
echo "Bridge service not loaded."
fi

if [[ -f "$PLIST_DEST" ]]; then
rm "$PLIST_DEST"
echo "Removed $PLIST_DEST"
fi

echo ""
echo "Bridge uninstalled. The bus, the database, and wake/ are untouched."
echo "Confirm the webhook row is gone with: agent-event-bus-cli webhook list"
echo ""
echo "The wake directory is transient and safe to clear BY HAND once no"
echo "bridge is running (clearing it under a live bridge orphans its"
echo "singleton lock inode):"
echo " ~/.claude/contrib/agent-event-bus/wake/"
13 changes: 6 additions & 7 deletions src/agent_event_bus/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,13 +240,12 @@ def _session_id_from_env() -> str | None:
tends not to be.

The fallback exists because setting the explicit var from a shell profile
is not reliable: the dotfiles that map one to the other live in ~/.exports,
which is sourced from ~/.zshrc - and zsh reads .zshrc for INTERACTIVE
shells only. Tool-spawned subprocesses are non-interactive, so the mapping
never runs there and publishes landed as "anonymous" (`zsh -i -c` sees it,
`zsh -c` does not). That is a property of shell startup, not of any OS -
it reproduces on Linux - so the fix belongs here, where it holds for every
shell, spawner, and machine, rather than in one shell's rc plumbing.
cannot be relied on: rc files are read for INTERACTIVE shells only, and a
tool-spawned subprocess is not interactive, so any profile-based mapping
is simply absent there and publishes land as "anonymous" (`zsh -i -c`
sees such a mapping, `zsh -c` does not). That is shell startup semantics
rather than a property of any OS or any particular dotfile layout, so the
fix belongs here, where it holds for every shell, spawner, and machine.

The two ids are the same value by construction: the SessionStart hook
registers on the bus with client_id = the Claude Code session id, which the
Expand Down
Loading
Loading