fix: classify unstreamed sub-agent messages as thinking

Sub-agent messages bypass the streaming path (turn-delta) entirely due to
SDK batching behavior. They arrive only at turn-complete time via
FinalizeCompletedMessages. Without classification, they appear as separate
chat bubbles cluttering the transcript.

Two-pronged fix:

Sidecar: FinalizeCompletedMessages now tags messages from
_reclassifiedMessageIds with MessageKind='thinking'. This covers messages
that WERE streamed and reclassified during the turn. Added MessageKind
property to ChatMessageDto.

Main process: finalizeTurn detects unstreamed assistant messages (not in
existing session.messages) and classifies them as thinking when a visible
response was already streamed. Emits message-reclassified events so the
renderer can update incrementally, though the primary path is the
workspace:updated broadcast which already includes the correct messageKind.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-30 16:24:30 +01:00
co-authored by Copilot
parent deb5c96d58
commit 59d3c81f9f
4 changed files with 92 additions and 1 deletions
+32 -1
View File
@@ -1824,6 +1824,15 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
const pattern = this.requirePattern(workspace, session.patternId);
const incomingIds = new Set(messages.map((message) => message.id));
// Messages that were streamed during the turn already exist in session.messages
// (possibly with messageKind: 'thinking' from reclassification). Unstreamed messages
// (e.g. from sub-agents) only appear now. Classify them as thinking when a visible
// response was already streamed, since they are intermediate tool-driving steps.
const existingIds = new Set(session.messages.map((m) => m.id));
const hasVisibleResponse = session.messages.some(
(m) => m.role === 'assistant' && m.messageKind !== 'thinking',
);
for (const message of messages) {
const occurredAt = nowIso();
const existing = session.messages.find((current) => current.id === message.id);
@@ -1832,9 +1841,22 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
existing.content = message.content;
existing.pending = false;
} else {
session.messages.push({ ...message, pending: false });
const isUnstreamedIntermediate =
message.role === 'assistant'
&& hasVisibleResponse
&& !message.messageKind;
session.messages.push({
...message,
pending: false,
messageKind: message.messageKind ?? (isUnstreamedIntermediate ? 'thinking' : undefined),
});
}
const reclassifiedAsThinking =
!existingIds.has(message.id)
&& (message.messageKind === 'thinking'
|| (message.role === 'assistant' && hasVisibleResponse && !message.messageKind));
const nextRun = this.updateSessionRun(session, requestId, (run) =>
upsertRunMessageEvent(run, {
messageId: message.id,
@@ -1851,6 +1873,15 @@ export class AryxAppService extends EventEmitter<AppServiceEvents> {
authorName: message.authorName,
content: message.content,
});
if (reclassifiedAsThinking) {
this.emitSessionEvent({
sessionId,
kind: 'message-reclassified',
occurredAt,
messageId: message.id,
messageKind: 'thinking',
});
}
if (nextRun) {
this.emitRunUpdated(sessionId, occurredAt, nextRun);
}