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
11 changes: 11 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,17 @@ git commits → PRs → CI feedback → thread updates back to Slack

**Deployment model:** Admin-managed source (this repo) is deployed as immutable, git-free release snapshots under `/opt/baudbot`. The agent runtime (`/home/baudbot_agent`) receives deployed extensions, skills, and bridge code. Updates and rollbacks are atomic symlink switches. See `docs/architecture.md` for full details.

## Startup boundary: `start.sh` vs agent

There are two startup phases with distinct ownership:

| Phase | Owner | Scope |
|-------|-------|-------|
| **OS boot** (`start.sh`) | Admin | Env validation, permissions, secrets, socket cleanup, launch `pi` |
| **Agent boot** (`startup-pi.sh`) | Agent | Slack bridge, sentry-agent, dev-agents, session wiring |

**Rule: `start.sh` must never spawn tmux sessions or background processes that need pi runtime state** (session UUIDs, socket paths, etc.). Those only exist after pi starts. All tmux sessions (bridge, sentry-agent, dev-agents) are owned and managed by the agent via `startup-pi.sh` or extensions. `start.sh` may only *kill* stale processes as pre-cleanup.

## What this repo contains (high-level)

- `bin/` — operational shell CLI, deploy/update/rollback, security and health scripts
Expand Down
13 changes: 8 additions & 5 deletions bin/ci/smoke-agent-runtime.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# - baudbot starts successfully
# - control-agent session socket is created and reachable
# - session-control RPC responds successfully
# - bridge supervisor status artifact exists
# - bridge supervisor status artifact exists (if bridge was started by start.sh)
# - process remains healthy for a short stabilization window
# - baudbot stops cleanly

Expand Down Expand Up @@ -139,11 +139,14 @@ main() {
log "probing session-control RPC"
probe_rpc_get_message "$socket_path"

# Bridge is now started by startup-pi.sh (inside the agent), not by
# start.sh. In CI the agent doesn't run long enough for startup-pi.sh
# to execute, so the status file may not exist. Log but don't fail.
log "checking bridge supervisor status file"
if [[ ! -f "$BRIDGE_STATUS_FILE" ]]; then
log "missing bridge supervisor status file: ${BRIDGE_STATUS_FILE}"
sudo baudbot status || true
exit 1
if [[ -f "$BRIDGE_STATUS_FILE" ]]; then
log "bridge supervisor status file exists"
else
log "bridge supervisor status file not found (expected — bridge starts inside agent)"
fi

log "stabilization window (${STABILIZE_SECONDS}s)"
Expand Down
10 changes: 5 additions & 5 deletions pi/skills/control-agent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -292,7 +292,7 @@ Use the Thread value as `thread_ts` when calling `/send` to reply in the same th

Run `list_sessions` to get live UUIDs, then run:
```bash
bash ~/.pi/agent/skills/control-agent/startup-cleanup.sh UUID1 UUID2 UUID3
bash ~/.pi/agent/skills/control-agent/startup-pi.sh UUID1 UUID2 UUID3
```

This removes stale `.sock` files, cleans dead aliases, and restarts the Slack bridge.
Expand All @@ -302,7 +302,7 @@ This removes stale `.sock` files, cleans dead aliases, and restarts the Slack br
### Checklist

- [ ] Run `list_sessions` — note live UUIDs, confirm `control-agent` is listed
- [ ] Run `startup-cleanup.sh` with live UUIDs (cleans sockets + restarts Slack bridge)
- [ ] Run `startup-pi.sh` with live UUIDs (cleans sockets + restarts Slack bridge)
- [ ] **Read memory files** — `ls ~/.pi/agent/memory/` then read each `.md` file to restore context from previous sessions
- [ ] If `BAUDBOT_EXPERIMENTAL=1`: verify `BAUDBOT_SECRET`, create/verify `BAUDBOT_EMAIL` inbox, and start email monitor (inline mode, **300s / 5 min**)
- [ ] Verify heartbeat is active (`heartbeat status` — should show enabled)
Expand Down Expand Up @@ -339,11 +339,11 @@ The sentry-agent operates in **on-demand mode** — it does NOT poll. Sentry ale

### Starting the Slack Bridge

The `startup-cleanup.sh` script handles bridge (re)start automatically — it detects broker vs Socket Mode, reads the control-agent UUID, and starts the bridge as a normal background process.
The `startup-pi.sh` script handles bridge (re)start automatically — it detects broker vs Socket Mode, reads the control-agent UUID, and starts the bridge as a normal background process.

If you need to restart the bridge manually, rerun startup cleanup and then inspect logs:
```bash
bash ~/.pi/agent/skills/control-agent/startup-cleanup.sh UUID1 UUID2 UUID3
bash ~/.pi/agent/skills/control-agent/startup-pi.sh UUID1 UUID2 UUID3
tail -n 200 ~/.pi/agent/logs/slack-bridge.log
cat ~/.pi/agent/slack-bridge-supervisor.json
```
Expand All @@ -363,7 +363,7 @@ If you need to check manually, use `heartbeat trigger` to run all checks immedia
When the heartbeat reports a failure, take the appropriate action:
1. **Missing sentry-agent**: Respawn with tmux and re-send role assignment.
2. **Orphaned dev-agents**: Kill tmux session and remove worktree.
3. **Bridge down**: Restart via `startup-cleanup.sh`, then check `~/.pi/agent/logs/slack-bridge.log`.
3. **Bridge down**: Restart via `startup-pi.sh`, then check `~/.pi/agent/logs/slack-bridge.log`.
4. **Stale worktrees**: `git worktree remove --force` + `rmdir` empty parents.
5. **Stuck todos**: Escalate to user via Slack.

Expand Down
2 changes: 1 addition & 1 deletion pi/skills/control-agent/memory/operational.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ Add entries under dated headings. Keep entries concise — one line per learning

<!-- Example:
## 2026-02-17
- Stale `.sock` files cause bridge "connect ENOENT" errors. Always run `startup-cleanup.sh` with live UUIDs on boot.
- Stale `.sock` files cause bridge "connect ENOENT" errors. Always run `startup-pi.sh` with live UUIDs on boot.
- `varlock run` must be used (not `source .env`) when launching agents in tmux — ensures schema validation.
-->
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
#!/usr/bin/env bash
# startup-cleanup.sh — Clean stale sockets and restart the Slack bridge.
# Run this at the start of every control-agent session.
# startup-pi.sh — Agent-side startup: clean stale sockets + start Slack bridge.
#
# Usage: bash ~/.pi/agent/skills/control-agent/startup-cleanup.sh <live-session-ids...>
# Called automatically by the control-agent on every session start (Step 0 in
# SKILL.md). start.sh launches pi, pi loads the control-agent skill, and the
# agent's first action is to run this script.
#
# Usage: bash ~/.pi/agent/skills/control-agent/startup-pi.sh <live-session-ids...>
#
# Pass the live session UUIDs (from list_sessions) as arguments.
# Any .sock file whose UUID is NOT in the live set gets removed.
# Stale .alias symlinks pointing to removed sockets also get cleaned.
# Then restarts the slack-bridge process with the current control-agent UUID.
# Then starts the slack-bridge process with the current control-agent UUID.
#
# This script is the SOLE owner of the bridge lifecycle. start.sh only does
# pre-cleanup (kill stale processes, release port) — it never launches the bridge.

set -euo pipefail

Expand Down Expand Up @@ -143,9 +149,15 @@ if [ -z "$BRIDGE_SCRIPT" ]; then
exit 0
fi

# --- Launch bridge in a tmux session with restart loop ---
# The tmux session stays alive independently of this script (same pattern as
# sentry-agent). If the bridge crashes, the loop restarts it after 5 seconds.
# --- Launch bridge in a tmux session with supervised restart loop ---
# The restart loop:
# - Re-reads .env on every restart (picks up config changes)
# - Unsets SLACK_BROKER_* before sourcing (avoids stale parent env)
# - Tracks consecutive fast failures (<60s runtime) and gives up after 10
# - Backs off: 5s base + 2s per failure, capped at 60s
# - Kills port holders before retrying (avoids EADDRINUSE spin)
MAX_CONSECUTIVE_FAILURES=10

echo "Starting slack-bridge ($BRIDGE_SCRIPT) via tmux..."
NODE_BIN_DIR="${NODE_BIN_DIR:-$HOME/opt/node/bin}"
if command -v bb_resolve_runtime_node_bin_dir >/dev/null 2>&1; then
Expand All @@ -161,20 +173,35 @@ tmux new-session -d -s "$BRIDGE_TMUX_SESSION" "\
export PATH=$NODE_BIN_DIR:\$PATH; \
export PI_SESSION_ID=$MY_UUID; \
cd $BRIDGE_DIR; \
consecutive_failures=0; \
while true; do \
echo \"[\$(date -Is)] bridge: starting $BRIDGE_SCRIPT\" >> $BRIDGE_LOG_FILE; \
echo \"[\$(date -Is)] bridge: starting $BRIDGE_SCRIPT (attempt \$((consecutive_failures + 1)))\" >> $BRIDGE_LOG_FILE; \
start_time=\$(date +%s); \
for v in \$(env | grep ^SLACK_BROKER_ | cut -d= -f1 || true); do unset \$v; done; \
set -a; source \$HOME/.config/.env; set +a; \
node $BRIDGE_SCRIPT >> $BRIDGE_LOG_FILE 2>&1; \
exit_code=\$?; \
echo \"[\$(date -Is)] bridge: exited with code \$exit_code, restarting in 5s\" >> $BRIDGE_LOG_FILE; \
sleep 5; \
tries=0; \
while lsof -ti :7890 >/dev/null 2>&1 && [ \$tries -lt 10 ]; do \
echo \"[\$(date -Is)] bridge: port 7890 still in use, waiting...\" >> $BRIDGE_LOG_FILE; \
sleep 2; \
tries=\$((tries + 1)); \
done; \
runtime=\$(( \$(date +%s) - start_time )); \
echo \"[\$(date -Is)] bridge: exited with code \$exit_code after \${runtime}s\" >> $BRIDGE_LOG_FILE; \
if [ \$runtime -ge 60 ]; then \
consecutive_failures=0; \
else \
consecutive_failures=\$((consecutive_failures + 1)); \
fi; \
if [ \$consecutive_failures -ge $MAX_CONSECUTIVE_FAILURES ]; then \
echo \"[\$(date -Is)] bridge: FATAL — \$consecutive_failures consecutive fast failures, giving up\" >> $BRIDGE_LOG_FILE; \
break; \
fi; \
delay=\$((5 + consecutive_failures * 2)); \
[ \$delay -gt 60 ] && delay=60; \
echo \"[\$(date -Is)] bridge: restarting in \${delay}s (failures: \$consecutive_failures/$MAX_CONSECUTIVE_FAILURES)\" >> $BRIDGE_LOG_FILE; \
sleep \$delay; \
port_pids=\$(lsof -ti :7890 2>/dev/null || true); \
if [ -n \"\$port_pids\" ]; then \
echo \"[\$(date -Is)] bridge: port 7890 still held, killing: \$port_pids\" >> $BRIDGE_LOG_FILE; \
echo \"\$port_pids\" | xargs kill -9 2>/dev/null || true; \
sleep 1; \
fi; \
done"

echo "Bridge tmux session: $BRIDGE_TMUX_SESSION"
Expand Down
78 changes: 30 additions & 48 deletions start.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
# shellcheck source=bin/lib/runtime-node.sh
source "$SCRIPT_DIR/bin/lib/runtime-node.sh"
# shellcheck source=bin/lib/bridge-restart-policy.sh
source "$SCRIPT_DIR/bin/lib/bridge-restart-policy.sh"
# bridge-restart-policy.sh no longer needed — bridge is started by
# startup-pi.sh, not start.sh (see PR #164)
cd ~

NODE_BIN_DIR="$(bb_resolve_runtime_node_bin_dir "$HOME")"
Expand Down Expand Up @@ -84,53 +84,35 @@ if [ -d "$SOCKET_DIR" ]; then
done
fi

# Start Slack bridge in the background (before pi, so it's ready for messages).
# Broker pull mode has priority when SLACK_BROKER_* keys are configured.
# Otherwise fallback to direct Slack Socket Mode.
BRIDGE_SCRIPT=""
if [ -n "${SLACK_BROKER_URL:-}" ] \
&& [ -n "${SLACK_BROKER_WORKSPACE_ID:-}" ] \
&& [ -n "${SLACK_BROKER_SERVER_PRIVATE_KEY:-}" ] \
&& [ -n "${SLACK_BROKER_SERVER_PUBLIC_KEY:-}" ] \
&& [ -n "${SLACK_BROKER_SERVER_SIGNING_PRIVATE_KEY:-}" ] \
&& [ -n "${SLACK_BROKER_PUBLIC_KEY:-}" ] \
&& [ -n "${SLACK_BROKER_SIGNING_PUBLIC_KEY:-}" ]; then
BRIDGE_SCRIPT="broker-bridge.mjs"
elif [ -n "${SLACK_BOT_TOKEN:-}" ] && [ -n "${SLACK_APP_TOKEN:-}" ]; then
BRIDGE_SCRIPT="bridge.mjs"
fi

if [ -n "$BRIDGE_SCRIPT" ]; then
RELEASE_BRIDGE="/opt/baudbot/current/slack-bridge"
BRIDGE_LOG_DIR="$HOME/.pi/agent/logs"
BRIDGE_LOG_FILE="$BRIDGE_LOG_DIR/slack-bridge.log"
BRIDGE_STATUS_FILE="$HOME/.pi/agent/slack-bridge-supervisor.json"
BRIDGE_PID_FILE="$HOME/.pi/agent/slack-bridge.pid"

mkdir -p "$BRIDGE_LOG_DIR"

# Stop any previous bridge process tracked by pid file.
if [ -f "$BRIDGE_PID_FILE" ]; then
old_pid="$(cat "$BRIDGE_PID_FILE" 2>/dev/null || true)"
if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then
kill "$old_pid" 2>/dev/null || true
sleep 1
kill -9 "$old_pid" 2>/dev/null || true
fi
rm -f "$BRIDGE_PID_FILE"
# ── Slack bridge cleanup (bridge is started by startup-pi.sh) ──
# The bridge needs the control-agent's session UUID (PI_SESSION_ID) to deliver
# messages to the correct socket. That UUID isn't known until pi starts and
# registers its socket. So we DON'T start the bridge here — the control-agent's
# startup-pi.sh handles it after the session is live.
#
# We DO kill any stale bridge processes from previous runs to avoid port
# conflicts when startup-pi.sh launches a fresh one.
BRIDGE_PID_FILE="$HOME/.pi/agent/slack-bridge.pid"
if [ -f "$BRIDGE_PID_FILE" ]; then
old_pid="$(cat "$BRIDGE_PID_FILE" 2>/dev/null || true)"
if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then
echo "Stopping stale bridge supervisor (PID $old_pid)..."
kill "$old_pid" 2>/dev/null || true
sleep 1
kill -9 "$old_pid" 2>/dev/null || true
fi

echo "Starting Slack bridge ($BRIDGE_SCRIPT)... logs: $BRIDGE_LOG_FILE"
(
export PATH="$HOME/.varlock/bin:$NODE_BIN_DIR:$PATH"
cd "$RELEASE_BRIDGE"
bb_bridge_supervise "$BRIDGE_LOG_FILE" "$BRIDGE_STATUS_FILE" "$BRIDGE_SCRIPT" \
varlock run --path ~/.config/ -- node "$BRIDGE_SCRIPT"
) &
# Intentionally track the supervisor subshell PID (not per-restart node child PID)
# so a single kill stops the entire bridge restart loop.
echo $! > "$BRIDGE_PID_FILE"
chmod 600 "$BRIDGE_PID_FILE"
rm -f "$BRIDGE_PID_FILE"
fi
# Kill the tmux session too (startup-pi.sh uses this)
tmux kill-session -t slack-bridge 2>/dev/null || true
# Force-release port 7890 in case anything survived
PORT_PIDS="$(lsof -ti :7890 2>/dev/null || true)"
if [ -n "$PORT_PIDS" ]; then
echo "Releasing port 7890 (PIDs: $PORT_PIDS)..."
echo "$PORT_PIDS" | xargs kill 2>/dev/null || true
sleep 1
PORT_PIDS="$(lsof -ti :7890 2>/dev/null || true)"
[ -n "$PORT_PIDS" ] && echo "$PORT_PIDS" | xargs kill -9 2>/dev/null || true
fi

# Set session name (read by auto-name.ts extension)
Comment thread
sentry[bot] marked this conversation as resolved.
Expand Down