Caricamento in corso...
Caricamento in corso...
Last synced: Today, 22:00
Technical reference for the OpenClaw framework. Real-time synchronization with the official documentation engine.
Use this file to discover all available pages before exploring further.
The channel turn kernel is the shared inbound state machine that turns a normalized platform event into an agent turn. Channel plugins provide the platform facts and the delivery callback. Core owns the orchestration: ingest, classify, preflight, resolve, authorize, assemble, record, dispatch, and finalize.
Use this when your plugin is on the inbound message hot path. For non-message events (slash commands, modals, button interactions, lifecycle events, reactions, voice state), keep them plugin-local. The kernel only owns events that may become an agent text turn.
Channel plugins repeat the same inbound flow: normalize, route, gate, build a context, record session metadata, dispatch the agent turn, finalize delivery state. Without a shared kernel, a change to mention gating, tool-only visible replies, session metadata, pending history, or dispatch finalization has to be applied per channel.
The kernel keeps four concepts deliberately separate:
ConversationFactsRouteFactsReplyPlanFactsMessageFactsSlack DMs, Telegram topics, Matrix threads, and Feishu topic sessions all distinguish these in practice. Treating them as one identifier causes drift over time.
The kernel runs the same fixed pipeline regardless of channel:
ingestNormalizedTurnInputclassifypreflightresolveauthorizeassembleFinalizedMsgContextbuildContextrecorddispatchfinalizeonFinalizeEach stage emits a structured log event when a
logThe kernel does not throw when a turn is gated. It returns a
ChannelTurnAdmission| Kind | When |
|---|---|
text dispatch | Turn is admitted. Agent turn runs and the visible reply path is exercised. |
text observeOnly | Turn runs end-to-end but the delivery adapter sends nothing visible. Used for broadcast observer agents and other passive multi-agent flows. |
text handled | A platform event was consumed locally (lifecycle, reaction, button, modal). Kernel skips dispatch. |
text drop | Skip path. Optionally text recordHistory: true |
Admission can come from
classifypreflightresolveTurnThe runtime exposes three preferred entry points so adapters can opt in at the level that matches the channel.
typescriptruntime.channel.turn.run(...) // adapter-driven full pipeline runtime.channel.turn.runPrepared(...) // channel owns dispatch; kernel runs record + finalize runtime.channel.turn.buildContext(...) // pure facts to FinalizedMsgContext mapping
Two older runtime helpers remain available for Plugin SDK compatibility:
typescriptruntime.channel.turn.runResolved(...) // deprecated compatibility alias; prefer run runtime.channel.turn.dispatchAssembled(...) // deprecated compatibility alias; prefer run or runPrepared
Use when your channel can express its inbound flow as a
ChannelTurnAdapter<TRaw>ingestclassifypreflightresolveTurnonFinalizetypescriptawait runtime.channel.turn.run({ channel: "tlon", accountId, raw: platformEvent, adapter: { ingest(raw) { return { id: raw.messageId, timestamp: raw.timestamp, rawText: raw.body, textForAgent: raw.body, }; }, classify(input) { return { kind: "message", canStartAgentTurn: input.rawText.length > 0 }; }, async preflight(input, eventClass) { if (await isDuplicate(input.id)) { return { admission: { kind: "drop", reason: "dedupe" } }; } return {}; }, resolveTurn(input) { return buildAssembledTurn(input); }, onFinalize(result) { clearPendingGroupHistory(result); }, }, });
runUse when the channel has a complex local dispatcher with previews, retries, edits, or thread bootstrap that must stay channel-owned. The kernel still records the inbound session before dispatch and surfaces a uniform
DispatchedChannelTurnResulttypescriptconst { dispatchResult } = await runtime.channel.turn.runPrepared({ channel: "matrix", accountId, routeSessionKey, storePath, ctxPayload, recordInboundSession, record: { onRecordError, updateLastRoute, }, onPreDispatchFailure: async (err) => { await stopStatusReactions(); }, runDispatch: async () => { return await runMatrixOwnedDispatcher(); }, });
Rich channels (Matrix, Mattermost, Microsoft Teams, Feishu, QQ Bot) use
runPreparedA pure function that maps fact bundles into
FinalizedMsgContexttypescriptconst ctxPayload = runtime.channel.turn.buildContext({ channel: "googlechat", accountId, messageId, timestamp, from, sender, conversation, route, reply, message, access, media, supplemental, });
buildContextresolveTurnrunThe facts the kernel consumes from your adapter are platform-agnostic. Translate platform objects into these shapes before handing them to the kernel.
| Field | Purpose |
|---|---|
text id | Stable message id used for dedupe and logs |
text timestamp | Optional epoch ms |
text rawText | Body as received from the platform |
text textForAgent | Optional cleaned body for the agent (mention strip, typing trim) |
text textForCommands | Optional body used for text /command |
text raw | Optional pass-through reference for adapter callbacks that need the original |
| Field | Purpose |
|---|---|
text kind | text messagetext commandtext interactiontext reactiontext lifecycletext unknown |
text canStartAgentTurn | If false the kernel returns text { kind: "handled" } |
text requiresImmediateAck | Hint for adapters that need to ACK before dispatch |
| Field | Purpose |
|---|---|
text id | Stable platform sender id |
text name | Display name |
text username | Handle if distinct from text name |
text tag | Discord-style discriminator or platform tag |
text roles | Role ids, used for member-role allowlist matching |
text isBot | True when the sender is a known bot (kernel uses for dropping) |
text isSelf | True when the sender is the configured agent itself |
text displayLabel | Pre-rendered label for envelope text |
| Field | Purpose |
|---|---|
text kind | text directtext grouptext channel |
text id | Conversation id used for routing |
text label | Human label for the envelope |
text spaceId | Optional outer space identifier (Slack workspace, Matrix homeserver) |
text parentId | Outer conversation id when this is a thread |
text threadId | Thread id when this message is inside a thread |
text nativeChannelId | Platform-native channel id when different from the routing id |
text routePeer | Peer used for text resolveAgentRoute |
| Field | Purpose |
|---|---|
text agentId | Agent that should handle this turn |
text accountId | Optional override (multi-account channels) |
text routeSessionKey | Session key used for routing |
text dispatchSessionKey | Session key used at dispatch when different from route key |
text persistedSessionKey | Session key written to persisted session metadata |
text parentSessionKey | Parent for branched/threaded sessions |
text modelParentSessionKey | Model-side parent for branched sessions |
text mainSessionKey | Main DM owner pin for direct conversations |
text createIfMissing | Allow record step to create a missing session row |
| Field | Purpose |
|---|---|
text to | Logical reply target written into context text To |
text originatingTo | Originating context target ( text OriginatingTo |
text nativeChannelId | Platform-native channel id for delivery |
text replyTarget | Final visible-reply destination if it differs from text to |
text deliveryTarget | Lower-level delivery override |
text replyToId | Quoted/anchored message id |
text replyToIdFull | Full-form quoted id when the platform has both |
text messageThreadId | Thread id at delivery time |
text threadParentId | Parent message id of the thread |
text sourceReplyDeliveryMode | text threadtext replytext channeltext directtext none |
AccessFacts| Field | Purpose |
|---|---|
text dm | DM allow/pairing/deny decision and text allowFrom |
text group | Group policy, route allow, sender allow, allowlist, mention requirement |
text commands | Command authorization across configured authorizers |
text mentions | Whether mention detection is possible and whether the agent was mentioned |
| Field | Purpose |
|---|---|
text body | Final envelope body (formatted) |
text rawBody | Raw inbound body |
text bodyForAgent | Body the agent sees |
text commandBody | Body used for command parsing |
text envelopeFrom | Pre-rendered sender label for the envelope |
text senderLabel | Optional override for the rendered sender |
text preview | Short redacted preview for logs |
text inboundHistory | Recent inbound history entries when the channel keeps a buffer |
Supplemental context covers quote, forwarded, and thread-bootstrap context. The kernel applies the configured
contextVisibilitysenderAllowedMedia is fact-shaped. Platform download, auth, SSRF policy, CDN rules, and decryption stay channel-local. The kernel maps facts into
MediaPathMediaUrlMediaTypeMediaPathsMediaUrlsMediaTypesMediaTranscribedIndexesFor full
runtypescripttype ChannelTurnAdapter<TRaw> = { ingest(raw: TRaw): Promise<NormalizedTurnInput | null> | NormalizedTurnInput | null; classify?(input: NormalizedTurnInput): Promise<ChannelEventClass> | ChannelEventClass; preflight?( input: NormalizedTurnInput, eventClass: ChannelEventClass, ): Promise<PreflightFacts | ChannelTurnAdmission | null | undefined>; resolveTurn( input: NormalizedTurnInput, eventClass: ChannelEventClass, preflight: PreflightFacts, ): Promise<ChannelTurnResolved> | ChannelTurnResolved; onFinalize?(result: ChannelTurnResult): Promise<void> | void; };
resolveTurnChannelTurnResolvedAssembledChannelTurn{ admission: { kind: "observeOnly" } }onFinalizeThe kernel does not call the platform directly. The channel hands the kernel a
ChannelTurnDeliveryAdaptertypescripttype ChannelTurnDeliveryAdapter = { deliver(payload: ReplyPayload, info: ChannelDeliveryInfo): Promise<ChannelDeliveryResult | void>; onError?(err: unknown, info: { kind: string }): void; }; type ChannelDeliveryResult = { messageIds?: string[]; threadId?: string; replyToId?: string; visibleReplySent?: boolean; };
deliver{ visibleReplySent: false }createNoopChannelTurnDeliveryAdapter()The record stage wraps
recordInboundSessionrecordtypescriptrecord: { groupResolution, createIfMissing: true, updateLastRoute, onRecordError: (err) => log.warn("record failed", err), trackSessionMetaTask: (task) => pendingTasks.push(task), }
The dispatcher waits for the record stage. If record throws, the kernel runs
onPreDispatchFailurerunPreparedEach stage emits a structured event when a
logtypescriptawait runtime.channel.turn.run({ channel: "twitch", accountId, raw, adapter, log: (event) => { runtime.log?.debug?.(`turn.${event.stage}:${event.event}`, { channel: event.channel, accountId: event.accountId, messageId: event.messageId, sessionKey: event.sessionKey, admission: event.admission, reason: event.reason, }); }, });
Logged stages:
ingestclassifypreflightresolveauthorizeassemblerecorddispatchfinalizeMessageFacts.previewThe kernel owns orchestration. The channel still owns:
If two channels start needing the same helper for one of these, extract a shared SDK helper instead of pushing it into the kernel.
runtime.channel.turn.*SenderFactsConversationFactsRouteFactsReplyPlanFactsAccessFactsMessageFactsSupplementalContextFactsInboundMediaFactsChannelTurnAdmissionChannelEventClassPluginRuntimeopenclaw/plugin-sdk/coreBackward compatibility rules apply: new fact fields are additive, admission kinds are not renamed, and the entry point names stay stable. New channel needs that require a non-additive change must go through the plugin SDK migration process.
runtime.*© 2024 TaskFlow Mirror
Powered by TaskFlow Sync Engine