Skip to content

fix(slack): treat Slack Connect finalize errors as benign in stopSlackStream#70370

Merged
steipete merged 3 commits into
openclaw:mainfrom
mvanhorn:fix/70295-slack-stream-finalize-fallback
Apr 23, 2026
Merged

fix(slack): treat Slack Connect finalize errors as benign in stopSlackStream#70370
steipete merged 3 commits into
openclaw:mainfrom
mvanhorn:fix/70295-slack-stream-finalize-fallback

Conversation

@mvanhorn

Copy link
Copy Markdown
Contributor

Summary

  • Problem: When Slack native streaming is enabled and the recipient is a Slack Connect user, chat.stopStream fails with An API error occurred: user_not_found. OpenClaw logs slack-stream: failed to stop stream: ... via runtime.error in dispatch, which makes every Slack Connect reply look like an error even though the text delivered via chat.appendStream is already visible to the user.
  • Why it matters: On any workspace that uses Slack Connect / shared channels, native streaming silently looks broken at the dispatch layer. Operators see a stream of red in the gateway log that is caused by the Slack Connect recipient shape, not a dropped reply.
  • What changed: stopSlackStream in extensions/slack/src/streaming.ts now catches session.streamer.stop(...) errors. When the Slack error code is one of user_not_found, team_not_found, or missing_recipient_user_id (all Slack Connect / recipient-resolution cases), swallow the error, keep the session marked as stopped, and log through the existing logVerbose path explaining that appended text is still visible. Any other Slack error still propagates so the caller's existing runtime.error(...) log is preserved.
  • What did NOT change (scope boundary): No behavior change for DMs in the host workspace, thread replies to non-Connect users, or non-streaming delivery paths. The dispatch call site in extensions/slack/src/monitor/message-handler/dispatch.ts is unchanged; this PR stays inside the streaming.ts boundary. No other plugin, core, or SDK changes.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Root Cause (if applicable)

  • Root cause: stopSlackStream in extensions/slack/src/streaming.ts awaited session.streamer.stop(...) without any error handling. The Slack Connect recipient_user_id / team_id path is not resolvable at finalize time, so Slack returns user_not_found (or sibling codes) on chat.stopStream. The error bubbled up through the outer try/catch in dispatch.ts and was logged via runtime.error as if the reply itself had failed.
  • Missing detection / guardrail: no classification of finalize errors that are "visible text already delivered" vs "stream actually failed". The caller cannot distinguish the two.
  • Contributing context (if known): the issue triage (@rafiki270 on [Bug]: Replies can fail with native Slack streaming enabled (user_not_found during stream finalize) when replying to in channel message from Slack Connect user #70295) identified extensions/slack/src/streaming.ts stopSlackStream() as the missing-error-handling site and the finalize failure mode as Slack-Connect-specific.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: extensions/slack/src/streaming.test.ts (new)
  • Scenario the test should lock in: session.streamer.stop() throwing each of the three benign Slack Connect error shapes -> stopSlackStream resolves without throwing and marks the session stopped. An unexpected Slack error code (not_authed) AND a non-Slack-shaped error (new Error("socket reset")) still re-throw so the caller's log path stays intact. Duplicate-stop on an already-stopped session remains a no-op.
  • Why this is the smallest reliable guardrail: stopSlackStream is the seam; the dispatch call site is unchanged.
  • Existing test that already covers this (if any): none - streaming.ts had no direct unit coverage.
  • If no new test is added, why not: N/A (added).

User-visible / Behavior Changes

  • Slack Connect replies with native streaming enabled no longer surface a runtime.error("slack-stream: failed to stop stream: user_not_found") line when the stream text was actually delivered.
  • Log output shifts these finalize failures to logVerbose with a clear note that appended text remains visible and the stream is treated as stopped.

Diagram (if applicable)

N/A

Security Impact (required)

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No (we do not issue follow-on calls on finalize failure; the append() call's text is already at Slack).
  • Command/tool execution surface changed? No
  • Data access scope changed? No
  • If any Yes, explain risk + mitigation: N/A

Repro + Verification

Environment

  • OS: macOS 15
  • Runtime/container: Node 22, pnpm
  • Model/provider: any (reply is generated upstream; this is the delivery path)
  • Integration/channel (if any): Slack, native streaming enabled
  • Relevant config (redacted):
    channels.slack.streaming.mode = "partial"
    channels.slack.streaming.nativeTransport = true
    channels.slack.replyToMode = "first"
    

Steps

  1. Configure OpenClaw Slack with native streaming as above.
  2. Trigger a reply in a shared channel/thread that includes a Slack Connect user.

Expected

Reply appears in thread (as it already does via append()) and no slack-stream: failed to stop stream error line appears at the dispatch layer for the user_not_found / team_not_found / missing_recipient_user_id cases.

Actual (before this PR)

[slack] slack-stream: failed to stop stream: Error: An API error occurred: user_not_found at runtime.error level, even when the reply text is visible.

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Local checks before push:

  • pnpm test extensions/slack/src/streaming.test.ts: 1 file, 6 tests pass.
  • pnpm test extensions/slack: 90 files, 736 tests pass.
  • pnpm check:changed --staged (pre-commit): extension lane ran 89 files, 734 tests, all pass.
  • pnpm format:check, pnpm lint on touched files: clean.

Human Verification (required)

  • Verified scenarios (via unit tests mocking ChatStreamer.stop):
    • user_not_found throw -> stopSlackStream resolves, session marked stopped.
    • team_not_found throw -> same.
    • missing_recipient_user_id throw -> same.
    • Unexpected Slack code (not_authed) -> re-thrown with the original message; session still marked stopped to prevent retry loops.
    • Non-Slack error (socket reset) -> re-thrown unchanged.
    • Duplicate stop on an already-stopped session -> no-op, streamer.stop not called.
    • Full Slack extension lane still green (736 tests).
  • Edge cases checked: preserving session.stopped = true on both throw paths so a retry would not re-enter streamer.stop. The error classification reads from err.data.error (shape used by @slack/web-api errors) with a message-string fallback for custom Error subclasses.
  • What I did not verify: I do not have a live Slack Connect workspace to reproduce against. The classification is based on the error strings reported in [Bug]: Replies can fail with native Slack streaming enabled (user_not_found during stream finalize) when replying to in channel message from Slack Connect user #70295 and the Slack API docs. If Slack adds / renames finalize error codes for Connect in the future the allowlist needs to follow.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? No
  • Migration needed? No
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk: The allowlist of benign finalize codes hides a real delivery failure if Slack later reuses one of these codes for a case where appended text did NOT reach the user.
    • Mitigation: the three allowlisted codes are recipient-resolution failures that occur at finalize time. chat.appendStream uses the same recipient context, so a user_not_found at stop generally means start/append also hit the same validation path and either already failed (caller sees that as a throw) or succeeded (text is visible). If Slack changes the semantics, the allowlist is a single file and easy to adjust.
  • Risk: Non-streaming delivery is accidentally affected.
    • Mitigation: the change is scoped to stopSlackStream inside extensions/slack/src/streaming.ts. Non-streaming delivery paths do not call it.

@greptile-apps

greptile-apps Bot commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds targeted error handling in stopSlackStream to swallow three known Slack Connect finalize error codes (user_not_found, team_not_found, missing_recipient_user_id) that arise when chat.stopStream cannot resolve a Slack Connect recipient, even though the streamed text is already visible to users. The change is well-scoped to streaming.ts, includes a full unit-test suite covering benign, non-benign, and non-Slack error shapes, and correctly marks session.stopped = true before the try/catch to prevent retry loops on any throw path.

Confidence Score: 5/5

Safe to merge — the change is narrowly scoped to the finalize path, all error paths are covered by tests, and no behavior changes for non-Slack-Connect users.

All findings are P2 or lower. The implementation correctly guards against retry loops, the allowlist is documented and easy to extend, and the test suite covers every claimed scenario including unexpected-error re-throw and duplicate-stop no-op.

No files require special attention.

Reviews (1): Last reviewed commit: "fix(slack): treat Slack Connect finalize..." | Re-trigger Greptile

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

Copy link
Copy Markdown
Contributor

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: 9d5aa5eed1

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +218 to +222
if (session.delivered) {
logVerbose(
`slack-stream: finalize rejected by Slack (${code}); prior appends delivered, treating stream as stopped`,
);
return;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Don't swallow finalize errors while buffered tail is unsent

This branch treats benign chat.stopStream errors as success whenever session.delivered is true, but delivered only means at least one earlier flush succeeded, not that the current ChatStreamer buffer is empty. A common case is: one chunk flushes (so delivered=true), then a final sub-buffer_size tail stays buffered, and stop() fails with user_not_found/team_not_found; returning here drops that tail and silently truncates the reply. Before this change the failure was surfaced via runtime.error, but now the truncation can be hidden behind verbose logging.

Useful? React with 👍 / 👎.

@mvanhorn

Copy link
Copy Markdown
Contributor Author

Pushed 9d5aa5e addressing a silent-data-loss cascade the first revision would have introduced for short Slack Connect replies.

The problem: @slack/web-api's ChatStreamer.append() buffers locally until buffer_size (256 by default). Replies under that threshold never issue chat.startStream - the first network call is the internal chat.startStream inside streamer.stop(). On Slack Connect recipients that call throws user_not_found, and the prior fix swallowed it. Dispatch thought the turn was delivered; the user saw 'done' reaction but no message.

SlackStreamSession now tracks delivered (flipped true only when append/stop return a non-null SDK response, i.e. actually hit Slack) and pendingText. stopSlackStream:

  • swallows the benign finalize error when delivered=true (prior append flushed text that is still visible in Slack - same as the first revision for the normal-length reply case)
  • throws a new SlackStreamNotDeliveredError carrying pendingText when delivered=false (nothing reached Slack)

dispatch.ts catches that error and posts pendingText via chat.postMessage (rename-bound to dodge the unicorn/postMessage-target-origin lint rule), and flips streamFallbackDelivered so anyReplyDelivered stays correct. Other errors still propagate to runtime.error unchanged.

11 new streaming tests (up from 6), full Slack extension lane passes (739 tests across 89 files).

@mvanhorn
mvanhorn force-pushed the fix/70295-slack-stream-finalize-fallback branch from 9d5aa5e to 995019c Compare April 23, 2026 00:25

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

Copy link
Copy Markdown
Contributor

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: 995019c1f9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically 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 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +886 to +889
await postChatMessage({
channel: finalStream.channel,
thread_ts: finalStream.threadTs,
text: err.pendingText,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Avoid reposting buffered text after append fallback

When appendSlackStream fails, deliverWithStreaming already falls back to deliverNormally for that payload, but finalization later catches SlackStreamNotDeliveredError and always posts err.pendingText. Because pendingText is accumulated before streamer.append succeeds, the failed chunk is included and gets sent again in this fallback path. In Slack Connect recipient-error scenarios where append and stop both fail before any flush, users can receive duplicated content (one normal fallback message plus another message containing the same chunk).

Useful? React with 👍 / 👎.

@steipete
steipete force-pushed the fix/70295-slack-stream-finalize-fallback branch from 995019c to cd950fb Compare April 23, 2026 00:58
@steipete
steipete force-pushed the fix/70295-slack-stream-finalize-fallback branch from cd950fb to 6d5fe17 Compare April 23, 2026 01:02

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

Copy link
Copy Markdown
Contributor

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: 6d5fe17498

ℹ️ 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 +448 to +452
await postChatMessage({
channel: session.channel,
thread_ts: session.threadTs,
text: fallbackText,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Route pending-stream fallback through chunked sender

deliverPendingStreamFallback sends err.pendingText via one raw chat.postMessage call, which bypasses the normal deliverNormally/deliverReplies path that chunks long replies. In Slack Connect cases where appendSlackStream raises SlackStreamNotDeliveredError with a large pending chunk, this single post can fail (for example due to message-length limits), and deliverWithStreaming then rethrows instead of using the chunked fallback, so the unsent text is dropped. Before this change, append failures always fell back through the normal sender and did not have this regression.

Useful? React with 👍 / 👎.

@steipete
steipete force-pushed the fix/70295-slack-stream-finalize-fallback branch from 6d5fe17 to a74fb6a Compare April 23, 2026 01:12

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

Copy link
Copy Markdown
Contributor

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: a74fb6a24b

ℹ️ 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 +448 to +452
await postChatMessage({
channel: session.channel,
thread_ts: session.threadTs,
text: fallbackText,
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Route stream fallback through chunked reply sender

deliverPendingStreamFallback sends err.pendingText with one raw chat.postMessage call instead of the normal deliverReplies/sendMessageSlack path that enforces chunking and Slack text limits. Because pendingText is built by repeated session.pendingText += text appends in streaming.ts, this fallback can contain multi-chunk buffered content; if it exceeds Slack limits, this single post fails and the buffered reply is not reliably recovered (append fallback rethrows, finalize fallback only logs). Using the standard reply sender here would avoid dropping long Slack Connect replies.

Useful? React with 👍 / 👎.

@openclaw-barnacle openclaw-barnacle Bot added the scripts Repository scripts label Apr 23, 2026
mvanhorn and others added 3 commits April 23, 2026 02:31
…kStream

When Slack's chat.stopStream fails with user_not_found (Slack Connect DM
recipients), team_not_found (cross-workspace shared channels), or
missing_recipient_user_id (DM closed mid-stream), the text already
delivered via append() is still visible to the user. Swallow those
specific codes and mark the session stopped rather than surfacing a
spurious 'slack-stream: failed to stop stream' error in dispatch. Other
Slack API errors still propagate.

Fixes openclaw#70295
…pre-flush

Address adversarial review finding on openclaw#70295: the prior swallow-on-benign
fix silently dropped short replies to Slack Connect users. The SDK's
ChatStreamer buffers text locally until buffer_size (256 default), so
short replies never trigger chat.startStream via append(). streamer.stop()
then issues startStream internally; on Slack Connect recipients this
throws user_not_found. With the prior fix that error was swallowed and
the dispatcher marked the turn delivered - user saw 'done' reaction but
no message.

SlackStreamSession now tracks delivered (true once any Slack API call
returned a response) and pendingText (accumulation of every append +
final-stop text). stopSlackStream:
  - swallows the benign code when delivered=true (prior append flushed;
    text is visible; same behavior as before)
  - throws a new SlackStreamNotDeliveredError carrying pendingText when
    delivered=false (nothing reached Slack)

dispatch.ts catches SlackStreamNotDeliveredError and posts pendingText
via a rename-bound chat.postMessage (to dodge the unicorn lint rule),
and flips streamFallbackDelivered so anyReplyDelivered stays correct.

Fixes openclaw#70295
@steipete
steipete force-pushed the fix/70295-slack-stream-finalize-fallback branch from 720a73e to 9dbefb7 Compare April 23, 2026 01:32
@openclaw-barnacle openclaw-barnacle Bot removed the scripts Repository scripts label Apr 23, 2026

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

Copy link
Copy Markdown
Contributor

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: 720a73e377

ℹ️ 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 +448 to +451
await postChatMessage({
channel: session.channel,
thread_ts: session.threadTs,
text: fallbackText,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1 Badge Route stream fallback through chunked Slack sender

deliverPendingStreamFallback sends err.pendingText in a single chat.postMessage call, but this bypasses the normal deliverReplies/sendMessageSlack chunking path used elsewhere in this file. Because pendingText can accumulate multiple buffered chunks before a flush error, this fallback can exceed Slack text limits; when that happens, deliverPendingStreamFallback returns false and the buffered reply is either rethrown from deliverWithStreaming or only logged during finalize, so users can lose the unsent tail instead of getting a chunked fallback delivery.

Useful? React with 👍 / 👎.

@steipete
steipete merged commit 2e90a22 into openclaw:main Apr 23, 2026
73 checks passed
medikoo pushed a commit to medikoo/openclaw that referenced this pull request Apr 24, 2026
steipete pushed a commit to martingarramon/openclaw that referenced this pull request Apr 24, 2026
deliverPendingStreamFallback was calling chat.postMessage directly for
err.pendingText, which bypasses the chunked reply path used everywhere
else. For Slack Connect cases where appendSlackStream throws
SlackStreamNotDeliveredError with a large pending buffer, the single
raw post could fail (msg_too_long) and drop the unsent tail.

Two changes:

1. deliverPendingStreamFallback now routes through deliverReplies so
   long pendingText is chunked by the normal sender and the fallback
   honors the configured replyToMode / identity.

2. The non-benign streaming-error branch in deliverWithStreaming now
   clears the session via markSlackStreamFallbackDelivered before
   falling back to deliverNormally. Without this, pendingText stays
   populated and the post-loop finalize (stopSlackStream →
   SlackStreamNotDeliveredError → fallback) re-posts the same chunk
   that deliverNormally already sent.

Addresses the three Codex P1 findings on openclaw#70370 about bypassing the
chunked sender, and the related "avoid reposting buffered text after
append fallback" P1 about duplicate delivery. Tests updated to assert
deliverReplies routing (instead of raw postMessage) and a new case
covers the non-benign-error dedup.

Follow-up to openclaw#70370.
steipete added a commit that referenced this pull request Apr 24, 2026
…llow-up to #70370) (#71124)

* fix(slack): route stream-fallback delivery through chunked sender

deliverPendingStreamFallback was calling chat.postMessage directly for
err.pendingText, which bypasses the chunked reply path used everywhere
else. For Slack Connect cases where appendSlackStream throws
SlackStreamNotDeliveredError with a large pending buffer, the single
raw post could fail (msg_too_long) and drop the unsent tail.

Two changes:

1. deliverPendingStreamFallback now routes through deliverReplies so
   long pendingText is chunked by the normal sender and the fallback
   honors the configured replyToMode / identity.

2. The non-benign streaming-error branch in deliverWithStreaming now
   clears the session via markSlackStreamFallbackDelivered before
   falling back to deliverNormally. Without this, pendingText stays
   populated and the post-loop finalize (stopSlackStream →
   SlackStreamNotDeliveredError → fallback) re-posts the same chunk
   that deliverNormally already sent.

Addresses the three Codex P1 findings on #70370 about bypassing the
chunked sender, and the related "avoid reposting buffered text after
append fallback" P1 about duplicate delivery. Tests updated to assert
deliverReplies routing (instead of raw postMessage) and a new case
covers the non-benign-error dedup.

Follow-up to #70370.

* fix(slack): preserve pending buffered text on non-benign stream errors

Address Codex P1 on #71124: `markSlackStreamFallbackDelivered` was
clearing `pendingText` before `deliverNormally` ran, so any earlier
buffered chunk was lost. E.g. chunk A buffered in the SDK, then
appending chunk B throws a generic network error → previous fix
dropped A+B and only sent B via `deliverNormally`, silently truncating
the final reply.

Route the full buffered `pendingText` through
`deliverPendingStreamFallback` with a synthetic
`SlackStreamNotDeliveredError`, then skip `deliverNormally` entirely
(pendingText already contains this payload's text, per
`appendSlackStream` accumulating before throw). If the chunked
fallback fails, fall back to `deliverNormally` so at least the current
payload lands.

Test updated to assert the full pendingText ("first buffered\nsecond
payload") gets routed through the chunked sender, not the
chunk-B-only partial send.

* fix(slack): harden stream fallback docs and chunking test (#71124)

---------

Co-authored-by: Peter Steinberger <[email protected]>
Angfr95 pushed a commit to Angfr95/openclaw that referenced this pull request Apr 25, 2026
…llow-up to openclaw#70370) (openclaw#71124)

* fix(slack): route stream-fallback delivery through chunked sender

deliverPendingStreamFallback was calling chat.postMessage directly for
err.pendingText, which bypasses the chunked reply path used everywhere
else. For Slack Connect cases where appendSlackStream throws
SlackStreamNotDeliveredError with a large pending buffer, the single
raw post could fail (msg_too_long) and drop the unsent tail.

Two changes:

1. deliverPendingStreamFallback now routes through deliverReplies so
   long pendingText is chunked by the normal sender and the fallback
   honors the configured replyToMode / identity.

2. The non-benign streaming-error branch in deliverWithStreaming now
   clears the session via markSlackStreamFallbackDelivered before
   falling back to deliverNormally. Without this, pendingText stays
   populated and the post-loop finalize (stopSlackStream →
   SlackStreamNotDeliveredError → fallback) re-posts the same chunk
   that deliverNormally already sent.

Addresses the three Codex P1 findings on openclaw#70370 about bypassing the
chunked sender, and the related "avoid reposting buffered text after
append fallback" P1 about duplicate delivery. Tests updated to assert
deliverReplies routing (instead of raw postMessage) and a new case
covers the non-benign-error dedup.

Follow-up to openclaw#70370.

* fix(slack): preserve pending buffered text on non-benign stream errors

Address Codex P1 on openclaw#71124: `markSlackStreamFallbackDelivered` was
clearing `pendingText` before `deliverNormally` ran, so any earlier
buffered chunk was lost. E.g. chunk A buffered in the SDK, then
appending chunk B throws a generic network error → previous fix
dropped A+B and only sent B via `deliverNormally`, silently truncating
the final reply.

Route the full buffered `pendingText` through
`deliverPendingStreamFallback` with a synthetic
`SlackStreamNotDeliveredError`, then skip `deliverNormally` entirely
(pendingText already contains this payload's text, per
`appendSlackStream` accumulating before throw). If the chunked
fallback fails, fall back to `deliverNormally` so at least the current
payload lands.

Test updated to assert the full pendingText ("first buffered\nsecond
payload") gets routed through the chunked sender, not the
chunk-B-only partial send.

* fix(slack): harden stream fallback docs and chunking test (openclaw#71124)

---------

Co-authored-by: Peter Steinberger <[email protected]>
@mvanhorn

Copy link
Copy Markdown
Contributor Author

Thanks for landing the benign-error treatment, @steipete.

ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
…llow-up to openclaw#70370) (openclaw#71124)

* fix(slack): route stream-fallback delivery through chunked sender

deliverPendingStreamFallback was calling chat.postMessage directly for
err.pendingText, which bypasses the chunked reply path used everywhere
else. For Slack Connect cases where appendSlackStream throws
SlackStreamNotDeliveredError with a large pending buffer, the single
raw post could fail (msg_too_long) and drop the unsent tail.

Two changes:

1. deliverPendingStreamFallback now routes through deliverReplies so
   long pendingText is chunked by the normal sender and the fallback
   honors the configured replyToMode / identity.

2. The non-benign streaming-error branch in deliverWithStreaming now
   clears the session via markSlackStreamFallbackDelivered before
   falling back to deliverNormally. Without this, pendingText stays
   populated and the post-loop finalize (stopSlackStream →
   SlackStreamNotDeliveredError → fallback) re-posts the same chunk
   that deliverNormally already sent.

Addresses the three Codex P1 findings on openclaw#70370 about bypassing the
chunked sender, and the related "avoid reposting buffered text after
append fallback" P1 about duplicate delivery. Tests updated to assert
deliverReplies routing (instead of raw postMessage) and a new case
covers the non-benign-error dedup.

Follow-up to openclaw#70370.

* fix(slack): preserve pending buffered text on non-benign stream errors

Address Codex P1 on openclaw#71124: `markSlackStreamFallbackDelivered` was
clearing `pendingText` before `deliverNormally` ran, so any earlier
buffered chunk was lost. E.g. chunk A buffered in the SDK, then
appending chunk B throws a generic network error → previous fix
dropped A+B and only sent B via `deliverNormally`, silently truncating
the final reply.

Route the full buffered `pendingText` through
`deliverPendingStreamFallback` with a synthetic
`SlackStreamNotDeliveredError`, then skip `deliverNormally` entirely
(pendingText already contains this payload's text, per
`appendSlackStream` accumulating before throw). If the chunked
fallback fails, fall back to `deliverNormally` so at least the current
payload lands.

Test updated to assert the full pendingText ("first buffered\nsecond
payload") gets routed through the chunked sender, not the
chunk-B-only partial send.

* fix(slack): harden stream fallback docs and chunking test (openclaw#71124)

---------

Co-authored-by: Peter Steinberger <[email protected]>
zhonghe0615 pushed a commit to zhonghe0615/openclaw that referenced this pull request May 7, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
…llow-up to openclaw#70370) (openclaw#71124)

* fix(slack): route stream-fallback delivery through chunked sender

deliverPendingStreamFallback was calling chat.postMessage directly for
err.pendingText, which bypasses the chunked reply path used everywhere
else. For Slack Connect cases where appendSlackStream throws
SlackStreamNotDeliveredError with a large pending buffer, the single
raw post could fail (msg_too_long) and drop the unsent tail.

Two changes:

1. deliverPendingStreamFallback now routes through deliverReplies so
   long pendingText is chunked by the normal sender and the fallback
   honors the configured replyToMode / identity.

2. The non-benign streaming-error branch in deliverWithStreaming now
   clears the session via markSlackStreamFallbackDelivered before
   falling back to deliverNormally. Without this, pendingText stays
   populated and the post-loop finalize (stopSlackStream →
   SlackStreamNotDeliveredError → fallback) re-posts the same chunk
   that deliverNormally already sent.

Addresses the three Codex P1 findings on openclaw#70370 about bypassing the
chunked sender, and the related "avoid reposting buffered text after
append fallback" P1 about duplicate delivery. Tests updated to assert
deliverReplies routing (instead of raw postMessage) and a new case
covers the non-benign-error dedup.

Follow-up to openclaw#70370.

* fix(slack): preserve pending buffered text on non-benign stream errors

Address Codex P1 on openclaw#71124: `markSlackStreamFallbackDelivered` was
clearing `pendingText` before `deliverNormally` ran, so any earlier
buffered chunk was lost. E.g. chunk A buffered in the SDK, then
appending chunk B throws a generic network error → previous fix
dropped A+B and only sent B via `deliverNormally`, silently truncating
the final reply.

Route the full buffered `pendingText` through
`deliverPendingStreamFallback` with a synthetic
`SlackStreamNotDeliveredError`, then skip `deliverNormally` entirely
(pendingText already contains this payload's text, per
`appendSlackStream` accumulating before throw). If the chunked
fallback fails, fall back to `deliverNormally` so at least the current
payload lands.

Test updated to assert the full pendingText ("first buffered\nsecond
payload") gets routed through the chunked sender, not the
chunk-B-only partial send.

* fix(slack): harden stream fallback docs and chunking test (openclaw#71124)

---------

Co-authored-by: Peter Steinberger <[email protected]>
globalcaos pushed a commit to globalcaos/tinkerclaw that referenced this pull request May 13, 2026
globalcaos pushed a commit to globalcaos/tinkerclaw that referenced this pull request May 13, 2026
…llow-up to openclaw#70370) (openclaw#71124)

* fix(slack): route stream-fallback delivery through chunked sender

deliverPendingStreamFallback was calling chat.postMessage directly for
err.pendingText, which bypasses the chunked reply path used everywhere
else. For Slack Connect cases where appendSlackStream throws
SlackStreamNotDeliveredError with a large pending buffer, the single
raw post could fail (msg_too_long) and drop the unsent tail.

Two changes:

1. deliverPendingStreamFallback now routes through deliverReplies so
   long pendingText is chunked by the normal sender and the fallback
   honors the configured replyToMode / identity.

2. The non-benign streaming-error branch in deliverWithStreaming now
   clears the session via markSlackStreamFallbackDelivered before
   falling back to deliverNormally. Without this, pendingText stays
   populated and the post-loop finalize (stopSlackStream →
   SlackStreamNotDeliveredError → fallback) re-posts the same chunk
   that deliverNormally already sent.

Addresses the three Codex P1 findings on openclaw#70370 about bypassing the
chunked sender, and the related "avoid reposting buffered text after
append fallback" P1 about duplicate delivery. Tests updated to assert
deliverReplies routing (instead of raw postMessage) and a new case
covers the non-benign-error dedup.

Follow-up to openclaw#70370.

* fix(slack): preserve pending buffered text on non-benign stream errors

Address Codex P1 on openclaw#71124: `markSlackStreamFallbackDelivered` was
clearing `pendingText` before `deliverNormally` ran, so any earlier
buffered chunk was lost. E.g. chunk A buffered in the SDK, then
appending chunk B throws a generic network error → previous fix
dropped A+B and only sent B via `deliverNormally`, silently truncating
the final reply.

Route the full buffered `pendingText` through
`deliverPendingStreamFallback` with a synthetic
`SlackStreamNotDeliveredError`, then skip `deliverNormally` entirely
(pendingText already contains this payload's text, per
`appendSlackStream` accumulating before throw). If the chunked
fallback fails, fall back to `deliverNormally` so at least the current
payload lands.

Test updated to assert the full pendingText ("first buffered\nsecond
payload") gets routed through the chunked sender, not the
chunk-B-only partial send.

* fix(slack): harden stream fallback docs and chunking test (openclaw#71124)

---------

Co-authored-by: Peter Steinberger <[email protected]>
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 24, 2026
…llow-up to openclaw#70370) (openclaw#71124)

* fix(slack): route stream-fallback delivery through chunked sender

deliverPendingStreamFallback was calling chat.postMessage directly for
err.pendingText, which bypasses the chunked reply path used everywhere
else. For Slack Connect cases where appendSlackStream throws
SlackStreamNotDeliveredError with a large pending buffer, the single
raw post could fail (msg_too_long) and drop the unsent tail.

Two changes:

1. deliverPendingStreamFallback now routes through deliverReplies so
   long pendingText is chunked by the normal sender and the fallback
   honors the configured replyToMode / identity.

2. The non-benign streaming-error branch in deliverWithStreaming now
   clears the session via markSlackStreamFallbackDelivered before
   falling back to deliverNormally. Without this, pendingText stays
   populated and the post-loop finalize (stopSlackStream →
   SlackStreamNotDeliveredError → fallback) re-posts the same chunk
   that deliverNormally already sent.

Addresses the three Codex P1 findings on openclaw#70370 about bypassing the
chunked sender, and the related "avoid reposting buffered text after
append fallback" P1 about duplicate delivery. Tests updated to assert
deliverReplies routing (instead of raw postMessage) and a new case
covers the non-benign-error dedup.

Follow-up to openclaw#70370.

* fix(slack): preserve pending buffered text on non-benign stream errors

Address Codex P1 on openclaw#71124: `markSlackStreamFallbackDelivered` was
clearing `pendingText` before `deliverNormally` ran, so any earlier
buffered chunk was lost. E.g. chunk A buffered in the SDK, then
appending chunk B throws a generic network error → previous fix
dropped A+B and only sent B via `deliverNormally`, silently truncating
the final reply.

Route the full buffered `pendingText` through
`deliverPendingStreamFallback` with a synthetic
`SlackStreamNotDeliveredError`, then skip `deliverNormally` entirely
(pendingText already contains this payload's text, per
`appendSlackStream` accumulating before throw). If the chunked
fallback fails, fall back to `deliverNormally` so at least the current
payload lands.

Test updated to assert the full pendingText ("first buffered\nsecond
payload") gets routed through the chunked sender, not the
chunk-B-only partial send.

* fix(slack): harden stream fallback docs and chunking test (openclaw#71124)

---------

Co-authored-by: Peter Steinberger <[email protected]>
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
jameslcowan pushed a commit to jameslcowan/openclaw that referenced this pull request Jun 2, 2026
…llow-up to openclaw#70370) (openclaw#71124)

* fix(slack): route stream-fallback delivery through chunked sender

deliverPendingStreamFallback was calling chat.postMessage directly for
err.pendingText, which bypasses the chunked reply path used everywhere
else. For Slack Connect cases where appendSlackStream throws
SlackStreamNotDeliveredError with a large pending buffer, the single
raw post could fail (msg_too_long) and drop the unsent tail.

Two changes:

1. deliverPendingStreamFallback now routes through deliverReplies so
   long pendingText is chunked by the normal sender and the fallback
   honors the configured replyToMode / identity.

2. The non-benign streaming-error branch in deliverWithStreaming now
   clears the session via markSlackStreamFallbackDelivered before
   falling back to deliverNormally. Without this, pendingText stays
   populated and the post-loop finalize (stopSlackStream →
   SlackStreamNotDeliveredError → fallback) re-posts the same chunk
   that deliverNormally already sent.

Addresses the three Codex P1 findings on openclaw#70370 about bypassing the
chunked sender, and the related "avoid reposting buffered text after
append fallback" P1 about duplicate delivery. Tests updated to assert
deliverReplies routing (instead of raw postMessage) and a new case
covers the non-benign-error dedup.

Follow-up to openclaw#70370.

* fix(slack): preserve pending buffered text on non-benign stream errors

Address Codex P1 on openclaw#71124: `markSlackStreamFallbackDelivered` was
clearing `pendingText` before `deliverNormally` ran, so any earlier
buffered chunk was lost. E.g. chunk A buffered in the SDK, then
appending chunk B throws a generic network error → previous fix
dropped A+B and only sent B via `deliverNormally`, silently truncating
the final reply.

Route the full buffered `pendingText` through
`deliverPendingStreamFallback` with a synthetic
`SlackStreamNotDeliveredError`, then skip `deliverNormally` entirely
(pendingText already contains this payload's text, per
`appendSlackStream` accumulating before throw). If the chunked
fallback fails, fall back to `deliverNormally` so at least the current
payload lands.

Test updated to assert the full pendingText ("first buffered\nsecond
payload") gets routed through the chunked sender, not the
chunk-B-only partial send.

* fix(slack): harden stream fallback docs and chunking test (openclaw#71124)

---------

Co-authored-by: Peter Steinberger <[email protected]>
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
sablehead pushed a commit to sablehead/openclaw that referenced this pull request Jun 10, 2026
…llow-up to openclaw#70370) (openclaw#71124)

* fix(slack): route stream-fallback delivery through chunked sender

deliverPendingStreamFallback was calling chat.postMessage directly for
err.pendingText, which bypasses the chunked reply path used everywhere
else. For Slack Connect cases where appendSlackStream throws
SlackStreamNotDeliveredError with a large pending buffer, the single
raw post could fail (msg_too_long) and drop the unsent tail.

Two changes:

1. deliverPendingStreamFallback now routes through deliverReplies so
   long pendingText is chunked by the normal sender and the fallback
   honors the configured replyToMode / identity.

2. The non-benign streaming-error branch in deliverWithStreaming now
   clears the session via markSlackStreamFallbackDelivered before
   falling back to deliverNormally. Without this, pendingText stays
   populated and the post-loop finalize (stopSlackStream →
   SlackStreamNotDeliveredError → fallback) re-posts the same chunk
   that deliverNormally already sent.

Addresses the three Codex P1 findings on openclaw#70370 about bypassing the
chunked sender, and the related "avoid reposting buffered text after
append fallback" P1 about duplicate delivery. Tests updated to assert
deliverReplies routing (instead of raw postMessage) and a new case
covers the non-benign-error dedup.

Follow-up to openclaw#70370.

* fix(slack): preserve pending buffered text on non-benign stream errors

Address Codex P1 on openclaw#71124: `markSlackStreamFallbackDelivered` was
clearing `pendingText` before `deliverNormally` ran, so any earlier
buffered chunk was lost. E.g. chunk A buffered in the SDK, then
appending chunk B throws a generic network error → previous fix
dropped A+B and only sent B via `deliverNormally`, silently truncating
the final reply.

Route the full buffered `pendingText` through
`deliverPendingStreamFallback` with a synthetic
`SlackStreamNotDeliveredError`, then skip `deliverNormally` entirely
(pendingText already contains this payload's text, per
`appendSlackStream` accumulating before throw). If the chunked
fallback fails, fall back to `deliverNormally` so at least the current
payload lands.

Test updated to assert the full pendingText ("first buffered\nsecond
payload") gets routed through the chunked sender, not the
chunk-B-only partial send.

* fix(slack): harden stream fallback docs and chunking test (openclaw#71124)

---------

Co-authored-by: Peter Steinberger <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: slack Channel integration: slack size: L

Projects

None yet

2 participants