Commit 14f0aa3
feat(streams): hybrid capture closes [Console]::* gap and merges Output+Error chronologically
The polling engine's Invoke-CommandWithAllStreams previously ran via
\`Invoke-Expression $cmd -OutVariable -ErrorVariable -WarningVariable
-InformationVariable\` and assembled the response from the four
buckets independently. Two structural problems with that:
1. \`[Console]::WriteLine\` and \`[Console]::Error.WriteLine\` direct
writes (and any .NET library that writes through System.Console
without going through PowerShell's stream system) bypassed every
capture variable. Those bytes still hit the visible terminal but
were invisible to the AI side. Confirmed by side-by-side test
against ripple, where \`[Console]::Error.WriteLine\` survives the
tool response.
2. Output and Error landed in separate buckets with no chronology
between them. The AI saw "here are 5 lines of output, here are 3
errors" but could not tell at WHICH point in the run an error
happened — a problem the user flagged when reviewing the
side-by-side comparison.
Fix: hybrid capture wired inside Invoke-CommandWithAllStreams.
* \`Invoke-Captured -Block $sb -WarningVariable -InformationVariable
2>&1 | Tee-Object -Variable pipelineStream | Out-Host\`
Streams 1 (Output) and 2 (Error) merge into a single chronological
PSObject sequence; Tee-Object captures while Out-Host renders to
the visible console in real time with the host's standard coloring
(red+cyan for ErrorRecord, plain for String). Captured items keep
their PowerShell types so the formatter can render Errors as
Exception.Message and outputs through Out-String.
* Streams 3 (Warning) and 6 (Information / Write-Host) stay
UN-merged. Merging them with \`*>&1\` would force their rendering
through Out-Host's generic record renderer and lose
Write-Host's chosen ForegroundColor on the visible console (tested
and confirmed during the design phase). Capturing via
-WarningVariable / -InformationVariable doesn't disturb the
independent yellow / Write-Host-color rendering the user sees.
* \`[Console]::SetOut\` / \`SetError\` swapped to
PowerShell.MCP.Services.TeeTextWriter (new file) for the duration
of the command. The tee writes to BOTH the original writer
(preserves real-time visible output, no buffering delay) AND a
StringBuilder. .NET-level direct writes that bypass the
PowerShell stream system land in those StringBuilders and surface
as new \`=== CONSOLE.OUT (direct) ===\` / \`=== CONSOLE.ERR (direct) ===\`
sections in the AI response. On the happy path both buffers stay
empty and the sections are omitted.
* Console.Out / Console.Error are restored in finally so a thrown
exception cannot leak the swap into subsequent commands.
The downstream Format-McpOutput rewrite walks PipelineItems in emit
order: outputs render via \`$item | Out-String\`, ErrorRecord renders
as \`$item.Exception.Message\` (matches what's visible on the console
minus the \`Write-Error: \` prefix and trace context PowerShell
decorates the inline render with). Result is a single chronological
text block that leads the response — sections for Warnings, Info,
direct Console writes, and Exceptions follow only when non-empty.
Removed the post-execute \`$streamResults.Success | Out-Default\` and
the manual \`Write-Host $err.Exception.Message -ForegroundColor Red\`
loops from the main event handler. They duplicated the
visible-console output line-for-line under the new wiring (Out-Host
inside the Tee pipe already streamed everything in real time).
Terminating-exception messages from the inner catch still get a
manual Write-Host pass because they bypass the streaming pipeline.
Smoke-tested against a real PowerShell.MCP.dll: confirmed
PipelineItems contains output and ErrorRecord items in emit order;
Warning, Information, ConsoleOut, ConsoleErr buckets each captured
their respective signals; visible console rendering preserved
real-time streaming and per-stream coloring (Write-Host Red,
Write-Warning Yellow, Write-Error red+cyan, native cmd /c stderr
red). 251 xunit unit tests still pass with no expected-value
adjustments. Pester integration tests target the cmdlets, not the
polling engine, so they're unaffected.
A test for the new behavior (assert PipelineItems chronology +
Console.Out/Err capture) is deferred to a follow-up commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>1 parent c79b55d commit 14f0aa3
2 files changed
Lines changed: 339 additions & 196 deletions
0 commit comments