fix(msteams): keep streaming alive during long tool chains via typing indicator (#59731)#64088
Conversation
Greptile SummaryThis 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 Confidence Score: 5/5Safe 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 No files require special attention. Prompt To Fix All With AIThis 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 |
| isStreamActive(): boolean { | ||
| if (!stream) { | ||
| return false; | ||
| } | ||
| if (stream.isFinalized) { | ||
| return false; | ||
| } | ||
| return streamReceivedTokens; | ||
| }, |
There was a problem hiding this 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.
| 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.… 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>
… 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>
… 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>
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
withRevokedProxyFallbackif the live turn context expiresTest plan
Fixes #59731
🤖 Generated with Claude Code