Skip to content

fix(msteams): trim streamed prefix in long-reply fallback to stop >4000-char double-post (regressed #59297 in #76262)#91729

Draft
devinkuhn wants to merge 2 commits into
openclaw:mainfrom
devinkuhn:fix/msteams-longreply-streamed-prefix-suppression
Draft

fix(msteams): trim streamed prefix in long-reply fallback to stop >4000-char double-post (regressed #59297 in #76262)#91729
devinkuhn wants to merge 2 commits into
openclaw:mainfrom
devinkuhn:fix/msteams-longreply-streamed-prefix-suppression

Conversation

@devinkuhn

Copy link
Copy Markdown

Summary

Restores streamed-prefix suppression for MS Teams long-reply block fallback. When native streaming fails mid-delivery (e.g. Teams 4000-char limit) or finalize() falls back after a suppressed stream close, block delivery now sends only the un-streamed suffix instead of re-posting the full text on top of the live preview card.

Root cause

PR #76262 (SDK rebase) dropped the #59297 behavior that trimmed emittedTextLength from fallbackPayloadForSuppressedFinal. When streamFailed latched after partial stream.emit, preparePayload returned the full payload and/or finalize returned an untrimmed pendingFinalPayload, producing a duplicate message (streamed prefix + full block reply).

Fix

Tests

Extended extensions/msteams/src/reply-stream-controller.test.ts:

  • Partial stream failure → block fallback delivers text.slice(emittedTextLength) only
  • Streamed prefix covers full text → fallback suppresses text (undefined), media preserved on direct preparePayload
  • No tokens streamed before failure → full block delivery unchanged
  • Updated existing finalize/fallback expectations for trimmed suffix behavior

Local proof: pnpm test extensions/msteams/src/reply-stream-controller.test.ts — 42/42 passed.

Related

Real behavior proof

Unit-tested (this PR): streamed-prefix trimming for streamFailed block fallback, full-text suppression when the streamed prefix already covers the payload, and unchanged full delivery when emittedTextLength === 0. All covered by reply-stream-controller.test.ts (42 tests).

Live-verified (equivalent dist patch, not this source commit): The same trim logic was validated as a deployed dist patch on a production OpenClaw 2026.6.1 NemoClaw instance (bifrost-claude via Bifrost→Azure). A >4000-char Teams DM reply was confirmed to render as a single message after the patch. A live screenshot from that deploy is pending (after-hours deploy window); this upstream source PR was not yet live-deployed at submit time.

What was not tested live with this PR branch: No Crabbox/Telegram-style E2E run against upstream main with this commit; reliance is on unit tests plus the prior production dist-patch proof of the equivalent behavior.

Made with Cursor

@openclaw-barnacle openclaw-barnacle Bot added channel: msteams Channel integration: msteams size: S triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels Jun 9, 2026
@clawsweeper

clawsweeper Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed June 30, 2026, 5:14 PM ET / 21:14 UTC.

Summary
The PR changes the MS Teams reply stream controller, stream type surface, and tests to trim already streamed text on fallback and suppress SDK final creation for streamed replies at or above 4000 characters.

PR surface: Source +30, Tests +286. Total +316 across 4 files.

Reproducibility: yes. for the PR-introduced trimming bug: source inspection shows streamFailed remains latched while later payloads still pass through preparePayload. No for the claimed live over-4000-character fix: branch-specific positive Teams proof is absent and the latest author comment reports a negative production result.

Review metrics: 1 noteworthy metric.

  • Long-reply final suppression threshold: 1 added threshold at 4000 characters. The cutoff decides when the PR suppresses SDK final activity, so maintainers need Teams-specific proof before merge.

Root-cause cluster
Relationship: fixed_by_candidate
Canonical: #91723
Summary: This PR is an open draft candidate for the canonical over-4000-character Teams duplicate issue, but it is not yet a safe fix because the latest discussion reports negative live evidence and source review found patch-introduced delivery risks.

Members:

Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything.

Merge readiness
Overall: 🧂 unranked krab
Proof: 🧂 unranked krab
Patch quality: 🧂 unranked krab
Result: blocked until stronger real behavior proof is added.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • [P1] Fix the failed-stream state boundary so later reply segments are not trimmed by stale emittedTextLength.
  • [P1] Add branch-specific redacted Teams screenshot, recording, live output, or logs for an over-4000-character DM reply after rework.
  • Preserve or explicitly decide AI-generated and feedback affordances when suppressing the long-reply stream-final create.

Proof guidance:

  • [P1] Needs stronger real behavior proof before merge: The PR has unit tests and an equivalent-patch claim, but no branch-specific positive Teams proof and the latest author comment reports the equivalent production patch still double-posted. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.

Proof path suggestion
A real visible Teams chat run would materially help prove whether the long-reply duplicate is fixed and whether final-message affordances remain present. Mantis is currently scoped to Telegram, Discord, and web UI chat proof, so it is not the right proof path for this surface.
Use maintainer screenshot/manual proof, browser or Playwright proof, Crabbox where appropriate, or normal local artifact proof instead.

Risk before merge

  • [P1] The PR can truncate unrelated later reply segments because streamFailed and emittedTextLength are controller-wide state while reply-dispatcher continues to pass later payloads through preparePayload.
  • [P1] The long-reply shortcut suppresses SDK final activity before the AI-generated entity and feedback channelData are emitted, so long Teams replies may lose final-message affordances.
  • [P1] The author’s latest comment reports the equivalent production trim patch still double-posted, so the user-visible Teams bug is not yet proven fixed by this branch.

Maintainer options:

  1. Rework and prove the Teams stream boundary (recommended)
    Scope fallback trimming to the failed payload or reset the failed state before later segments, preserve final metadata, then add live Teams proof for the long-reply path.
  2. Keep the draft blocked for live proof
    Hold the PR until branch-specific redacted Teams screenshot, recording, live output, or logs show a single over-4000-character DM reply after rework.
  3. Replace with a stream-final collapse fix
    If the real duplicate is the SDK preview/final collapse path rather than fallback trimming, pause or close this draft in favor of a narrower fix tied to the canonical issue.

Next step before merge

  • [P1] The draft needs author or maintainer rework plus live Teams proof; automation cannot supply the contributor’s Teams environment proof or decide the final-message metadata tradeoff.

Security
Cleared: The diff does not change secrets handling, permissions, dependency versions, package resolution, CI, or executable supply-chain surfaces.

Review findings

  • [P1] Scope failed-stream trimming to the failed payload — extensions/msteams/src/reply-stream-controller.ts:324-325
  • [P2] Preserve long-reply final metadata — extensions/msteams/src/reply-stream-controller.ts:355-357
Review details

Best possible solution:

Rework the controller so failed-stream trimming is scoped to the failed payload, preserve final-message affordances or get an explicit maintainer tradeoff decision, and provide redacted live Teams proof for an over-4000-character DM before merge.

Do we have a high-confidence way to reproduce the issue?

Yes for the PR-introduced trimming bug: source inspection shows streamFailed remains latched while later payloads still pass through preparePayload. No for the claimed live over-4000-character fix: branch-specific positive Teams proof is absent and the latest author comment reports a negative production result.

Is this the best way to solve the issue?

No. The patch is a plausible hardening direction, but the best fix needs a per-payload stream-failure boundary or state reset plus explicit preservation of final-message metadata.

Full review comments:

  • [P1] Scope failed-stream trimming to the failed payload — extensions/msteams/src/reply-stream-controller.ts:324-325
    After a non-cancel stream failure, streamFailed stays latched for the controller lifetime while reply-dispatcher.ts sends later reply segments through preparePayload. This branch slices emittedTextLength from those unrelated later payloads too, so post-tool text can lose its first N characters; reset or scope the failure state after the failed fallback.
    Confidence: 0.9
  • [P2] Preserve long-reply final metadata — extensions/msteams/src/reply-stream-controller.ts:355-357
    The >=4000-character branch returns before emitting the final MessageActivity that carries the AI-generated entity and feedbackLoopEnabled channelData. Long streamed replies would avoid the duplicate by losing those affordances; preserve them with a no-text final strategy or make that tradeoff explicit with maintainer approval and proof.
    Confidence: 0.82

Overall correctness: patch is incorrect
Overall confidence: 0.86

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against b885c81479d0.

Label changes

Label justifications:

  • P1: The PR targets a user-visible Microsoft Teams duplicate-delivery regression and the current draft can also corrupt delivered reply text.
  • merge-risk: 🚨 message-delivery: The diff changes Teams streamed-reply fallback and finalization behavior where a wrong state transition can duplicate, truncate, or suppress messages.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🧂 unranked krab and patch quality is 🧂 unranked krab.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: The PR has unit tests and an equivalent-patch claim, but no branch-specific positive Teams proof and the latest author comment reports the equivalent production patch still double-posted. After adding proof, update the PR body; ClawSweeper should re-review automatically. If it does not, the PR author or someone with repository write access can comment @clawsweeper re-review.
Evidence reviewed

PR surface:

Source +30, Tests +286. Total +316 across 4 files.

View PR surface stats
Area Files Added Removed Net
Source 2 36 6 +30
Tests 2 292 6 +286
Docs 0 0 0 0
Config 0 0 0 0
Generated 0 0 0 0
Other 0 0 0 0
Total 4 328 12 +316

What I checked:

  • Repository policy applied: Root and scoped extension policy were read; OpenClaw requires channel delivery reviews to inspect callers, tests, current behavior, and dependency contracts before verdict. (AGENTS.md:24, b885c81479d0)
  • Current caller path: reply-dispatcher sends every reply payload through streamController.preparePayload, so controller-wide stream failure state can affect later post-tool/block segments. (extensions/msteams/src/reply-dispatcher.ts:317, b885c81479d0)
  • PR-head stale failure trim: At PR head, any payload after streamFailed is latched and emittedTextLength is positive is trimmed with no reset or per-payload boundary. (extensions/msteams/src/reply-stream-controller.ts:324, e4556d67929a)
  • PR-head final suppression path: The >=4000-character branch clears text and closes before emitting the final MessageActivity entities and feedback channelData. (extensions/msteams/src/reply-stream-controller.ts:355, e4556d67929a)
  • SDK contract checked: @microsoft/teams.apps v2.0.12 HttpStream.clearText clears accumulated text and queued message deltas, and close returns without sending when there is no text, attachment, or suggested action. (microsoft/teams.ts/packages/apps/src/http/http-stream.ts:109)
  • Author negative proof update: The latest author comment says an equivalent production trim patch still double-posted and did not traverse the streamFailed fallback path, so the PR is not proven to fix the observed Teams duplicate. (e4556d67929a)

Likely related people:

  • heyitsaamir: Authored the merged Teams SDK rebase that introduced the current ctx.stream controller boundary used by the affected reply path. (role: SDK migration author and adjacent feature owner; confidence: high; commits: 04c29825356f; files: extensions/msteams/src/reply-stream-controller.ts, extensions/msteams/src/reply-dispatcher.ts, extensions/msteams/src/sdk-types.ts)
  • BradGroux: Authored and merged the pre-SDK fix that tracked streamed length and trimmed already delivered text for the earlier over-4000-character Teams duplicate class. (role: previous long-reply duplicate fix author; confidence: high; commits: 57949397fa18; files: extensions/msteams/src/reply-stream-controller.ts, extensions/msteams/src/streaming-message.ts, extensions/msteams/src/reply-stream-controller.test.ts)
  • steipete: Merged the Teams SDK rebase and appears in adjacent Teams reply helper history for the same delivery area. (role: merger and adjacent area contributor; confidence: medium; commits: 04c29825356f, 27448c311300; files: extensions/msteams/src/reply-stream-controller.ts, extensions/msteams/src/reply-dispatcher.ts, extensions/msteams/src/sdk.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.
Review history (1 earlier review cycle)
  • reviewed 2026-06-21T20:16:31.977Z sha e4556d6 :: needs real behavior proof before merge. :: [P1] Scope failed-stream trimming to the failed payload | [P2] Preserve long-reply final metadata

@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: 8b456d3819

ℹ️ 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 +319 to +320
if (streamFailed && emittedTextLength > 0) {
return trimAlreadyStreamedPrefix(payload);

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.

P2 Badge Preserve later block segments after a stream failure

When a partial stream fails, streamFailed remains latched for the rest of the reply controller, and reply-dispatcher.ts calls preparePayload for every later reply segment. This new branch therefore trims emittedTextLength from unrelated post-tool/block segments too; for example after the first streamed segment fails, a later payload like "Second segment after tools" loses its first N characters even though none of that text was streamed. The existing multi-segment tests cover later segments without failure, but this failure path now corrupts subsequent content rather than only trimming the cumulative final for the failed streamed segment.

Useful? React with 👍 / 👎.

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. labels Jun 9, 2026
@devinkuhn
devinkuhn marked this pull request as draft June 9, 2026 15:46
@devinkuhn

Copy link
Copy Markdown
Author

Correction / honest negative result: I deployed the equivalent trim fix to a production 2026.6.1 NemoClaw instance and a >4000-char Teams DM reply STILL double-posted. Gateway logs showed NO streamFailed / stream-emit-failed / finalize-failed warnings — meaning the duplicate does NOT traverse the streamFailed fallback path this PR hardens. The observed mechanism appears to be the SDK stream not collapsing the streaming preview into the final close() activity for long replies (i.e. the #90398 class, where preview card + final MessageActivity both persist as separate bubbles), which is not addressed by payload-prefix trimming in the plugin layer. This PRs change is still arguably a correct hardening of the genuine streamFailed fallback path (unit-tested), but it does NOT fix the observed >4000-char double-post. Converting to draft so it is not mistaken for a verified fix for #91723. Will re-investigate the actual stream.close()/collapse behavior before re-proposing.

@clawsweeper clawsweeper Bot added rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. P1 High-priority user-facing bug, regression, or broken workflow. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. and removed rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. labels Jun 9, 2026
Repro with real HttpStream shows Teams double-post comes from preview
stream CREATE plus addStreamFinal CREATE (not streamFailed block fallback).
For replies at/above 4000 streamed chars, clearText()+close() skips the
second CREATE while the preview card already holds the full text.
@openclaw-barnacle

Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added stale Marked as stale due to inactivity triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. and removed stale Marked as stale due to inactivity labels Jul 21, 2026
@clawsweeper

clawsweeper Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

ClawSweeper status: review started.

I am starting a fresh review of this pull request: fix(msteams): trim streamed prefix in long-reply fallback to stop >4000-char double-post (regressed #59297 in #76262) This is item 1/1 in the current shard. Shard 0/1.

This placeholder means the worker is alive and reading the current context. I will edit this same comment with the actual review when the claws are done clicking.

Crustacean status: shell secured, claws on keyboard, evidence pebbles being sorted.

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

Labels

channel: msteams Channel integration: msteams merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. P1 High-priority user-facing bug, regression, or broken workflow. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: M status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. triage: needs-pr-context Candidate: external PR body lacks required problem context or evidence. triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

msteams: streaming double-posts replies over 4000 chars after SDK rebase (#76262 regressed #59297)

2 participants