Skip to content

Fix Stream Chat scheduled delivery targets#9

Open
steookk wants to merge 4 commits into
mainfrom
streamchat-cid-scheduled-delivery
Open

Fix Stream Chat scheduled delivery targets#9
steookk wants to merge 4 commits into
mainfrom
streamchat-cid-scheduled-delivery

Conversation

@steookk

@steookk steookk commented May 8, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Store Stream Chat inbound targets as raw CIDs (type:id) so scheduled/tool deliveries reuse the same channel identity.
  • Add strict Stream Chat target resolver hooks for OpenClaw's message-tool and cron outbound paths.
  • Force Stream Chat inbound replies through OpenClaw's automatic source-reply path so the placeholder message receives streamed partials.
  • Retry authoritative terminal Stream Chat message updates so rate limits do not leave streamed replies visibly truncated.
  • Reduce preview update frequency to chunks 1 and 5, then every 35 chunks, to stay farther from Stream Chat update-message limits.
  • Parse CIDs only at the Stream SDK boundary and clear empty placeholders when a tool-delivered message provides the visible reply.

Why this fixes scheduled messages

The first working change was adding plugin-side target resolution for OpenClaw's message tool / cron outbound path. Normal inbound replies could work because they used the live reply pipeline, but scheduled/tool messages take a different route:

agent/tool call -> OpenClaw outbound target resolver -> plugin outbound.sendText

Before this change, OpenClaw asked the Stream Chat plugin whether a target such as ai-test-channel belonged to Stream Chat, but the plugin did not expose the newer messaging/outbound target resolver hooks that current OpenClaw expects. The delivery failed before sendText could do useful work with errors like:

Unknown target "ai-test-channel" for Stream Chat

This PR gives OpenClaw that adapter surface through messaging.targetResolver, messaging.inferTargetChatType, and outbound.resolveTarget. The simplified model now passes Stream's canonical raw CID, such as messaging:ai-test-channel, through the gateway and scheduled job. When delivery fires, the plugin splits the CID into channelType and channelId only when calling the Stream SDK.

Why this fixes streamed truncation (#6)

The old streamed-token issue still reproduced against openclaw-user-1. In a real chat, the fourth response stopped at Advantages - Reduces while the Stream Chat indicator cleared. Container logs showed Stream Chat rate limits on both partial and final message updates:

partialUpdate failed: ... Too many requests
Final update failed: ... Too many requests

That localizes the loss to the plugin-to-Stream delivery layer. OpenClaw had produced the response, but the final authoritative partialUpdateMessage({ generating: false, text: fullText }) was not retried. If Stream rejected that final update with 429, the visible message stayed at the last accepted partial update.

This PR retries terminal message updates on retryable Stream Chat failures (429, 5xx, SDK code 9, and the observed Too many requests error). Partial streaming updates remain best-effort, but completion, error, empty-placeholder fallback, and force-stop terminal updates now get retry/backoff. The preview cadence is also reduced from chunks 1/3/5/7 plus every 15 chunks to chunks 1/5 plus every 35 chunks, keeping live typing while making long fast generations much less likely to exceed Stream's update-message rate limits. The generating indicator is sent fire-and-forget so a delayed indicator retry cannot enqueue a stale generating: true partial update after finalization.

Why this restores visible source streaming

After the scheduled-delivery work, user-1 chat-client showed only a final bot message. The cause was OpenClaw's group/channel default route falling into message_tool_only, which made the model send the answer through the message tool as a separate final outbound message. That bypassed the Stream Chat placeholder and onPartialReply source-reply path.

This PR now forces sourceReplyDeliveryMode: "automatic" for Stream Chat inbound replies. Scheduled/tool outbound delivery still uses the plugin target resolver and outbound.sendText; ordinary inbound replies use the placeholder streaming path. The handler also de-duplicates cumulative partial snapshots against block/final dispatcher payloads, and can replace stale partial text if OpenClaw provides an authoritative non-prefix final payload.

The explicit plugin reply option takes priority over messages.groupChat.visibleReplies because OpenClaw resolves an explicitly requested sourceReplyDeliveryMode before consulting config. That means the backend config change to generate visibleReplies: "automatic" is still useful defensively for newly provisioned containers, older plugin code, other OpenClaw surfaces, and any Stream Chat path that does not pass explicit reply options. For this plugin's normal inbound reply path, however, placeholder streaming is treated as a plugin invariant, so the plugin pins automatic source replies directly.

replaceText() is included for the rare case where OpenClaw's final dispatcher payload is not a prefix extension of the streamed partial text. Normally partials are cumulative snapshots and final text is identical. If OpenClaw or a provider later normalizes, filters, or repairs the final output differently from the preview stream, appending would create partial + final garbage and ignoring the final would leave stale preview text. In that divergence case, the final payload is treated as authoritative and replaces the accumulated preview text before the final Stream Chat update.

The duplicate-safe handling was not fixing an already-observed duplicate append in the old plugin behavior. Previously, text blocks from deliver(...) were ignored because onPartialReply was assumed to be the only visible text path. This PR opens a fallback path where deliver(...) text can contribute if partial streaming is absent or incomplete; the duplicate guard is what makes that fallback safe when normal cumulative partial streaming is active and deliver(final) repeats the same text.

Verification

  • npm run type-check
  • npx tsx scripts/test-streaming-final-retry.ts
  • npx tsx scripts/test-channel-streaming-delivery.ts
  • Native OpenClaw direct send to messaging:ai-test-channel delivered successfully.
  • Bare target ai-test-channel is rejected with the Stream CID hint.
  • Native chat-client prompt asking the bot to schedule a message to the same Stream Chat channel delivered CID_SCHEDULED_FROM_CONTEXT_OK.
  • openclaw cron list --json showed no remaining jobs.
  • User-1 chat-client reproduced issue [bug] Streamed tokens lost – parts of AI message missing #6 before the fix; logs showed Stream Chat 429s on partial/final updates.
  • User-1 chat-client E2E after restoring automatic source replies streamed visible partial text in multiple batches before the final message id line, using the same placeholder message id throughout.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant