원클릭으로
worker-architecture
Use when you need to understand worker architecture, endpoint flow, and command routing behavior before making changes
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Use when you need to understand worker architecture, endpoint flow, and command routing behavior before making changes
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Use when deploying pigeon code changes across all machines after merging to main
Use when you need to understand daemon route flow, storage model, worker connectivity, and command injection architecture before making changes
Use when you need to understand the OpenCode plugin event lifecycle, session state transitions, and daemon API contracts
Use when developing or refactoring OpenCode plugin handlers, tests, and daemon integration payloads
Use when you need to understand the swarm IPC subsystem — tables, routes, the per-target arbiter, the session→directory registry, and the wire envelope — before changing it
Use when implementing or testing swarm IPC features (new kinds, channels, plugin tools, or schema changes) with a TDD-first workflow
| name | worker-architecture |
| description | Use when you need to understand worker architecture, endpoint flow, and command routing behavior before making changes |
Use this skill when you need system-level understanding before debugging, deployment, or parity checks.
@pigeon/worker is a Cloudflare Worker backed by D1 (serverless SQLite).
packages/worker/src/index.tspackages/worker/src/d1-ops.tspackages/worker/src/d1-schema.sqlpigeon-router/sessions/register) with sessionId -> machineId/notifications/send) to Telegram and mapped as chat_id + message_id -> sessionId + token/webhook/telegram/...)commands tableGET /machines/:id/next, receives command, delivers to pluginPOST /commands/:id/ack, command marked doneWhen the daemon sends a notification with inline buttons, the callback_data contains the daemon's token as cmd:TOKEN:action. The worker's /notifications/send handler extracts this token from the first button's callback_data and stores it in the messages table (instead of generating its own). This ensures that when the user clicks a button, the worker can look up the token and resolve it to the correct session.
If no cmd:TOKEN:action pattern is found in the replyMarkup (e.g. plain text notifications), the worker generates a fresh random token. See extractTokenFromCallbackData() in notifications.ts.
The worker binds an R2 bucket (MEDIA, bucket pigeon-media) for bidirectional media relay between Telegram and OpenCode sessions.
env.MEDIA (configured in wrangler.toml){direction}/{timestamp}-{id}/{filename} where direction is inbound or outboundcleanupExpiredMedia in media.ts)POST /media/upload (Bearer) -- multipart form with key, mime, filename, file fields. Returns { ok, key }.GET /media/<key> (Bearer) -- returns raw binary with Content-Type and Content-Disposition from R2 metadata.extractMedia() in webhook.ts detects photo/document/audio/video/voice on incoming messages.MAX_IMAGE_DIMENSION (1568px). If no variant fits, the media is skipped entirely (text command still goes through). This avoids Anthropic's 2000px multi-image limit and the latency penalty for images above 1568px.relayMediaToR2() calls Telegram getFile API, downloads the binary, stores in R2 with key inbound/<ts>-<fileUniqueId>/<filename>.MediaRef { key, mime, filename, size } is serialized as media_json in the commands row.media: { key, mime, filename, size } in the command payload.Image sizing trade-offs: We rely on Telegram's pre-generated photo variants (~90px, ~320px, ~800px, ~1280px) rather than resizing ourselves. The ~1280px variant is typically selected, which is high quality for most use cases. If finer control is ever needed, daemon-side resizing with sharp (after R2 fetch) or worker-side WASM resizing can be layered on. See docs/plans/2026-03-08-inbound-image-sizing-design.md.
POST /media/upload with key outbound/<ts>-<uuid>/<filename>.POST /notifications/send accepts an optional media: Array<{ key, mime, filename }> field.sendPhoto (images) or sendDocument (other types), each as a reply to the text notification message.messages table for reply routing.commands: command delivery queue with lease-based polling (pending/leased/done lifecycle)sessions: session-to-machine registrymessages: Telegram message mapping for reply routing. Includes notification_id TEXT (nullable, unique index where not null) for idempotent notification delivery -- if a notificationId is provided on /notifications/send and already exists in this table, the request is deduplicated without calling Telegram again.seen_updates: Telegram update deduplicationmachines: daemon last-poll-at tracking for online detectionGET /health -> okGET /machines/:id/next (Bearer CCR_API_KEY) -> poll for next command (204 if none, JSON if available)POST /commands/:id/ack (Bearer) -> acknowledge a command (mark done)GET /sessions (Bearer) -> JSON list of session rowsPOST /sessions/register (Bearer) -> upsert sessionPOST /sessions/unregister (Bearer) -> remove sessionPOST /notifications/send (Bearer) -> send Telegram message + store mapping; optional media[] sends photos/documents as replies; optional notificationId enables idempotent delivery (returns {ok: true, messageId, deduplicated: true} if already delivered, without calling Telegram)POST /notifications/edit (Bearer) -> edit an existing Telegram message by notificationId; looks up (chat_id, message_id) from the messages table. Used by the daemon for wizard step transitions.POST /media/upload (Bearer) -> upload file to R2 (multipart form)GET /media/<key> (Bearer) -> download file from R2POST /webhook/telegram/{path} (X-Telegram-Bot-Api-Secret-Token) -> process replies/callbacks/launch <machine> <directory> <prompt>Starts a headless OpenCode session on a remote machine.
"launch" type command in D1.{ commandType: "launch", commandId, directory, prompt, chatId }launch-ingest.ts creates a session via OpencodeClient.createSession(), sends the prompt, and replies to Telegram with the session ID./sessions/register./kill <session-id>Terminates a running OpenCode session.
sessions table to find the machine_id."kill" type command in D1.{ commandType: "kill", commandId, sessionId, chatId }kill-ingest.ts calls OpencodeClient.deleteSession() and replies to Telegram with the result./compact (reply-based)Summarizes a session's conversation to reduce context.
/compact to any session notification in Telegram.messages table lookup."compact" type command in D1.compact-ingest.ts fetches session messages, finds the last user message's model, and calls OpencodeClient.summarize()./mcp list|enable|disable <args>Manages MCP server connections for a session.
/mcp list <session-id>: lists all MCP servers with status (connected, disabled, failed, needs_auth)./mcp enable <server> <session-id>: connects (or reconnects if already connected) an MCP server./mcp disable <server> <session-id>: disconnects an MCP server.All three variants check the session exists and the machine is online before queuing. Daemon's mcp-ingest.ts calls OpencodeClient.mcpStatus(), mcpConnect(), or mcpDisconnect().
/model <args>Lists or sets the model for a session.
/model <session-id>: lists available models from the configured provider allowlist (default anthropic, openai; extendable per-device via the PIGEON_ALLOWED_PROVIDERS env var, e.g. to add google-vertex and google-vertex-anthropic) with the current default./model <provider/model> <session-id>: validates the model exists, then stores it as a model_override on the session in daemon SQLite.The override is applied on subsequent command deliveries -- command-ingest.ts reads it and passes it through the adapter as metadata.model. The plugin includes it in the prompt_async request body.
CommandType = "execute" | "launch" | "kill" | "compact" | "mcp_list" | "mcp_enable" | "mcp_disable" | "model_list" | "model_set" (in webhook.ts)
execute: regular command injection into an existing session (default)launch: create a new headless session + send initial promptkill: terminate an existing session via opencode APIcompact: summarize/compact a session's conversationmcp_list: list MCP servers with connection statusmcp_enable: connect (or reconnect) an MCP servermcp_disable: disconnect an MCP servermodel_list: list available models from allowed providersmodel_set: set a per-session model overridestatus = 'pending'GET /machines/:id/next atomically sets status = 'leased' and leased_at = nowPOST /commands/:id/ack sets status = 'done' and acked_at = now0 * * * * (hourly)scheduled() in index.ts calls:
cleanupExpiredMedia(env) -- deletes R2 objects older than 24 hourscleanupCommands(env.DB) -- deletes old acked and stuck commands from D1cleanupSeenUpdates(env.DB) -- deletes old Telegram dedup entries from D1Long polling at the Worker level (GET /machines/:id/next?timeout=25) would reduce daemon HTTP traffic from ~17K/day to ~3.5K/day per daemon. Workers support up to 30s request duration on the free plan. Not needed at current scale.
Run:
npm run --workspace @pigeon/worker test
npm run --workspace @pigeon/worker typecheck
Expected: