一键导入
opencode-plugin-architecture
Use when you need to understand the OpenCode plugin event lifecycle, session state transitions, and daemon API contracts
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Use when you need to understand the OpenCode plugin event lifecycle, session state transitions, and daemon API contracts
用 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 worker architecture, endpoint flow, and command routing behavior before making changes
Use when you need to understand daemon route flow, storage model, worker connectivity, and command injection architecture before making changes
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 | opencode-plugin-architecture |
| description | Use when you need to understand the OpenCode plugin event lifecycle, session state transitions, and daemon API contracts |
Use this before changing plugin event handling or daemon payload contracts.
@pigeon/opencode-pluginpackages/opencode-plugin/session-start)./stop).question.asked events enqueue question in in-memory retry queue (bypasses circuit breaker) → sendQuestionAsked with 3s timeout → daemon /question-asked.question.replied / question.rejected events notify daemon the question is resolved (/question-answered).Question notifications use a dedicated path that bypasses the circuit breaker to avoid question delivery being blocked by unrelated daemon HTTP failures:
question.asked fires, the question is enqueued immediately (synchronous) and delivered asynchronously with retries.sendQuestionAsked: calls daemon /question-asked with a 3s timeout. Does not affect circuit breaker state -- success or failure is recorded only in the retry queue.{ok: true, deliveryState: "accepted", notificationId} (HTTP 202). The plugin handles both this format and the legacy {notified: true} (HTTP 200) shape.notifyQuestionAsked from daemon-client is no longer used for question events. It remains available but the plugin routes question delivery through sendQuestionAsked instead.The plugin's direct channel server (direct-channel.ts) exposes /pigeon/direct/question-reply for the daemon to deliver answers back from Telegram. The handler calls OpenCode's /question/{requestId}/reply API to unblock the question tool.
Critical: in-process fetch. In TUI mode, OpenCode does NOT run an HTTP server. The SDK client injected into plugins uses a custom fetch that calls Server.App().fetch() in-process (bypassing the network). ctx.serverUrl resolves to http://localhost:4096 but nothing listens there.
The plugin extracts the SDK client's internal fetch at init time:
const sdkClientConfig = (ctx.client as any)._client?.getConfig?.()
const internalFetch: typeof fetch = sdkClientConfig?.fetch ?? globalThis.fetch
The onQuestionReply handler uses internalFetch (with a new Request(...)) to call the OpenCode API. Using raw globalThis.fetch would fail with "Unable to connect" because no HTTP server is running.
swarm.read)The plugin registers swarm.read as an opencode tool the LLM can call to fetch its swarm inbox from the local pigeon daemon. This is the receiver side of the swarm IPC subsystem (sender side is pigeon-send; full subsystem in swarm-architecture).
packages/opencode-plugin/src/swarm-tool.ts:
swarmRead({daemonBaseUrl, sessionId, fetchFn?}, since?) -- pure helper. GET /swarm/inbox?session=<id>[&since=<msg_id>]. Returns the parsed messages array.formatInbox(messages) -- renders the messages as compact text blocks for the LLM.createSwarmReadTool(daemonBaseUrl) -- returns a ToolDefinition that captures daemonBaseUrl from closure and reads sessionID from ToolContext at execute-time.Registered in index.ts via the Hooks.tool map:
return {
tool: {
"swarm.read": createSwarmReadTool(daemonUrl),
},
event: async (input) => { ... },
}
Hooks.tool is { [key: string]: ToolDefinition } from @opencode-ai/plugin/tool. The tool({description, args, execute}) factory accepts zod-described args via tool.schema.string().optional(). The execute callback receives (args, ToolContext) where ToolContext.sessionID is the calling session — so the tool always reads its OWN inbox, no spoofing.
Critical: import from @opencode-ai/plugin/tool (subpath), NOT @opencode-ai/plugin. The upstream package's compiled JS uses extensionless ESM imports (import "./tool") which Node ESM rejects, but the explicit ./tool subpath export resolves correctly. See opencode-plugin-development for the gotcha details.
MessageTail in message-tail.ts captures files from AI responses:
type: "file" with mime and url, pushed to tail.files.status: "completed" and attachments[], each attachment's { mime, filename, url } is captured.On session.idle, the plugin includes media: FileInfo[] (data URIs) in the /stop payload. The daemon uploads these to R2 and the worker sends them to Telegram as photo/document replies.
When the daemon delivers an execute command with a media field (from a Telegram photo/document relayed through R2):
ExecuteCommandEnvelope at /pigeon/direct/execute.onExecute handler builds a parts array for OpenCode's prompt_async endpoint:
{ type: "text", text: command } if text is present{ type: "file", mime, filename, url } if media is present (data URI from daemon's R2 fetch){ parts, noReply: false } to /session/<id>/prompt_async./session-start payload includes session/process/transport context./stop payload includes event + summary/message, label context, and optional media: Array<{ mime, filename, url }> (data URIs from captured files)./question-asked payload includes session_id, request_id, questions[] (with question, header, options[], custom?, multiple?), and label. Response: HTTP 202 {ok: true, deliveryState: "accepted", notificationId}. Daemon stores durably in SQLite outbox and returns immediately; background OutboxSender delivers to Telegram./question-answered payload includes session_id.npm run --workspace @pigeon/opencode-plugin test
npm run --workspace @pigeon/opencode-plugin typecheck