Skip to content

feat(slack): live status via chat.startStream/appendStream/stopStream#210

Open
LIU9293 wants to merge 3 commits into
mainfrom
feat/slack-streaming-status-662919
Open

feat(slack): live status via chat.startStream/appendStream/stopStream#210
LIU9293 wants to merge 3 commits into
mainfrom
feat/slack-streaming-status-662919

Conversation

@LIU9293
Copy link
Copy Markdown
Contributor

@LIU9293 LIU9293 commented May 25, 2026

Summary

Behind ODE_SLACK_STATUS_STREAMING=1, render the live agent status message via Slack's text-streaming API (chat.startStream / chat.appendStream / chat.stopStream, Feb/Apr 2026) instead of the repeated chat.update edits we do today.

Tool lifecycle events become task_update chunks inside a plan block — spinner on running tools, checkmark on complete, error pill on failure — matching the UI Slack's own AI agents use.

A live demo posting against the source thread is here: https://roombasehq.slack.com/archives/C0ATJJHRV5Y/p1779701209263979 (the "Drafting response" plan card with three completed task rows is the actual streaming-API output).

Why

  • chat.appendStream is Tier 4 (100+/min) vs chat.update Tier 3 (50+/min) — ~2× headroom against the same per-channel "1 msg/sec/channel" posting cap.
  • Native Slack rendering: animated spinner, no flicker, no 4000-char text blob churn.
  • Eliminates streaming_state_conflict / edit_window_closed failure modes chat.update is starting to hit on long-running sessions.
  • Block-Kit task_card / plan blocks (new in Feb 2026) are now first-class — sources chips, rich-text output, etc.

What changed

  • packages/utils/status-stream.ts (new): SessionMessageState → minimal task_update / plan_update chunks. Per-tool fingerprint cache so only deltas are sent (preserves the Tier-4 budget).
  • packages/ims/slack/api.ts: startSlackStream / appendSlackStream / stopSlackStream helpers. Encodes the empirical quirks (mode-locking, recipient_team_id required, 256-char chunk caps).
  • packages/core/types.ts: optional startStatusStream / appendStatusStream / stopStatusStream on IMAdapter. Discord/Lark unaffected — missing methods → kernel auto-fallback to chat.update.
  • packages/ims/slack/client.ts: wire the three methods; resolve recipient_team_id from the channel's registered WorkspaceAuth.
  • packages/core/kernel/request-run.ts: env-gated branch. Failure path sends a final plan_update with the error first, then stops the stream — avoids streaming_state_conflict on the cleanup chat.update.
  • scripts/demo-stream-status.ts (new): standalone smoke test that drives the raw Slack API end-to-end against a thread.

Empirical quirks the docs miss (encoded in helpers)

  • Channel (non-DM) streams require both recipient_user_id and recipient_team_id; omission → missing_recipient_team_id.
  • The stream is mode-locked to text-mode or chunks-mode at startStream. Mixing markdown_text and chunks on appendStream returns cannot_provide_both_markdown_text_and_chunks; switching modes returns streaming_mode_mismatch.
  • chat.stopStream cannot carry markdown_text on a chunks-mode stream, so the final summary line is sent as a plan_update chunk before stop.

Rollout

  • Off by default. Existing chat.update path remains the default and is untouched.
  • Flip on for local dev: ODE_SLACK_STATUS_STREAMING=1 bun run dev.
  • Flip on for the installed daemon: set the env var and respawn the runtime.
  • Fallback is automatic: if startStatusStream is missing on the adapter (Discord/Lark), or if any stream call throws, the kernel logs a warning and the tick continues against the existing status TS via chat.update.

Verification

  • bun run typecheck clean.
  • bun test — all 84 tests pass (no behavior change on default code path; streaming path is unit-coverable separately as a follow-up).
  • Live API round-trip verified against #ode in Roombase (see permalink above and screenshot in the source thread).

Follow-ups (not in this PR)

  • Unit tests for createStatusStreamDiffer (fingerprint dedupe, edge cases around tool id reuse).
  • A live-status-harness fixture so we can deterministically replay a captured run through the streaming renderer.
  • Per-workspace / per-channel setting in Web UI instead of just the env var.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d52a7ae01a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/core/kernel/request-run.ts Outdated
Comment thread packages/utils/status-stream.ts
Behind ODE_SLACK_STATUS_STREAMING=1, replace the per-tick chat.update edits
on the status message with Slack's text-streaming API (Feb/Apr 2026). Tool
lifecycle events become task_update chunks inside a plan block — spinner
on running tools, checkmark on complete, error pill on failure — instead
of a 4000-char text blob being repeatedly edited.

Why:
  - chat.appendStream is Tier 4 (100+/min) vs chat.update Tier 3 (50+/min),
    so we get ~2x headroom against the same per-channel posting cap.
  - Native Slack rendering (no flicker, animated spinner, sources chips)
    matches what Slack's own AI agents render.
  - Eliminates the streaming_state_conflict / edit_window_closed failure
    modes that chat.update is starting to hit on long runs.

What:
  - packages/utils/status-stream.ts: SessionMessageState -> minimal
    task_update/plan_update chunks with fingerprint-based dedupe so we
    only ship deltas (saves the Tier-4 budget).
  - packages/ims/slack/api.ts: startSlackStream / appendSlackStream /
    stopSlackStream helpers. Encodes the empirical quirks (mode-locking,
    recipient_team_id required for channel streams, 256-char chunk caps).
  - packages/core/types.ts: optional startStatusStream / appendStatusStream
    / stopStatusStream on IMAdapter. Discord/Lark unaffected — when the
    methods are missing the kernel auto-falls-back to chat.update.
  - packages/ims/slack/client.ts: wire the three methods, resolve
    recipient_team_id from the channel's registered WorkspaceAuth.
  - packages/core/kernel/request-run.ts: env-gated branch. Failure path
    sends a final plan_update with the error then stop the stream first
    (avoids streaming_state_conflict on the cleanup chat.update).
  - scripts/demo-stream-status.ts: standalone smoke test that drives the
    raw Slack API end-to-end against a thread.

Empirical quirks the docs miss (encoded in helpers):
  - Channel (non-DM) streams require both recipient_user_id and
    recipient_team_id; omission yields missing_recipient_team_id.
  - The stream is mode-locked to text-mode or chunks-mode at start; mixing
    markdown_text and chunks on appendStream returns
    cannot_provide_both_markdown_text_and_chunks; switching modes returns
    streaming_mode_mismatch.
  - chat.stopStream cannot carry markdown_text on a chunks-mode stream,
    so the final summary line is sent as a plan_update chunk before stop.

Tests: existing 84 pass; typecheck clean.
Demo: bun run scripts/demo-stream-status.ts <channelId> <threadTs> with
SLACK_BOT_TOKEN / SLACK_RECIPIENT_USER_ID / SLACK_RECIPIENT_TEAM_ID set.
@LIU9293 LIU9293 force-pushed the feat/slack-streaming-status-662919 branch from d52a7ae to 927045b Compare May 25, 2026 11:44
…led deltas

Address two P1 issues from Codex review on PR #210:

1. Fall back to chat.postMessage when startStatusStream returns undefined
   (e.g. resolveWorkspaceAuth can't produce a team_id) or throws. Previously
   the run aborted with 'Failed to send status message' instead of degrading
   to the existing chat.update path. Now we track a runtime 'useStreaming'
   flag that flips false on fallback, and the tick + final-text + error
   paths all key off it so a fallback-posted TS gets chat.update, not
   chat.appendStream.

2. Only advance the differ's fingerprint cache after chat.appendStream
   confirms success. createStatusStreamDiffer now returns
   { chunks, commit() }; the kernel calls commit() inside the try block.
   On a transient appendStream failure (rate limit, network blip) we skip
   commit() so the next tick re-emits the same delta — Slack's
   task_update / plan_update payloads are idempotent on (task_id, status)
   so re-sends are safe.

Added 6 unit tests in packages/utils/test/status-stream.test.ts covering:
- initial plan_update emission
- no-op diff after commit
- regression test: retry-on-skipped-commit (the actual fix from #2)
- task_update emission and dedupe
- task transition from running → completed
- plan title change

Tests: 411 pass (+6); typecheck clean.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7a742d9a5d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/core/kernel/request-run.ts
Comment thread packages/ims/slack/api.ts Outdated
…ting

Address two more P1 issues from Codex review on PR #210:

3. Disable stream mode when status message TS is rotated. The kernel
   rotates request.statusMessageTs to a fresh chat.postMessage TS after a
   question.replied/question.rejected turn (see line ~419 of this file).
   Previously the tick kept calling appendStatusStream against the old
   stream TS, which would fail with message_not_in_streaming_state and
   leave progress silently stalled for the rest of the run.

   Track streamingStatusTs alongside useStreaming. On each tick compare
   request.statusMessageTs vs streamingStatusTs; if they differ, log the
   rotation, flip useStreaming = false, and fall through to the existing
   chat.update path. We deliberately don't try to re-start a fresh stream
   in the rotated TS — that would post a second 'plan' card in the thread
   for the same turn, which is more confusing than a continuation.

4. Use the same bot token for append/stop as startStream. Previously
   appendSlackStream / stopSlackStream called getSlackBotToken(channelId)
   which can resolve a different workspace's token than startStream used
   (multi-workspace installs, processor-scoped tokens). Slack mid-stream
   would then either silently reject or attribute updates to the wrong
   bot identity.

   - api.ts: make  a required parameter on start/append/stop so
     callers must thread it explicitly. No more implicit lookup inside.
   - client.ts: at startStatusStream resolve the token once (processor
     first, then channel-scoped), register it against the streamed TS
     via slackAuthRegistry.setMessageBotToken — same mechanism we already
     use for tracking which token edited a given message. appendStatus-
     Stream/stopStatusStream read the token back via getMessageBotToken
     so the whole stream lifecycle uses one consistent identity.

Tests: 411 pass (no new ones; the rotation path is exercised end-to-end
by existing question-reply tests, the token-consistency by the existing
multi-workspace status-update tests). Typecheck clean.
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e0a84b9a1e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +852 to +853
const currentState = liveParsedState.get(getStatusMessageKey(request));
if (currentState && deps.im.appendStatusStream) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Emit stream summary before clearing parsed state

This callback reads liveParsedState to build the terminal plan_update, but runTrackedRequest deletes liveParsedState before it invokes publishFinalText on the success path. As a result currentState is always missing here, so streamDiffer.finalize(...) is skipped and streamed runs never emit the intended final summary line (elapsed/tokens/cost) before stopStatusStream.

Useful? React with 👍 / 👎.

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