From 460b92d7a4fe2feb13fe6f6531feb53ff09c495b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 05:16:59 +0000 Subject: [PATCH 1/5] Initial plan From d0897f29cebec3d2a382fbb70b40d21c980377ee Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 05:24:27 +0000 Subject: [PATCH 2/5] fix(apps): auto-send buffered stream output in group conversations Agent-Logs-Url: https://github.com/microsoft/teams.ts/sessions/9987b20f-6ce2-450a-b7fe-67eec7f00efb Co-authored-by: heyitsaamir <48929123+heyitsaamir@users.noreply.github.com> --- packages/apps/src/http/http-stream.spec.ts | 29 +++++++++++++++ packages/apps/src/http/http-stream.ts | 43 +++++++++++++++++----- 2 files changed, 62 insertions(+), 10 deletions(-) diff --git a/packages/apps/src/http/http-stream.spec.ts b/packages/apps/src/http/http-stream.spec.ts index f75b456a0..0ec4182ab 100644 --- a/packages/apps/src/http/http-stream.spec.ts +++ b/packages/apps/src/http/http-stream.spec.ts @@ -264,6 +264,35 @@ describe('HttpStream', () => { ); }); + test('group-scope stream buffers and sends a single normal message on close', async () => { + const groupRef = { + ...ref, + conversation: { ...ref.conversation, isGroup: true }, + }; + const stream = new HttpStream(client, groupRef, logger); + mockCreate(); + + stream.update('Thinking...'); + stream.emit('first message'); + stream.emit('last message'); + await jest.runAllTimersAsync(); + + expect(client.conversations.activities().create).toHaveBeenCalledTimes(0); + + await stream.close(); + + expect(client.conversations.activities().create).toHaveBeenCalledTimes(1); + expect(client.conversations.activities().create).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'message', + text: 'first messagelast message', + }) + ); + + const sent = client.conversations.activities().create.mock.calls[0][0]; + expect(sent.channelData?.streamType).toBeUndefined(); + }); + test('close waits for flush to complete before sending final message', async () => { mockCreate(); diff --git a/packages/apps/src/http/http-stream.ts b/packages/apps/src/http/http-stream.ts index 3e4478676..f7111173e 100644 --- a/packages/apps/src/http/http-stream.ts +++ b/packages/apps/src/http/http-stream.ts @@ -65,6 +65,10 @@ export class HttpStream implements IStreamer { this._logger = logger?.child('stream') || new ConsoleLogger('@teams/http-stream'); } + protected get isGroupConversation(): boolean { + return this.ref.conversation.isGroup === true; + } + /** * Emit a new activity or text to the stream. * @param activity Activity object or string message. @@ -106,7 +110,13 @@ export class HttpStream implements IStreamer { * Waits for all queued activities to flush. */ async close() { - if (!this.index && !this.queue.length && !this._flushing) { + const hasBufferedContent = this.text !== '' || !!this.attachments.length || !!this.entities.length; + if ( + !this.index + && !this.queue.length + && !this._flushing + && (!hasBufferedContent || !this.isGroupConversation) + ) { this._logger.debug('closed with no content'); return; } @@ -124,7 +134,7 @@ export class HttpStream implements IStreamer { // Wait until all queued activities are flushed const start = Date.now(); - while ((this.queue.length || !this.id || this._flushing) && !this._canceled) { + while ((this.queue.length || (!this.id && !this.isGroupConversation) || this._flushing) && !this._canceled) { if (Date.now() - start > this._totalTimeout) { this._logger.warn('Timeout while waiting for id and queue to flush'); return; @@ -138,7 +148,7 @@ export class HttpStream implements IStreamer { return; } - if (!this.id) { + if (!this.id && !this.isGroupConversation) { this._logger.warn('no stream id set, cannot close stream'); return; } @@ -148,13 +158,24 @@ export class HttpStream implements IStreamer { return; } + const channelData = this.isGroupConversation + ? { ...this.channelData } + : this.channelData; + if (this.isGroupConversation && 'streamType' in channelData) { + delete channelData.streamType; + } + // Build final message activity const activity = new MessageActivity(this.text) - .withId(this.id) .addAttachments(...this.attachments) .addEntities(...this.entities) - .withChannelData(this.channelData) - .addStreamFinal(); + .withChannelData(channelData); + + if (!this.isGroupConversation) { + activity + .withId(this.id!) + .addStreamFinal(); + } const res = await promises.retry(() => this.send(activity), { logger: this._logger @@ -227,12 +248,14 @@ export class HttpStream implements IStreamer { if (startLength === 0) return; // Send informative updates immediately - for (const informativeUpdate of informativeUpdates) { - const activity = new TypingActivity().withText(informativeUpdate.text || '').withChannelData({ streamType: 'informative' }); - await this.pushStreamChunk(activity); + if (!this.isGroupConversation) { + for (const informativeUpdate of informativeUpdates) { + const activity = new TypingActivity().withText(informativeUpdate.text || '').withChannelData({ streamType: 'informative' }); + await this.pushStreamChunk(activity); + } } - if (this.text) { + if (this.text && !this.isGroupConversation) { const activity = new TypingActivity().withText(this.text); await this.pushStreamChunk(activity); } From c4672625eaea31075da93c76a11da1cf95ea0c59 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 05:25:21 +0000 Subject: [PATCH 3/5] chore(apps): refine group-stream close content checks Agent-Logs-Url: https://github.com/microsoft/teams.ts/sessions/9987b20f-6ce2-450a-b7fe-67eec7f00efb Co-authored-by: heyitsaamir <48929123+heyitsaamir@users.noreply.github.com> --- packages/apps/src/http/http-stream.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/apps/src/http/http-stream.ts b/packages/apps/src/http/http-stream.ts index f7111173e..18a459e34 100644 --- a/packages/apps/src/http/http-stream.ts +++ b/packages/apps/src/http/http-stream.ts @@ -110,7 +110,7 @@ export class HttpStream implements IStreamer { * Waits for all queued activities to flush. */ async close() { - const hasBufferedContent = this.text !== '' || !!this.attachments.length || !!this.entities.length; + const hasBufferedContent = this.text !== '' || this.attachments.length > 0 || this.entities.length > 0; if ( !this.index && !this.queue.length From 65c7dce591a83da6724616eec9ab08dd3396689b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 05:26:12 +0000 Subject: [PATCH 4/5] chore(apps): add defensive stream id guard on close Agent-Logs-Url: https://github.com/microsoft/teams.ts/sessions/9987b20f-6ce2-450a-b7fe-67eec7f00efb Co-authored-by: heyitsaamir <48929123+heyitsaamir@users.noreply.github.com> --- packages/apps/src/http/http-stream.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/apps/src/http/http-stream.ts b/packages/apps/src/http/http-stream.ts index 18a459e34..3cb6e22f8 100644 --- a/packages/apps/src/http/http-stream.ts +++ b/packages/apps/src/http/http-stream.ts @@ -172,8 +172,12 @@ export class HttpStream implements IStreamer { .withChannelData(channelData); if (!this.isGroupConversation) { + if (!this.id) { + this._logger.warn('no stream id set, cannot close stream'); + return; + } activity - .withId(this.id!) + .withId(this.id) .addStreamFinal(); } From ed496dd1f0908597b62fd1c30c2ae888e2bffc72 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 15 May 2026 05:27:03 +0000 Subject: [PATCH 5/5] chore(apps): remove duplicate stream id close check Agent-Logs-Url: https://github.com/microsoft/teams.ts/sessions/9987b20f-6ce2-450a-b7fe-67eec7f00efb Co-authored-by: heyitsaamir <48929123+heyitsaamir@users.noreply.github.com> --- packages/apps/src/http/http-stream.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/apps/src/http/http-stream.ts b/packages/apps/src/http/http-stream.ts index 3cb6e22f8..493fdfe2b 100644 --- a/packages/apps/src/http/http-stream.ts +++ b/packages/apps/src/http/http-stream.ts @@ -148,11 +148,6 @@ export class HttpStream implements IStreamer { return; } - if (!this.id && !this.isGroupConversation) { - this._logger.warn('no stream id set, cannot close stream'); - return; - } - if (this.text === '' && !this.attachments.length) { this._logger.warn('no text or attachments to send, cannot close stream'); return; @@ -172,12 +167,13 @@ export class HttpStream implements IStreamer { .withChannelData(channelData); if (!this.isGroupConversation) { - if (!this.id) { + const streamId = this.id; + if (!streamId) { this._logger.warn('no stream id set, cannot close stream'); return; } activity - .withId(this.id) + .withId(streamId) .addStreamFinal(); }