13 KiB
TinyChannels Integration Plan
Plan for adopting the tinychannels crate
(~/work/tinyhumansai/tinychannels) as the typed channel layer, deleting the
duplicated copies in this repo, and fixing the channel bugs surfaced by the
2026-07-04 cross-repo audit.
Companion docs in the tinychannels repo:
docs/spec/openclaw-hermes-channel-porting.md— the upstream research spec.docs/spec/tinychannels-execution-plan.md— the crate-side phased plan. Steps below reference its phases.
Current State
-
This repo now depends on
tinychannelsthrough the TinyChannelsfeat/phase-0-hygienebranch while the companion PR lands. The first duplicate-removal pass has landed:openhuman-4 file tinychannels file src/openhuman/channels/traits.rsre-exports src/traits.rssrc/openhuman/channels/controllers/definitions.rsre-exports src/controllers/definitions.rssrc/openhuman/channels/controllers/schemas.rsschema declarationsconverts from src/controllers/schemas.rssrc/openhuman/channels/controllers/ops/connect.rsallowlist/key helperscalls src/controllers/credentials.rssrc/openhuman/channels/controllers/ops/types.rsre-exports src/controllers/types.rssrc/openhuman/config/schema/channels.rsre-exports provider config from src/config.rs, while keeping OpenHuman-owned security/sandbox config localsrc/openhuman/channels/context.rskey/constants helperscalls/re-exports src/context.rswhere type-compatiblesrc/openhuman/channels/runtime/supervision.rsin-flight sizingre-exports src/runtime.rs::compute_max_in_flight_messagesTelegram/Discord text splitting calls src/text.rschunker with UTF-16 measurement -
OpenHumanChannelBackendnow lives insrc/openhuman/channels/controllers/backend.rsand implementstinychannels::ChannelBackendby delegating to the existingchannels/controllers/ops/{connect,messaging,discord,telegram}.rsflows. -
The crate-side Phase 5 relay contract now includes typed gateway/connector relay frames, the request/response frame transport loop, a feature-gated WebSocket dialer with reconnect supervision, and a portable relay runtime config shape under
channels_config.relay. OpenHuman now enables the crate's relay WebSocket feature and starts the relay runtime for complete relay config. -
The metadata controllers (
channels.list/channels.describe) now useChannelManagerfor definition lookup. Thechannels.statusandchannels.testcontrollers now route throughChannelManager<OpenHumanChannelBackend>, preserving the existing no-log JSON response shapes while using the crate manager for definition lookup and credential validation where applicable.channels.get_defaultalso routes through the manager while preserving the public{ active_channel }response,channels.connectandchannels.set_defaultpreserve their public log/value envelopes after manager dispatch, and the managed Telegram/Discord link controllers route through the manager while preserving their no-log typed responses. The no-log send/reaction/thread controllers now dispatch through the manager and unwrap the backendrawpayload to preserve legacy top-level JSON shapes;channels.send_message, direct channel-bus backend sends, and legacy runtime/proactive/provider-helperChannel::sendcallers now cross the crate intent bridge asChannelOutboundIntents and get deterministic idempotency keys before legacy backend/provider calls.channels.disconnectalso routes through the manager while preserving the old log string and extra fields such asmemory_chunks_deleted. Discord guild/channel/permission discovery dispatches through the manager while restoring the old log strings and top-level provider JSON. Outbound sends now use the live relay transport for configured relay identities. The remaining OpenHuman work is still full session identity switchover, provider extraction, and the planned test split below; the first envelope slice is landed because the runtime now publishes a TinyChannelsChannelInboundEnvelopeonChannelMessageReceivedevents, memory conversation persistence records the TinyChannels session key as migration metadata, and startup now has a relay inbound handler that preserves the original authenticated relay envelope, includingscope_id, from the live relay socket through dispatch.
Step 1 — Add the dependency, delete duplicates
- Landed: Add the
tinychannelsgit dependency toCargo.toml(single crate, not a workspace; edition 2021 depending on an edition-2024 crate is fine). - Landed for the first slice: Delete the duplicated
definitions/types/traits/config-schema/helper code and
re-export from the old paths so the 100+ call sites keep compiling:
src/openhuman/channels/mod.rs:pub use tinychannels::{Channel, ChannelMessage, SendMessage, ...};src/openhuman/channels/controllers/...: re-exportChannelDefinition,ChannelAuthMode, response types.src/openhuman/config/schema/channels.rs: re-exportChannelsConfigand provider config structs. Note tinychannels inlinedEmailConfigandYuanbaoConfig, which this repo currently sources fromchannels::email_channel/providers::yuanbao— repoint those two to the crate and delete the local definitions.
- Still pending: delete migrated in-module tests once each suite is fully represented in tinychannels. The duplicate definitions, config-schema, and in-flight sizing helper suites have been deleted; keep everything provider- or app-specific (see Step 4).
- Do not remove sandbox/security config from this repo based on the crate's
copy: the crate is dropping its unwired
SecurityConfig/SandboxConfigcluster as scope creep; this repo's originals (if used) stay where they are.
Step 2 — Implement ChannelBackend
- Landed: New
OpenHumanChannelBackendinsrc/openhuman/channels/controllers/backend.rs, delegating each trait method to the existing ops functions:send_message→messaging.rs::channel_send_message(already composeseffective_backend_api_url+jwt::get_session_token+BackendOAuthClient), withsend_message_valuepreserving OpenHuman's arbitrary rich-message JSON for the public RPC controller.connect_channel/disconnect_channel→connect.rsflows pluscredentials::ops::{store,remove}_provider_credentials(keyed"channel:<slug>"), with disconnect returning the raw legacy payload for fields such asmemory_chunks_deleted.channel_status/test_channel→ existing status/test ops plushealth::snapshot.- Telegram login and Discord link/guild/permission methods → the existing
telegram.rs/discord.rsops.
- Landed: The current
channels.*controller entry points dispatch throughChannelManager<OpenHumanChannelBackend>where they cross this crate seam, while preserving the legacy public JSON/log envelopes. - The event bus, health bus, and dispatch engine stay app-side and drive
tinychannels. Never add a tinychannels → openhuman dependency; the
runtime/dispatch engine and thewebprovider are consumers of the crate, not porting candidates (they import ~45 openhuman modules).
Step 3 — Bug fixes coordinated with the crate
Fix these here in lockstep with tinychannels Phase 1/2 (same semantics, one implementation — prefer calling the crate's new helpers over patching local copies):
- Landed: UTF-16 chunking. Telegram and Discord splitting now call the crate's Phase 2 chunker with UTF-16 code-unit measurement and markdown fence preservation.
- Landed for existing helper semantics: Telegram history keys.
channels/context.rsnow delegates to the crate helper, preserving OpenHuman's current Telegram topic behavior. Runtime received-message events now also carry the crate's normalized inbound envelope, with Telegramthread_tsprojected astopic_id; conversation persistence records the TinyChannels session key when an event carries an envelope. Relay inbound now preserves the relay-supplied envelope andscope_idthrough dispatch. Full non-relay session identity switchover still needs provider-sourcedscope_id. - Partially landed for relay inbound; non-relay providers still deferred:
workspace/tenant discriminator. Session keys
(
channels/bus.rs:1005-1042) omit guild/team/tenant; Slack channel ids are only workspace-unique. Relay inbound carries the relay envelope'sscope_idthrough received-message events; provider-originated legacy messages still need native envelope construction with provider-sourced scope. Deferral reason: adding scope to the current legacy string keys would change conversation identity without the envelope migration that can preserve compatibility aliases. - Landed for portable send boundaries; provider wire enforcement remains
platform-specific.
No idempotency key exists on legacy sends; a retry after a transport error
double-posts.
channels.send_messagenow routes through a crateChannelOutboundIntentat theChannelBackendseam while preserving the legacy public RPC response and backend JSON shape. The direct channel bus backend sends and runtime/proactive/provider-helperChannel::sendpaths use the same crate bridge, carrying deterministic idempotency keys throughSendMessage. Provider-specific use of those keys on external wire APIs remains Phase 6/platform work. - Deferred pending product decision:
conversation_memory_keyintent check. It keys onmsg.id(per-message, not per-conversation) and this repo's tests assert that behavior (tests/memory.rs). Confirm whether per-turn keying is intended; the crate will rename or fix accordingly — this repo should follow.
Step 4 — Test split
- Partly landed: the portable
controllers/schemas_tests.rsschema-catalog assertions, pure allowlist/key helper tests, catalog/request-shape halves ofcontrollers/ops_tests.rs, backend-agnostic default-channel validation, and config-backed channel status detection now live in tinychannels. The duplicate definitions suite, config-schema assertions, in-flight sizing helper assertions, and portable schema-catalog assertions were deleted here. This repo keeps handler parity, adapter conversion, params, legacy envelope helper tests, local security/sandbox config defaults, supervisor classification tests, and app-side persistence/REST wiring tests. - Still migrate to tinychannels: any remaining backend-agnostic assertions
from
controllers/ops_tests.rs, rewritten against a mockChannelBackend, plus the already-mirrored suites listed in Step 1. - Stay here: all
providers/*_tests.rs(telegram 148, web 93, imessage 58, discord 55+23, email 50, lark 42, whatsapp 41+29, irc 37, mattermost 33, signal 32, presentation 29, linq 23, qq 11),bus_tests.rs,routes_tests.rs,runtime/dispatch_tests.rs,runtime/startup_tests.rs, and the harness-driventests/integration files (prompt, telegram/discord integration, runtime tool calls, health, identity). - The REST-wiring halves of
ops_tests.rsbecome the test bed forOpenHumanChannelBackend(Step 2).
Step 5 — Provider extraction ladder (later, tracks crate Phase 6)
Move provider wire code into tinychannels only when its dependencies reduce to crate traits:
- Now portable (no cross-module imports): email, irc, yuanbao, cli, imessage, mattermost, qq, dingtalk, presentation.
- After a configured-HTTP-client trait (replaces
config::build_runtime_proxy_client/apply_runtime_proxy_to_builder): discord, slack, whatsapp, lark, signal. - After approval, voice/STT, pairing, and conversation-memory traits:
telegram (imports
approval,voice::create_stt_provider,memory_conversations,security::pairing). - Never:
webprovider andruntime/dispatch — they are the harness front-end and orchestrator that consume the crate.
Sequencing and risk notes
- Land Step 1 and Step 2 together in one PR if possible: adding the dep while keeping duplicates invites split-brain imports (some call sites on the crate types, some on the local copies — Rust will treat them as different types).
- Session-key changes (Step 3, items 2–3) change conversation identity. Ship them behind the crate's legacy-key canonicalization with a migration test that maps a sample of existing keys to new keys losslessly.
- Positive invariants to preserve (verified in the audit): no secrets are logged in the channels tree, and no blocking calls exist in async provider paths. Any moved code must keep both properties.