Files
openhuman/docs/plans/tinychannels-integration.md

13 KiB
Raw Permalink Blame History

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 tinychannels through the TinyChannels feat/phase-0-hygiene branch while the companion PR lands. The first duplicate-removal pass has landed:

    openhuman-4 file tinychannels file
    src/openhuman/channels/traits.rs re-exports src/traits.rs
    src/openhuman/channels/controllers/definitions.rs re-exports src/controllers/definitions.rs
    src/openhuman/channels/controllers/schemas.rs schema declarations converts from src/controllers/schemas.rs
    src/openhuman/channels/controllers/ops/connect.rs allowlist/key helpers calls src/controllers/credentials.rs
    src/openhuman/channels/controllers/ops/types.rs re-exports src/controllers/types.rs
    src/openhuman/config/schema/channels.rs re-exports provider config from src/config.rs, while keeping OpenHuman-owned security/sandbox config local
    src/openhuman/channels/context.rs key/constants helpers calls/re-exports src/context.rs where type-compatible
    src/openhuman/channels/runtime/supervision.rs in-flight sizing re-exports src/runtime.rs::compute_max_in_flight_messages
    Telegram/Discord text splitting calls src/text.rs chunker with UTF-16 measurement
  • OpenHumanChannelBackend now lives in src/openhuman/channels/controllers/backend.rs and implements tinychannels::ChannelBackend by delegating to the existing channels/controllers/ops/{connect,messaging,discord,telegram}.rs flows.

  • 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 use ChannelManager for definition lookup. The channels.status and channels.test controllers now route through ChannelManager<OpenHumanChannelBackend>, preserving the existing no-log JSON response shapes while using the crate manager for definition lookup and credential validation where applicable. channels.get_default also routes through the manager while preserving the public { active_channel } response, channels.connect and channels.set_default preserve 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 backend raw payload to preserve legacy top-level JSON shapes; channels.send_message, direct channel-bus backend sends, and legacy runtime/proactive/provider-helper Channel::send callers now cross the crate intent bridge as ChannelOutboundIntents and get deterministic idempotency keys before legacy backend/provider calls. channels.disconnect also routes through the manager while preserving the old log string and extra fields such as memory_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 TinyChannels ChannelInboundEnvelope on ChannelMessageReceived events, 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, including scope_id, from the live relay socket through dispatch.

Step 1 — Add the dependency, delete duplicates

  • Landed: Add the tinychannels git dependency to Cargo.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-export ChannelDefinition, ChannelAuthMode, response types.
    • src/openhuman/config/schema/channels.rs: re-export ChannelsConfig and provider config structs. Note tinychannels inlined EmailConfig and YuanbaoConfig, which this repo currently sources from channels::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/SandboxConfig cluster as scope creep; this repo's originals (if used) stay where they are.

Step 2 — Implement ChannelBackend

  • Landed: New OpenHumanChannelBackend in src/openhuman/channels/controllers/backend.rs, delegating each trait method to the existing ops functions:
    • send_messagemessaging.rs::channel_send_message (already composes effective_backend_api_url + jwt::get_session_token + BackendOAuthClient), with send_message_value preserving OpenHuman's arbitrary rich-message JSON for the public RPC controller.
    • connect_channel / disconnect_channelconnect.rs flows plus credentials::ops::{store,remove}_provider_credentials (keyed "channel:<slug>"), with disconnect returning the raw legacy payload for fields such as memory_chunks_deleted.
    • channel_status / test_channel → existing status/test ops plus health::snapshot.
    • Telegram login and Discord link/guild/permission methods → the existing telegram.rs / discord.rs ops.
  • Landed: The current channels.* controller entry points dispatch through ChannelManager<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 the web provider 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):

  1. 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.
  2. Landed for existing helper semantics: Telegram history keys. channels/context.rs now 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 Telegram thread_ts projected as topic_id; conversation persistence records the TinyChannels session key when an event carries an envelope. Relay inbound now preserves the relay-supplied envelope and scope_id through dispatch. Full non-relay session identity switchover still needs provider-sourced scope_id.
  3. 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's scope_id through 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.
  4. 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_message now routes through a crate ChannelOutboundIntent at the ChannelBackend seam while preserving the legacy public RPC response and backend JSON shape. The direct channel bus backend sends and runtime/proactive/provider-helper Channel::send paths use the same crate bridge, carrying deterministic idempotency keys through SendMessage. Provider-specific use of those keys on external wire APIs remains Phase 6/platform work.
  5. Deferred pending product decision: conversation_memory_key intent check. It keys on msg.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.rs schema-catalog assertions, pure allowlist/key helper tests, catalog/request-shape halves of controllers/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 mock ChannelBackend, 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-driven tests/ integration files (prompt, telegram/discord integration, runtime tool calls, health, identity).
  • The REST-wiring halves of ops_tests.rs become the test bed for OpenHumanChannelBackend (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:

  1. Now portable (no cross-module imports): email, irc, yuanbao, cli, imessage, mattermost, qq, dingtalk, presentation.
  2. After a configured-HTTP-client trait (replaces config::build_runtime_proxy_client / apply_runtime_proxy_to_builder): discord, slack, whatsapp, lark, signal.
  3. After approval, voice/STT, pairing, and conversation-memory traits: telegram (imports approval, voice::create_stt_provider, memory_conversations, security::pairing).
  4. Never: web provider and runtime/ 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 23) 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.