Skip to content

Commit 723b4f2

Browse files
committed
fix(cli): map legacy trigger:* data records into controlValue in MCP collector
The MCP `collectAgentResponse` legacy filter added in f8f9898 was a blanket `continue` for any `trigger:*` data record — which silently dropped legacy turn-complete (loop never breaks) and legacy upgrade-required (continuation never fires). On a session whose `.out` was populated by an old agent, the collector would hang on turn-complete instead of returning the assistant message. Moved the legacy detection up alongside the header-based `controlSubtype` check so both shapes feed the same `controlValue` — the existing break / continuation logic now fires for header-form and data-chunk-form control signals alike. Matches the pattern just added to `chat.ts` and `chat-client.ts`.
1 parent 950d10b commit 723b4f2

1 file changed

Lines changed: 22 additions & 11 deletions

File tree

packages/cli-v3/src/mcp/tools/agentChat.ts

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,24 @@ async function collectAgentResponse(
398398
// Trigger control records (turn-complete, upgrade-required) ride
399399
// on headers — see `client-protocol.mdx#records-on-session-out`.
400400
// Data records carry UIMessageChunks on `value.chunk`.
401-
const controlValue = controlSubtype(value.headers);
401+
//
402+
// Cross-version bridge: an agent SDK that hasn't been redeployed
403+
// yet still writes turn-complete / upgrade-required as
404+
// `chunk.type` data records. Map those into `controlValue` so the
405+
// existing break / continuation paths fire for both shapes.
406+
let controlValue = controlSubtype(value.headers);
407+
if (!controlValue && value.chunk && typeof value.chunk === "object") {
408+
const chunk = value.chunk as { type?: unknown };
409+
if (chunk.type === "trigger:turn-complete") {
410+
controlValue = TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE;
411+
} else if (chunk.type === "trigger:upgrade-required") {
412+
controlValue = TRIGGER_CONTROL_SUBTYPE.UPGRADE_REQUIRED;
413+
} else if (typeof chunk.type === "string" && chunk.type.startsWith("trigger:")) {
414+
// Unknown legacy `trigger:*` type — drop so it doesn't reach
415+
// the chunk handler as a UIMessageChunk.
416+
continue;
417+
}
418+
}
402419

403420
if (controlValue === TRIGGER_CONTROL_SUBTYPE.TURN_COMPLETE) {
404421
break;
@@ -439,19 +456,13 @@ async function collectAgentResponse(
439456
}
440457

441458
// v2 (session) SSE already parses record.body.data, so `chunk` is
442-
// the UIMessageChunk object written by the agent.
459+
// the UIMessageChunk object written by the agent. Any legacy
460+
// `trigger:*` data record was already mapped to `controlValue`
461+
// (and either broke the loop, triggered continuation, or got
462+
// dropped) above; we only see real UIMessageChunks here.
443463
if (value.chunk != null && typeof value.chunk === "object") {
444464
const chunk = value.chunk as Record<string, unknown>;
445465

446-
// Legacy belt-and-suspenders: prior SDK versions emitted
447-
// `trigger:turn-complete` / `trigger:upgrade-required` as
448-
// data records (`chunk.type`) instead of header-form control
449-
// records. Filter them so an in-flight session whose `.out`
450-
// was populated by an older agent doesn't stall this loop.
451-
if (typeof chunk.type === "string" && chunk.type.startsWith("trigger:")) {
452-
continue;
453-
}
454-
455466
if (chunk.type === "text-delta" && typeof chunk.delta === "string") {
456467
text += chunk.delta;
457468
// Accumulate into a text part

0 commit comments

Comments
 (0)