Skip to content
Open
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
34 changes: 34 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,37 @@ jobs:

- name: Lint
run: npx oxlint src/

test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [20, 22]
services:
redis:
image: redis:7
ports:
- 6379:6379
options: >-
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4

- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}

- name: Install dependencies
run: npm install --ignore-scripts

- name: Build
run: npm run build

- name: Run tests
run: npm test
env:
REDIS_URL: redis://127.0.0.1:6379
134 changes: 103 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,35 +13,45 @@ gptqueue lets AI agents (Claude Code, Codex, Gemini CLI, or any MCP-compatible c
## Architecture

```
┌─────────────┐ MCP (stdio) ┌──────────────┐
│ AI Agent A │◄──────────────────────►│ │
└─────────────┘ │ gptqueue │ ┌───────┐
│ MCP server │◄─────►│ Redis │
┌─────────────┐ MCP (stdio) │ │ └───────┘
│ AI Agent B │◄──────────────────────►│ │
└─────────────┘ └──────────────┘
MCP (stdio)
┌─────────────┐ ◄──────────────────────► ┌──────────────┐
│ AI Agent A │ │ │
└─────────────┘ │ gptqueue │
MCP (HTTP) │ MCP server │◄─────► Redis
┌─────────────┐ ◄──────────────────────► │ │
│ AI Agent B │ │ (sessions) │
└─────────────┘ └──────────────┘
```

Each agent gets its own bounded inbox queue in Redis. Messages are delivered atomically via a Lua script that enforces queue size limits. Agents publish heartbeats so others can see who is online.
Each agent gets its own bounded inbox queue in Redis. Messages are delivered atomically via a Lua script that enforces queue size limits.

Agent identity is backed by Redis session records with TTL-based leases, so sessions survive process restarts and work across transport boundaries.

## Components

| Component | Description |
|---|---|
| **MCP server** (`src/mcp-server/`) | Stdio-based MCP server exposing 6 tools for agent communication |
| **MCP server** (`src/mcp-server/`) | MCP server exposing tools for agent communication |
| **HTTP transport** (`src/transports/http.ts`) | Streamable HTTP server -- no bridge needed for shared hosting |
| **Core** (`src/core/`) | Transport-agnostic session store, mailbox store, and type definitions |
| **PTY wrapper** (`src/pty-wrapper/`) | Wraps a CLI process in a PTY, watches Redis for incoming messages, and injects notifications when the process is idle |
| **Hook script** (`scripts/check-queue.sh`) | Claude Code hook for startup context injection and stop-gate (blocks exit if inbox has unread messages) |

## Design Reports

- [Session and Transport Redesign Report](docs/SESSION_TRANSPORT_REDESIGN_REPORT.md)

## MCP Tools

| Tool | Description |
|---|---|
| `register_agent` | Register with a name, role (`publisher`/`consumer`/`both`), and description |
| `send_message` | Send a typed message (`task`/`result`/`status`/`error`/`ping`) to another agent's inbox. Supports optional `metadata` object and `in_reply_to` message ID for threading |
| `receive_message` | Blocking pop from your inbox (default timeout: 5s) |
| `register_agent` | Register with a name, role, and description. Returns a `session_id` for session resumption |
| `send_message` | Send a typed message (`task`/`result`/`status`/`error`/`ping`) to another agent's inbox. Supports optional `metadata`, `in_reply_to`, and `session_id` for stateless transports |
| `receive_message` | Blocking pop from your inbox (default timeout: 5s). Supports optional `session_id` for stateless transports |
| `list_agents` | Discover all registered agents with online/offline status |
| `get_queue_status` | Check queue depth and capacity for one or all agents |
| `unregister_agent` | Unregister and clean up queue data |
| `close_session` | Close the current session but preserve the mailbox. Messages remain queued for later reconnection. Supports optional `session_id` for stateless transports |
| `unregister_agent` | Unregister and delete all queue data (destructive). Supports optional `session_id` for stateless transports |

## Prerequisites

Expand All @@ -56,9 +66,33 @@ cd gptqueue
npm install # builds automatically via postinstall
```

## Transports

### Stdio (direct, single-client)

For local use with one MCP client:

```bash
node /path/to/gptqueue/dist/mcp-server/index.js [agent-name]
```

Or set `GPTQ_AGENT_NAME` in the environment.

### HTTP (shared, multi-client)

For shared hosting without bridges like supergateway:

```bash
node /path/to/gptqueue/dist/transports/http.js --port 3001
```

Each connecting client gets its own MCP session backed by Redis. Sessions survive reconnects.

Health check: `GET /health` returns `{"status":"ok","sessions":N}`.

## Configuration

### Claude Code
### Claude Code (stdio)

Add to `~/.claude.json` under `mcpServers`:

Expand All @@ -72,6 +106,26 @@ Add to `~/.claude.json` under `mcpServers`:
}
```

### Claude Code (HTTP)

Start the HTTP server, then configure the client to connect:

```bash
# Start the server
node /path/to/gptqueue/dist/transports/http.js --port 3001
```

```json
{
"gptqueue": {
"type": "streamable-http",
"url": "http://127.0.0.1:3001/mcp"
}
}
```

### Hook script

Optionally add the hook script to `~/.claude/settings.json` for automatic startup context and exit gating:

```json
Expand All @@ -95,23 +149,14 @@ Optionally add the hook script to `~/.claude/settings.json` for automatic startu
}
```

### Any MCP client

The server speaks stdio MCP. Point your client at:

```bash
node /path/to/gptqueue/dist/mcp-server/index.js [agent-name]
```

Or set `GPTQ_AGENT_NAME` in the environment.

### Environment variables

| Variable | Default | Description |
|---|---|---|
| `REDIS_URL` | `redis://127.0.0.1:6379` | Redis connection URL (MCP server and PTY wrapper) |
| `GPTQ_AGENT_NAME` | _(none)_ | Pre-register with this agent name on startup |
| `REDIS_URL` | `redis://127.0.0.1:6379` | Redis connection URL |
| `GPTQ_AGENT_NAME` | _(none)_ | Pre-register with this agent name on startup (stdio only) |
| `GPTQ_QUEUE_BOUND` | `10` | Max messages per agent inbox |
| `GPTQ_HTTP_PORT` | `3001` | HTTP server port |
| `REDIS_HOST` | `127.0.0.1` | Redis host (hook script only) |
| `REDIS_PORT` | `6379` | Redis port (hook script only) |

Expand All @@ -125,15 +170,42 @@ gptqueue-pty --agent alice --cmd claude

## How it works

1. **Registration** -- An agent calls `register_agent` with a name, role, and description. This writes to a Redis hash (`gptq:registry`) and starts a heartbeat (10s interval, 30s TTL).
1. **Registration** -- An agent calls `register_agent` with a name, role, and description. This creates a Redis-backed session with a TTL lease and returns a `session_id`.

2. **Sessions** -- Each registration creates a session record in Redis. Sessions have TTL-based leases that are automatically refreshed. An agent can have multiple concurrent sessions (e.g. from different transports).

3. **Discovery** -- Any agent (even unregistered) can call `list_agents` to see all registered agents and whether they're online. Online status is computed from active session leases.

4. **Messaging** -- `send_message` pushes to the target agent's Redis list (`gptq:q:<name>`). A Lua script enforces the queue bound atomically. If the queue is full, the sender retries with exponential backoff (up to 10 attempts).

5. **Receiving** -- `receive_message` does a blocking pop (`BLPOP`) with a configurable timeout.

6. **Session close** -- `close_session` drops the live session but preserves the mailbox. Queued messages remain available for a future session.

7. **Unregister** -- `unregister_agent` closes the session AND deletes the mailbox. This is a destructive operation.

2. **Discovery** -- Any agent (even unregistered) can call `list_agents` to see all registered agents and whether they're online.
## Known Limitations

3. **Messaging** -- `send_message` pushes to the target agent's Redis list (`gptq:q:<name>`). A Lua script enforces the queue bound atomically. If the queue is full, the sender retries with exponential backoff (up to 10 attempts).
### Bridge-based transport (supergateway)

4. **Receiving** -- `receive_message` does a blocking pop (`BLPOP`) with a configurable timeout.
When using the stdio transport behind a bridge like `supergateway`, tool calls may land in different worker processes. The recommended solution is to use the native HTTP transport instead, which eliminates the need for bridges entirely.

5. **Cleanup** -- `unregister_agent` removes the agent from the registry and deletes its queue, metadata, and heartbeat keys.
If you must use a stateless or bridge-based transport, capture the
`session_id` returned by `register_agent` and pass it to session-scoped
tools such as `send_message`, `receive_message`, `close_session`, and
`unregister_agent`.

### Child-process accumulation

If still using bridges, they can accumulate orphaned gptqueue worker processes. To check and clean up:

```bash
# Count gptqueue child workers
pgrep -f 'gptqueue' | wc -l

# Kill orphaned workers (use with caution)
pkill -f 'dist/mcp-server/index.js'
```

## License

Expand Down
2 changes: 2 additions & 0 deletions bin/gptqueue-http
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#!/usr/bin/env node
import "../dist/transports/http.js";
81 changes: 81 additions & 0 deletions docs/MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Migration Guide: Session-Aware gptqueue

This document covers changes from the original single-process stdio design to the session-aware architecture.

## What Changed

### Registration returns a session_id

**Before:**
```json
{ "status": "registered", "name": "my-agent", "role": "both" }
```

**After:**
```json
{ "status": "registered", "name": "my-agent", "session_id": "abc-123", "role": "both" }
```

The `session_id` can be used to reconnect from a different process without re-registering.

For stateless or bridge-based transports, pass that `session_id` to
session-scoped tools:

- `send_message(session_id?, to, ...)`
- `receive_message(session_id?, timeout)`
- `close_session(session_id?)`
- `unregister_agent(session_id?)`

### New tool: close_session

`close_session` drops the live session but **preserves** the mailbox. Queued messages remain available for a future session.

`unregister_agent` remains available and is **destructive** -- it deletes the mailbox and all queue data.

### Online status from session leases

Online/offline status is now computed from session leases (TTL-based keys in Redis) rather than a single heartbeat key. An agent with multiple sessions is online if any session has a live lease.

### Native HTTP transport

A new HTTP transport eliminates the need for `supergateway` or other stdio bridges:

```bash
node dist/transports/http.js --port 3001
```

## Redis Key Changes

### New keys

| Key | Type | Purpose |
|---|---|---|
| `gptq:session:<session_id>` | Hash | Session record (agent_name, role, timestamps) |
| `gptq:lease:<session_id>` | String with TTL | Session liveness indicator |
| `gptq:agent-sessions:<name>` | Set | Active session IDs for an agent |

### Preserved keys

| Key | Type | Purpose |
|---|---|---|
| `gptq:registry` | Hash | Agent discovery index (unchanged) |
| `gptq:q:<name>` | List | Mailbox queue (unchanged) |
| `gptq:meta:<name>` | Hash | Queue metadata (unchanged) |
| `gptq:heartbeat:<name>` | String with TTL | Legacy heartbeat (kept for backward compat) |

## Backward Compatibility

- Existing stdio clients continue to work without changes
- `GPTQ_AGENT_NAME` auto-registration still works (now creates a session automatically)
- Legacy heartbeat keys are still written alongside session leases
- `list_agents` checks both session leases and legacy heartbeats
- `unregister_agent` still performs the same destructive cleanup

## Upgrading

1. Update your gptqueue installation: `git pull && npm install`
2. No Redis data migration needed -- new keys are additive
3. To use HTTP transport, start the HTTP server and update your MCP client config
4. To use stateless or bridge-friendly session reconnection, capture
`session_id` from `register_agent` responses and pass it to
session-scoped tools
3 changes: 3 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# gptqueue Docs

- [Session and Transport Redesign Report](./SESSION_TRANSPORT_REDESIGN_REPORT.md)
Loading
Loading