Skip to content

fix(msteams): keep streaming alive during long tool chains via typing indicator (#59731)#64088

Merged
BradGroux merged 4 commits intoopenclaw:mainfrom
sudie-codes:fix/msteams-streaming-tool-chain-59731
Apr 10, 2026
Merged

fix(msteams): keep streaming alive during long tool chains via typing indicator (#59731)#64088
BradGroux merged 4 commits intoopenclaw:mainfrom
sudie-codes:fix/msteams-streaming-tool-chain-59731

Conversation

@sudie-codes
Copy link
Copy Markdown
Contributor

Summary

Bot replies in Teams were getting dropped mid-stream when the agent ran long tool chains (30s+). The typing keepalive was being suppressed in personal DMs to avoid duplicate UI, which left the bot silent long enough for the TurnContext proxy to expire.

Fix

  • Always start the typing keepalive loop when typing is enabled
  • Gate actual typing sends on whether the streaming card is currently chunking (avoids duplicate UX)
  • Keepalive interval: 8s, max duration: 10min (tuned for long tool chains)
  • Falls back to proactive sends via withRevokedProxyFallback if the live turn context expires

Test plan

  • 36 new tests (keepalive cadence, chunking suppression, tool-chain resume, group/channel behavior)
  • 590 msteams tests passing

Fixes #59731

🤖 Generated with Claude Code

@openclaw-barnacle openclaw-barnacle Bot added channel: msteams Channel integration: msteams size: M labels Apr 10, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 10, 2026

Greptile Summary

This PR fixes dropped bot replies in Microsoft Teams when the agent runs long tool chains (30s+). The root cause was that the typing keepalive loop was skipped for personal DMs (to avoid overlapping the streaming card UI), causing the Bot Framework TurnContext proxy to expire before the post-tool reply could be delivered.

The fix separates concerns cleanly: the keepalive loop always starts when typingIndicator is enabled, but a new isStreamActive() gate on TeamsReplyStreamController suppresses actual typing sends while the streaming card is visibly chunking. Between segments—during tool chains—the stream is finalized so isStreamActive() returns false and typing indicators fire normally, keeping the TurnContext alive.

Confidence Score: 5/5

Safe to merge — the fix is logically sound, well-tested, and the only finding is a minor edge case in the failed-stream path.

All 590 existing tests pass, 36 new tests cover the key scenarios (suppression while chunking, resume after finalization, group/channel behavior, opt-out). The one observation (missing isFailed guard in isStreamActive) is a P2: the window is bounded by the 8s keepalive interval and preparePayload corrects it shortly after, so it carries no meaningful reliability risk for the bug being fixed.

No files require special attention.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/msteams/src/reply-stream-controller.ts
Line: 137-145

Comment:
**`isStreamActive` does not check `stream.isFailed`**

When the underlying `TeamsHttpStream` errors mid-stream (e.g. message exceeds the 4 000-char limit), `stream.isFailed` becomes `true` while `streamReceivedTokens` is still `true` and `stream.isFinalized` is `false`. In that window—up to one full keepalive tick (8s)—`isStreamActive()` returns `true`, suppressing typing keepalive sends even though the stream is effectively dead. `preparePayload` eventually resets `streamReceivedTokens`, so the window is short, but adding the `isFailed` guard makes the state machine consistent with the failure path in `preparePayload`.

```suggestion
    isStreamActive(): boolean {
      if (!stream) {
        return false;
      }
      if (stream.isFinalized || stream.isFailed) {
        return false;
      }
      return streamReceivedTokens;
    },
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(msteams): keep streaming alive durin..." | Re-trigger Greptile

Comment on lines +137 to +145
isStreamActive(): boolean {
if (!stream) {
return false;
}
if (stream.isFinalized) {
return false;
}
return streamReceivedTokens;
},
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 isStreamActive does not check stream.isFailed

When the underlying TeamsHttpStream errors mid-stream (e.g. message exceeds the 4 000-char limit), stream.isFailed becomes true while streamReceivedTokens is still true and stream.isFinalized is false. In that window—up to one full keepalive tick (8s)—isStreamActive() returns true, suppressing typing keepalive sends even though the stream is effectively dead. preparePayload eventually resets streamReceivedTokens, so the window is short, but adding the isFailed guard makes the state machine consistent with the failure path in preparePayload.

Suggested change
isStreamActive(): boolean {
if (!stream) {
return false;
}
if (stream.isFinalized) {
return false;
}
return streamReceivedTokens;
},
isStreamActive(): boolean {
if (!stream) {
return false;
}
if (stream.isFinalized || stream.isFailed) {
return false;
}
return streamReceivedTokens;
},
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/reply-stream-controller.ts
Line: 137-145

Comment:
**`isStreamActive` does not check `stream.isFailed`**

When the underlying `TeamsHttpStream` errors mid-stream (e.g. message exceeds the 4 000-char limit), `stream.isFailed` becomes `true` while `streamReceivedTokens` is still `true` and `stream.isFinalized` is `false`. In that window—up to one full keepalive tick (8s)—`isStreamActive()` returns `true`, suppressing typing keepalive sends even though the stream is effectively dead. `preparePayload` eventually resets `streamReceivedTokens`, so the window is short, but adding the `isFailed` guard makes the state machine consistent with the failure path in `preparePayload`.

```suggestion
    isStreamActive(): boolean {
      if (!stream) {
        return false;
      }
      if (stream.isFinalized || stream.isFailed) {
        return false;
      }
      return streamReceivedTokens;
    },
```

How can I resolve this? If you propose a fix, please make it concise.

@BradGroux BradGroux merged commit 99f76ec into openclaw:main Apr 10, 2026
32 checks passed
lovewanwan pushed a commit to lovewanwan/openclaw that referenced this pull request Apr 28, 2026
… indicator (openclaw#59731) (openclaw#64088)

* fix(msteams): keep streaming alive during long tool chains via periodic typing (openclaw#59731)

* test(msteams): align thread-session store mock with interface

* fix(msteams): treat failed streams as inactive

---------

Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
ogt-redknie pushed a commit to ogt-redknie/OPENX that referenced this pull request May 2, 2026
… indicator (openclaw#59731) (openclaw#64088)

* fix(msteams): keep streaming alive during long tool chains via periodic typing (openclaw#59731)

* test(msteams): align thread-session store mock with interface

* fix(msteams): treat failed streams as inactive

---------

Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
github-actions Bot pushed a commit to Desicool/openclaw that referenced this pull request May 9, 2026
… indicator (openclaw#59731) (openclaw#64088)

* fix(msteams): keep streaming alive during long tool chains via periodic typing (openclaw#59731)

* test(msteams): align thread-session store mock with interface

* fix(msteams): treat failed streams as inactive

---------

Co-authored-by: Brad Groux <bradgroux@users.noreply.github.com>
Co-authored-by: Brad Groux <3053586+BradGroux@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: msteams Channel integration: msteams size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MSTeams: streaming reply drops during long tool chains (30s+)

2 participants