Set up a persistent AI agent gateway on macOS with Redis event bridge, heartbeat monitoring, and multi-session routing. Interactive Q&A to match your intent — from minimal (Redis + extension) to full (embedded daemon + Telegram + watchdog). Use when: 'set up a gateway', 'I want my agent always on', 'event bridge', 'heartbeat monitoring', 'agent notifications', or any request to make an AI agent persistent and reachable.
Set up a persistent AI agent gateway on macOS with Redis event bridge, heartbeat monitoring, and multi-session routing. Interactive Q&A to match your intent — from minimal (Redis + extension) to full (embedded daemon + Telegram + watchdog). Use when: 'set up a gateway', 'I want my agent always on', 'event bridge', 'heartbeat monitoring', 'agent notifications', or any request to make an AI agent persistent and reachable.
version
1.0.0
author
Joel Hooks
tags
["joelclaw","gateway","setup","redis","telegram"]
disable-model-invocation
true
Gateway Setup for AI Agents on macOS
This skill builds a persistent gateway for an AI coding agent on a Mac. It bridges background workflows (Inngest, cron, pipelines) into your agent's session and optionally routes responses to external channels (Telegram, WebSocket).
Before You Start
Required:
macOS (Apple Silicon preferred)
pi coding agent installed and working
Redis running (locally, Docker, or k8s — see inngest-local skill if you need this)
Tailscale (for secure remote access from phone/laptop)
Telegram bot token (for mobile notifications/chat)
Critical Setup Notes
GATEWAY_ROLE=central is required for the always-on session. Without it, the session runs as a satellite and misses heartbeats, system alerts, and any events not targeted at it specifically. Set it when launching:
GATEWAY_ROLE=central pi
serveHost is mandatory when Inngest runs in Docker and the worker runs on the host. The SDK advertises localhost:3100 as its callback URL, but Docker can't reach the host's loopback. Set it in your Hono serve handler:
Then force re-sync: curl -X PUT http://localhost:3100/api/inngest
ioredis resolution in Bun is flaky. If you get Cannot find module '@ioredis/commands', install it explicitly:
bun add @ioredis/commands
# or: rm -rf node_modules && bun install
Two ioredis clients required for pub/sub. A subscribed client can't run LRANGE, DEL, or other commands. The extension creates separate sub and cmd clients.
Intent Alignment
Before building anything, ask the user these questions to determine scope. Adapt based on their answers.
Question 1: What's your goal?
Present these options:
Notifications only — background jobs finish, I want to know about it without watching the terminal
Always-on agent — I want a persistent session that survives terminal closes, handles heartbeats, routes events
Full gateway — always-on + talk to my agent from Telegram/phone + multi-session routing
Each level builds on the previous. Start with what they need now.
Question 2: What's your event source?
Just cron/timers — I want a heartbeat that checks system health periodically
Inngest functions — I have durable workflows that emit completion events
Mixed — Inngest + cron + maybe webhooks
Question 3: How many concurrent agent sessions?
One — I run one pi session at a time
Multiple — I often have 2-5 sessions in different terminals working on different things
If multiple: enable central/satellite routing. If one: simpler single-session mode.
Architecture Tiers
Tier 1: Notification Bridge (simplest)
What you get: Background events show up in your pi session as messages.
Components:
Redis (already running)
Gateway pi extension (~100 lines)
pushGatewayEvent() utility function
How it works:
Background process → Redis LPUSH → pi extension drains on notify → injected as user message
When you need this: You run 2+ pi sessions simultaneously — one for oversight, others for coding tasks. Heartbeats should only go to the oversight session.
GATEWAY_ROLE=central pi # This one gets ALL events
Other sessions start normally (satellites):
pi # Gets only events it initiated
Tier 4: External Channels (Telegram, WebSocket)
Adds: Talk to your agent from your phone.
This tier requires the embedded daemon approach — pi runs as a library inside a Node.js process, not as a TUI. See the joelclaw.com article "Building a Gateway for Your AI Agent" for the full architecture.
Key components:
createAgentSession() from pi SDK — headless agent session
grammY for Telegram bot
Command queue that serializes all inputs (TUI, heartbeat, Telegram)
Outbound router that sends responses back to the asking channel
This is the most complex tier. Only build it if you actually need mobile access.
Verification Checklist
After setup, verify:
Pi session starts and extension loads (check status bar for 🔗)
Push a test event: redis-cli LPUSH agent:events:main '{"id":"test","type":"test","source":"manual","payload":{},"ts":0}' + redis-cli PUBLISH agent:notify:main test
Event appears in pi session within seconds
(Tier 2+) Heartbeat fires on schedule
(Tier 2+) Kill the heartbeat source — watchdog alarm fires after 30 min
(Tier 3+) Central session receives heartbeats, satellite sessions don't
Embeds pi as library. grammY for Telegram. Command queue serializes all inputs. Most complex tier.
Read order for full context: 0010 → 0018 → 0035 → 0037 → 0038 (skip 0036, superseded)
Known Limitations
Drain race condition. The extension does LRANGE then DEL — not atomic. Events pushed between those calls are deleted without processing. The in-memory seenIds dedup prevents double-delivery but doesn't prevent lost events. Fix: use LRANGE + LTRIM or a Redis transaction. Low-impact on single-user systems but real.
Redis connection recovery is notify-only. If Redis goes down, the extension catches the error and logs it, but doesn't retry or reconnect automatically. ioredis retryStrategy handles reconnection at the client level, but accumulated events during the outage may be lost.
Watchdog intervals are hardcoded. Check interval (5 min) and threshold (30 min) are constants in the extension. Should be configurable via env vars or Redis config.
No persistent dedup across restarts. The seenIds Set lives in memory and caps at 500. Process restart = dedup resets. For the heartbeat-every-15-min use case this is fine. For high-frequency events it could cause duplicates.