Queue System

March 18, 2026 · View on GitHub

TinyAGI uses a SQLite-backed queue (tinyagi.db) to coordinate message processing across multiple channels and agents. Messages are stored in a messages table (incoming) and responses table (outgoing), with atomic transactions for reliable delivery.

Overview

┌─────────────────────────────────────────────────────────────┐
│                     Message Channels                         │
│         (Discord, Telegram, WhatsApp, Heartbeat)            │
└────────────────────┬────────────────────────────────────────┘
                     │ enqueueMessage()

┌─────────────────────────────────────────────────────────────┐
│                   ~/.tinyagi/tinyagi.db                     │
│                                                              │
│  messages table                    responses table           │
│  status: pending → processing →   status: pending → acked   │
│          completed / dead                                    │
│                                                              │
└────────────────────┬────────────────────────────────────────┘
                     │ Queue Processor

┌─────────────────────────────────────────────────────────────┐
│              Parallel Processing by Agent                    │
│                                                              │
│  Agent: coder        Agent: writer       Agent: assistant   │
│  ┌──────────┐       ┌──────────┐        ┌──────────┐       │
│  │ Message 1│       │ Message 1│        │ Message 1│       │
│  │ Message 2│ ...   │ Message 2│  ...   │ Message 2│ ...   │
│  │ Message 3│       │          │        │          │       │
│  └────┬─────┘       └────┬─────┘        └────┬─────┘       │
│       │                  │                     │            │
└───────┼──────────────────┼─────────────────────┼────────────┘
        ↓                  ↓                     ↓
   claude CLI         claude CLI             claude CLI
  (workspace/coder)  (workspace/writer)  (workspace/assistant)

Database Schema

The queue lives in ~/.tinyagi/tinyagi.db (SQLite, WAL mode).

Messages Table (incoming queue)

ColumnTypeDescription
idINTEGERAuto-incrementing primary key
message_idTEXTUnique message identifier (nanoid with prefix)
channelTEXTSource channel (discord, telegram, web, etc.)
senderTEXTSender display name
sender_idTEXTSender platform ID
messageTEXTMessage content
agentTEXTTarget agent (null = default)
from_agentTEXTSource agent (internal messages)
statusTEXTpendingprocessingcompleted / dead
retry_countINTEGERNumber of failed attempts
last_errorTEXTLast error message
created_atINTEGERTimestamp (ms)
updated_atINTEGERTimestamp (ms)

Responses Table (outgoing queue)

ColumnTypeDescription
idINTEGERAuto-incrementing primary key
message_idTEXTOriginal message ID
channelTEXTTarget channel for delivery
senderTEXTOriginal sender
sender_idTEXTOriginal sender platform ID
messageTEXTResponse content
original_messageTEXTOriginal user message
agentTEXTAgent that generated the response
filesTEXTJSON array of file paths
metadataTEXTJSON metadata from hooks
statusTEXTpendingacked
created_atINTEGERTimestamp (ms)
acked_atINTEGERTimestamp when channel client acknowledged

Chat Messages Table (team chat room persistence)

ColumnTypeDescription
idINTEGERAuto-incrementing primary key
team_idTEXTTeam that owns this chat room
from_agentTEXTAgent that posted the message
messageTEXTMessage content
created_atINTEGERTimestamp (ms)

This table is append-only and grows indefinitely. All chat room delivery happens through the messages table via postToChatRoom().

Agent Messages Table (per-agent history)

ColumnTypeDescription
idINTEGERAuto-incrementing primary key
agent_idTEXTAgent identifier
roleTEXTuser or assistant
channelTEXTSource channel
senderTEXTSender name
message_idTEXTRelated message ID
contentTEXTMessage content
created_atINTEGERTimestamp (ms)

This table is append-only and grows indefinitely. Provides complete agent interaction history.

Message IDs

All message IDs use nanoid (8 lowercase alphanumeric chars) with a descriptive prefix:

PrefixSource
api_Messages from the REST API
discord_Messages from Discord
telegram_Messages from Telegram
whatsapp_Messages from WhatsApp
internal_Agent-to-agent DMs (teammate mentions)
chat_Chat room broadcasts to individual agents
chatroom_Chat room posts via API
chatroom_batch_Batched chat room messages
proactive_Proactive outgoing messages

Example: internal_a1b2c3d4, api_x9y8z7w6

Message Flow

1. Incoming Message

A channel client receives a message and enqueues it:

enqueueMessage({
    channel: 'discord',
    sender: 'Alice',
    senderId: 'user_12345',
    message: '@coder fix the authentication bug',
    messageId: genId('discord'),
});

This inserts a row into messages with status = 'pending' and emits a message:enqueued event for instant pickup.

2. Processing

The queue processor picks up messages via two mechanisms:

  • Event-driven: queueEvents.on('message:enqueued') — instant for in-process messages
  • Polling fallback: Every 5s — catches cross-process messages from channel clients

For each pending agent, the processor claims all pending messages at once via claimAllPendingMessages(agentId):

const msgs = claimAllPendingMessages('coder');
// Sets status = 'processing' for all claimed messages

The first message becomes the primary message; the rest are batched as additional context and delivered together in a single agent invocation.

3. Agent Processing

Each agent has its own promise chain for sequential processing:

// Messages to same agent = sequential (preserve conversation order)
agentChain: msg1 → msg2 → msg3

// Different agents = parallel (don't block each other)
@coder:     msg1 ──┐
@writer:    msg1 ──┼─→ All run concurrently
@assistant: msg1 ──┘

4. Response

After the AI responds, the response is streamed to the user immediately via streamResponse(), which enqueues it in the responses table. The original message is marked status = 'completed'.

If the response contains [@teammate: message] tags, those are extracted and enqueued as new internal messages — flat DMs with no conversation tracking.

5. Channel Delivery

Channel clients poll for responses:

const responses = getResponsesForChannel('discord');
for (const response of responses) {
    await sendToUser(response);
    ackResponse(response.id);  // marks status = 'acked'
}

Error Handling & Retry

Retry Logic

When processing fails, failMessage() increments retry_count:

Attempt 1: fails → retry_count = 1, status = 'pending'
Attempt 2: fails → retry_count = 2, status = 'pending'
...
Attempt 5: fails → retry_count = 5, status = 'dead'

Messages that exhaust retries (default: 5) are marked status = 'dead'.

Dead-Letter Management

GET    /api/queue/dead           → list dead messages
POST   /api/queue/dead/:id/retry → reset retry count, re-queue
DELETE /api/queue/dead/:id       → permanently delete

Stale Message Recovery

Messages stuck in processing (e.g., from a crash) are automatically recovered every minute:

recoverStaleMessages(10 * 60 * 1000);  // anything processing > 10 min

Real-Time Events

The queue processor emits events via an in-memory listener system. The API server broadcasts these over SSE at GET /api/events/stream.

EventDescription
message_receivedNew message picked up
agent_routedMessage routed to agent
chain_step_startAgent begins processing
chain_step_doneAgent finished (includes response)
chain_handoffAgent mentions a teammate
response_readyResponse enqueued for delivery
processor_startQueue processor started

API Endpoints

The API server runs on port 3777 (configurable via TINYAGI_API_PORT):

EndpointDescription
POST /api/messageEnqueue a message
GET /api/queue/statusQueue depth (pending, processing, dead)
GET /api/queue/agentsPer-agent queue depth (pending, processing)
GET /api/responsesRecent responses
GET /api/queue/deadDead messages
POST /api/queue/dead/:id/retryRetry a dead message
DELETE /api/queue/dead/:idDelete a dead message
GET /api/events/streamSSE event stream

Maintenance

Periodic cleanup tasks run every 60 seconds:

  • Stale message recovery: Messages stuck in processing > 10 min reset to pending
  • Acked response pruning: Responses acked > 24h ago are deleted
  • Completed message pruning: Messages completed > 24h ago are deleted

See Also