Claude
Skills
Sign in
Back

gateway-setup

Included with Lifetime
$97 forever

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.

Backend & APIsjoelclawgatewaysetupredistelegramassets

What this skill does


# 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)

**Optional (unlocks more features):**
- Inngest self-hosted (for durable event-driven workflows)
- 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:
```bash
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:
```typescript
inngestServe({
  client: inngest,
  functions,
  serveHost: "http://host.docker.internal:3100",
})
```
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:
```bash
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:
1. **Notifications only** — background jobs finish, I want to know about it without watching the terminal
2. **Always-on agent** — I want a persistent session that survives terminal closes, handles heartbeats, routes events
3. **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?

1. **Just cron/timers** — I want a heartbeat that checks system health periodically
2. **Inngest functions** — I have durable workflows that emit completion events
3. **Mixed** — Inngest + cron + maybe webhooks

### Question 3: How many concurrent agent sessions?

1. **One** — I run one pi session at a time
2. **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
```

**Build steps:**

1. Create the extension directory:
```bash
mkdir -p ~/.pi/agent/extensions/gateway
```

2. Create `~/.pi/agent/extensions/gateway/package.json`:
```json
{
  "name": "gateway-extension",
  "private": true,
  "dependencies": {
    "ioredis": "^5.4.2"
  }
}
```

3. Install dependencies:
```bash
cd ~/.pi/agent/extensions/gateway && npm install
```

4. Create `~/.pi/agent/extensions/gateway/index.ts` with the minimal bridge:

```typescript
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";

const SESSION_ID = "main";
const EVENT_LIST = `agent:events:${SESSION_ID}`;
const NOTIFY_CHANNEL = `agent:notify:${SESSION_ID}`;

type RedisLike = {
  on(event: string, listener: (...args: unknown[]) => void): void;
  connect(): Promise<void>;
  subscribe(channel: string): Promise<unknown>;
  lrange(key: string, start: number, stop: number): Promise<string[]>;
  del(key: string): Promise<number>;
  llen(key: string): Promise<number>;
  unsubscribe(): void;
  disconnect(): void;
};

type RedisCtor = new (options: { host: string; port: number; lazyConnect: boolean }) => RedisLike;

let Redis: RedisCtor | null = null;
let sub: RedisLike | null = null;
let cmd: RedisLike | null = null;
let ctx: ExtensionContext | null = null;
let piRef: ExtensionAPI | null = null;

interface SystemEvent {
  id: string;
  type: string;
  source: string;
  payload: Record<string, unknown>;
  ts: number;
}

function formatEvents(events: SystemEvent[]): string {
  return events.map((e) => {
    const time = new Date(e.ts).toLocaleTimeString("en-US", { hour12: false });
    return `- **[${time}] ${e.type}** (${e.source})`;
  }).join("\n");
}

async function drain(): Promise<void> {
  if (!cmd || !piRef) return;
  const raw = await cmd.lrange(EVENT_LIST, 0, -1);
  if (raw.length === 0) return;

  const events = raw.reverse().map(r => {
    try { return JSON.parse(r) as SystemEvent; } catch { return null; }
  }).filter(Boolean) as SystemEvent[];

  if (events.length === 0) { await cmd.del(EVENT_LIST); return; }

  const prompt = [
    `## 🔔 ${events.length} event(s) — ${new Date().toISOString()}`,
    "", formatEvents(events), "",
    "Take action if needed, otherwise acknowledge briefly.",
  ].join("\n");

  if (ctx?.isIdle()) {
    piRef.sendUserMessage(prompt);
  } else {
    piRef.sendUserMessage(prompt, { deliverAs: "followUp" });
  }
  await cmd.del(EVENT_LIST);
}

export default function (pi: ExtensionAPI) {
  piRef = pi;

  pi.on("session_start", async (_event, _ctx) => {
    ctx = _ctx;
    if (!Redis) {
      try {
        Redis = (await import("ioredis")).default as RedisCtor;
      } catch (error) {
        const message = error instanceof Error ? error.message : String(error);
        _ctx.ui.notify(`Gateway extension running without Redis: ${message}`, "warning");
        return;
      }
    }

    sub = new Redis({ host: "localhost", port: 6379, lazyConnect: true });
    cmd = new Redis({ host: "localhost", port: 6379, lazyConnect: true });
    await sub.connect();
    await cmd.connect();

    await sub.subscribe(NOTIFY_CHANNEL);
    sub.on("message", () => { if (ctx?.isIdle()) drain(); });

    // Drain anything that accumulated while session was down
    const pending = await cmd.llen(EVENT_LIST);
    if (pending > 0) await drain();

    ctx.ui.setStatus("gateway", "🔗 connected");
  });

  pi.on("agent_end", async () => { drain(); });

  pi.on("session_shutdown", async () => {
    if (sub) { sub.unsubscribe(); sub.disconnect(); }
    if (cmd) { cmd.disconnect(); }
  });
}
```

5. Push events from any script:
```typescript
import Redis from "ioredis";
const redis = new Redis();

async function pushEvent(type: string, source: string, payload = {}) {
  const event = { id: crypto.randomUUID(), type, source, payload, ts: Date.now() };
  await redis.lpush("agent:events:main", JSON.stringify(event));
  await redis.publish("agent:notify:main", JSON.stringify({ type }));
}

// Example: notify when a download finishes
await pushEvent("download.complete", "my-script", { file: "video.mp4" });
```

6. Restart pi — the extension loads automatically.

### Tier 2: Always-On with Heartbeat

**Adds:** Cron heartbeat, watchdog failure detection, boot sequence.

**Additional components:**
- HEARTBEAT.md checklist (read by the agent on each heartbeat)
- Watchdog timer in the extension
- tmux or launchd for persistence

**Build steps (on top of Tier 1):**

1. Create `~/HEARTBEAT.md` (or wherever your agent's home is):
```markdown
# Heartbeat Checklist

## System Health
- [ ] Redis is reachable
- [ ] Background worker is responding
- [ ] No stuck jobs

## Pending Work
- [ ] Check inbox for unprocessed items

If nothing needs attention, reply HEARTBEAT_OK.
```

2. Add heartbeat cron — if u
Files: 4
Size: 23.1 KB
Complexity: 46/100
Category: Backend & APIs

Related in Backend & APIs