Replatform messaging on Chat SDK behind contract v2
joelclaw will use Vercel Chat SDK as the platform-adapter layer for Discord, Telegram, and Slack. Chat SDK replaces platform transport mechanics. It does not replace joelclaw authorization, routing policy, durable queueing, signal suppression, message journal, delivery receipts, health supervision, or error budgets.
The gateway remains the composition root and runs one embedded Chat SDK instance. Agents must not start a second Telegram poller, Slack Socket Mode connection, Discord Gateway listener, or standalone Chat SDK process. Duplicate listeners can steal rather than mirror events and can create double execution or Telegram 409 conflicts.
Context
The hand-rolled gateway accumulated large platform-specific channel implementations and repeated API-drift fixes. Chat SDK provides a common message, command, interaction, reaction, thread, and card surface, but all three selected adapters were beta at the pinned source review. Recent minor releases fixed silent Slack retry loss, broken concurrency limits, and Telegram send failures.
The motivating contract failure was the neat-memory DM. Its producer used priority and channel flags to infer delivery. A low-priority message entered the gateway prompt lane and was suppressed by policy. Producers should state what a message means; one routing table should decide where and how it travels.
Decision
Chat SDK is an adapter, not the gateway
Pin chat and the Discord, Telegram, Slack, and state packages to the same exact release train. The first acting train is 4.34.0.
Keep these joelclaw wrappers around the SDK:
- exact Joel/sender authorization and Reply Grants
command-queueas the only path into the Pi session- Telegram deliver/digest/suppress policy
- operator-relay attention policy
- message journal, platform-message-to-flow index, and fail-open spool
- HATEOAS delivery receipts
- channel health, lifecycle ownership, and bounded error emission
Producers send contract-v2 intents
A rich outbound intent is:
{
contractVersion: 2,
kind: "memory" | "alert" | "digest" | "ask" | "receipt",
content: string,
correlationId: string,
replyTo?: FlowId
}Every send mints a flowId. The confirmed receipt records the route, timestamps, delivery state, platform message ID, and thread ID. The journal maps the platform message ID back to the flowId.
Producers do not choose platforms, lanes, urgency, or formatting. ROUTING_TABLE_V2 owns that mapping. This keeps alert-vs-memory suppression policy out of producer flags.
joelclaw notify send remains compatible for existing callers. Under the acting flag, the gateway maps its legacy priority/source/channel inputs into a contract-v2 kind and delivers through Chat SDK. Legacy --channel and --telegram-only flags are accepted during migration but do not regain routing authority.
Return events correlate by flowId
Platform reactions and replies use the bus contracts:
message/reaction.receivedwith{ flowId, platform, emoji, action, actor, at }message/reply.receivedwith{ flowId, platform, text, actor, at }
Reaction publication is wired in the acting inbound dispatcher. message/reply.received is a versioned schema and reserved bus contract, but it remains schema-only until an acting reply publisher and live canary land. Consumers must not treat reply return as operational yet.
Consumers store the flowId from the delivery receipt and subscribe through ordinary Inngest functions. replyTo: <flowId> anchors a new outbound message to the parent platform message where the adapter supports it. Platform thread objects do not leak into producer code.
Inbound observes on the bus and executes once
Normalized inbound messages, commands, interactions, and reactions are published to the bus for observation and replay. Only the authorized acting wrapper may enqueue work. command-queue remains the sole execution path into Pi.
Shadow comparison happens inside the existing owner process. It mirrors normalized events; it never starts duplicate platform listeners.
Acting-path cutover
The cutover uses an explicit transport handover:
- legacy Telegram and Slack start as the known owners
handoverMessagingTransports(...)stops legacy owners- acting handlers register
startChatSdkRuntime({ legacyTransportsStopped: true })starts SDK ownership- ordered OTEL receipts prove
legacy-active,stopping-legacy,legacy-stopped,starting-sdk, andsdk-active
CHAT_SDK_ACTING_ENABLED=1 selects the acting path. CHAT_SDK_INBOUND_SHADOW_ENABLED=1 keeps comparison taps on during the observation window. The gateway is restarted once under supervision; no second process is launched.
Rollback sets CHAT_SDK_ACTING_ENABLED=0 and restarts the gateway once. In-process emergency rollback stops the SDK before restarting legacy Telegram and Slack. If SDK shutdown cannot be proved, legacy listeners are not restarted because uncertain dual ownership is worse than a visible outage. Journal, diff, OTEL, and unacked queue evidence must be preserved.
Rollback triggers include duplicate payloads, Telegram poll-owner conflicts, unauthorized or self messages reaching command-queue, missing replay, duplicate execution, reaction-flow mismatch, repeated adapter failure, or a confirmed receipt without visible delivery.
Legacy deletion gate
Do not delete hand-rolled channel paths at the moment of cutover. Deletion waits for an accepted acting observation window with:
- clean inbound shadow diffs or explained differences
- notify compatibility proof with one visible delivery and one
flowId - five-message burst proof preserving batching and supersession
- kill/restart ingress proof with exactly-once replay
- reaction-on-exact-outbound-message proof returning the same
flowId - normal daily-use evidence with no rollback trigger
Until that gate passes, the legacy code is rollback equipment, not dead code. After the window is accepted, delete replaced transport mechanics rather than leaving two permanent implementations.
Considered options
- Keep the hand-rolled channels — rejected because platform drift and duplicated transport mechanics remain joelclaw’s maintenance burden.
- Replace the whole gateway with Chat SDK — rejected because the SDK does not own joelclaw authorization, durable execution, routing policy, journaling, health, or receipts.
- Run Chat SDK as a second service/listener during shadow — rejected because Telegram and Slack listeners are ownership paths, not broadcast mirrors.
- Freeze the old notify contract — rejected because reactions, replies, cards, and stable correlation require a richer intent and receipt model.
- Adopt Chat SDK behind joelclaw policy wrappers with supervised handover — accepted.
Consequences
- Platform API drift should land in pinned SDK upgrades and adapter tests instead of new hand-written transports.
- Every SDK upgrade requires changelog review and renewed platform canaries before acting deployment.
- Existing notify callers keep working, but new rich producers should use contract-v2 kinds and
flowIdreceipts. - iMessage remains hand-rolled because Chat SDK has no adapter for it.
- Discord identity and adapter support exist, but live ownership still requires its own canary and explicit enablement.
- Contract schemas and routing live in
packages/message-contract; gateway side effects live inpackages/gateway.
Implementation and verification
Primary paths:
packages/message-contract/packages/gateway/src/chat-sdk/packages/gateway/src/chat-sdk-inbound/packages/gateway/src/channels/redis.tspackages/gateway/src/daemon.ts.brain/projects/messaging-stabilization/run-shadow-window-cutover.svx
Required preflight skills: gateway, telegram, system-architecture, effect, inngest-events, and o11y-logging.
Before legacy deletion, verify the focused Chat SDK/contract suite, gateway typecheck, repository typecheck, targeted lint, ordered handover telemetry, no Telegram conflict, exactly one Slack socket, queue replay, and reaction flowId correlation. The exact supervised commands and rollback sequence live in the cutover step linked above.
Status receipt: 2026-07-16
Telegram and Slack transport ownership were flipped to the Chat SDK acting path with ordered handover telemetry and a live notify send compatibility delivery. Shadow taps remain enabled for the accepted observation window. Legacy deletion is not authorized yet.