Skip to content

Commit a865332

Browse files
yotsudaclaude
andcommitted
feat(streams): merge Warning into chronological pipelineStream
Follow-up to 4273771. The earlier hybrid layout left Warnings in a dedicated \`=== WARNINGS ===\` section because of a misread of the stream-merge color behavior — I'd assumed Warning would behave like Information (lose color when merged) and didn't test 3>&1 separately. A direct experiment showed the assumption was wrong: WarningRecord has its own type-specific Out-Host formatter that emits the canonical yellow \`WARNING: \` prefix regardless of which stream the record arrived on, the same shape as VerboseRecord and DebugRecord. So merging stream 3 is safe and brings Warnings into the chronological pipelineText alongside Output, Error, Verbose, and Debug. Now an AI command body that emits e.g. Write-Output "step-A" Write-Warning "low-disk" Write-Output "step-B" Write-Error "perm-denied" surfaces in the AI response as a single time-ordered block: step-A WARNING: low-disk step-B perm-denied The "low-disk warning happened between step-A and step-B" context is no longer collected at the end of the response — it sits where it actually fired in the run. Only stream 6 (Information / Write-Host) remains UN-merged because InformationRecord carries Write-Host's user-chosen ConsoleColor and Out-Host's generic record formatter doesn't read it back — the visible-console color would silently flatten if merged. Changes: * Stream merge widened: \`2>&1 4>&1 5>&1\` → \`2>&1 3>&1 4>&1 5>&1\`. * \`-WarningVariable\` dropped from Invoke-Captured. Without it, Warning records flow through the merge cleanly with no double-counting (capturing into both -WarningVariable AND the redirected stream would record each warning twice). * \`Warning\` field removed from the StreamResults hashtable. * Format-McpOutput renders WarningRecord items inline as \`WARNING: msg\` and the warningCount for the status-line tag accumulates from PipelineItems instead of from a separate bucket. * \`=== WARNINGS ===\` section gone from the response — Warnings are inline now, not bucketed. * Comment in MCPPollingEngine.ps1 updated to call out which streams merge and why; the previous comment incorrectly grouped Warning with Information as "merge breaks color." Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4273771 commit a865332

1 file changed

Lines changed: 36 additions & 45 deletions

File tree

PowerShell.MCP/Resources/MCPPollingEngine.ps1

Lines changed: 36 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,6 @@ if (-not (Test-Path Variable:global:McpTimer)) {
168168
# side. The tee writes to BOTH the original
169169
# writer (preserves real-time visible-console
170170
# output) AND a StringBuilder for capture.
171-
$warningVar = @()
172171
$informationVar = @()
173172
$exceptionVar = @()
174173
$pipelineStream = @()
@@ -218,31 +217,28 @@ if (-not (Test-Path Variable:global:McpTimer)) {
218217
try {
219218
try {
220219
$sb = [scriptblock]::Create($Command)
221-
# Stream merge map: 2 (Error), 4 (Verbose), 5
222-
# (Debug) all merge into stream 1 so the
223-
# chronological tee captures them. Stream 6
224-
# (Information) and 3 (Warning) stay UN-merged
225-
# because:
226-
# * 6 carries Write-Host's chosen ConsoleColor
227-
# in the InformationRecord and merging would
228-
# route rendering through Out-Host's default
229-
# formatter which doesn't read the color
230-
# back — the user-visible Write-Host color
231-
# would silently flatten to default.
232-
# * 3 has its own yellow auto-render that
233-
# Out-Host preserves when the WarningRecord
234-
# stays on stream 3, but loses when merged.
235-
# 4 and 5 don't have this problem: their
236-
# auto-render produces the yellow "VERBOSE: " /
237-
# "DEBUG: " prefix as part of Out-Host's
238-
# VerboseRecord / DebugRecord handler, which
239-
# fires whether the records arrive on the
240-
# original stream or merged into stream 1.
241-
# Tested both directions before settling on
242-
# this merge layout.
220+
# Stream merge map: 2 (Error), 3 (Warning), 4
221+
# (Verbose), 5 (Debug) all merge into stream 1
222+
# so the chronological tee captures them in
223+
# emit order. Only stream 6 (Information) stays
224+
# UN-merged because the InformationRecord
225+
# carries Write-Host's user-chosen
226+
# ConsoleColor as a property, and Out-Host's
227+
# generic record formatter doesn't read it back
228+
# when rendering merged records — Write-Host's
229+
# color would silently flatten to default.
230+
# Streams 2/3/4/5 each have their own
231+
# type-specific Out-Host formatter that emits
232+
# the canonical colored prefix (red+cyan for
233+
# ErrorRecord, yellow `WARNING:` for
234+
# WarningRecord, yellow `VERBOSE:` /
235+
# yellow `DEBUG:` for the corresponding
236+
# records) regardless of which stream the
237+
# record arrived on, so merging is safe for
238+
# them. Tested all four before settling on
239+
# this layout.
243240
Invoke-Captured -Block $sb `
244-
-WarningVariable +warningVar `
245-
-InformationVariable +informationVar 2>&1 4>&1 5>&1 |
241+
-InformationVariable +informationVar 2>&1 3>&1 4>&1 5>&1 |
246242
Tee-Object -Variable pipelineStream |
247243
Out-Host
248244
# Capture post-pipeline state IMMEDIATELY. Any
@@ -307,7 +303,6 @@ if (-not (Test-Path Variable:global:McpTimer)) {
307303

308304
return @{
309305
PipelineItems = $dedupedStream
310-
Warning = $warningVar
311306
Information = $informationVar
312307
Exception = $exceptionVar
313308
ConsoleOut = $consoleOutBuf.ToString()
@@ -428,6 +423,7 @@ if (-not (Test-Path Variable:global:McpTimer)) {
428423
# "=== ERRORS ===" section.
429424
$pipelineLines = @()
430425
$errorCount = 0
426+
$warningCount = 0
431427
foreach ($item in $StreamResults.PipelineItems) {
432428
if ($item -is [System.Management.Automation.ErrorRecord]) {
433429
$errorCount++
@@ -436,6 +432,11 @@ if (-not (Test-Path Variable:global:McpTimer)) {
436432
# `Write-Error: ` prefix and trace context that
437433
# are PowerShell's own decoration).
438434
$pipelineLines += $item.Exception.Message
435+
} elseif ($item -is [System.Management.Automation.WarningRecord]) {
436+
$warningCount++
437+
# Mirrors PowerShell's own visible render which
438+
# prefixes WARNING: in yellow.
439+
$pipelineLines += "WARNING: " + $item.Message
439440
} elseif ($item -is [System.Management.Automation.VerboseRecord]) {
440441
# Mirrors PowerShell's own visible render which
441442
# prefixes VERBOSE: in yellow. AI side gets the
@@ -466,17 +467,6 @@ if (-not (Test-Path Variable:global:McpTimer)) {
466467
}
467468
$exceptionText = ($exceptionLines -join "`n").Trim()
468469

469-
# Process warnings.
470-
$warningLines = @()
471-
foreach ($warn in $StreamResults.Warning) {
472-
$warningLines += if ($warn -is [System.Management.Automation.WarningRecord]) {
473-
$warn.Message
474-
} else {
475-
$warn.ToString()
476-
}
477-
}
478-
$warningText = ($warningLines -join "`n").Trim()
479-
480470
# Process information / Write-Host. Skip empty/whitespace
481471
# records (PowerShell sometimes emits a blank
482472
# InformationRecord at pipeline boundaries).
@@ -579,7 +569,7 @@ if (-not (Test-Path Variable:global:McpTimer)) {
579569
# HostWrite.
580570
$known = [System.Collections.Generic.HashSet[string]]::new(
581571
[System.StringComparer]::Ordinal)
582-
foreach ($src in @($pipelineText, $exceptionText, $warningText, $infoText, $consoleOutText, $consoleErrText)) {
572+
foreach ($src in @($pipelineText, $exceptionText, $infoText, $consoleOutText, $consoleErrText)) {
583573
if (-not [string]::IsNullOrEmpty($src)) {
584574
foreach ($line in $src -split "`r?`n") {
585575
$trimmed = $line.Trim()
@@ -629,7 +619,9 @@ if (-not (Test-Path Variable:global:McpTimer)) {
629619

630620
# Calculate statistics.
631621
$errorCount += $StreamResults.Exception.Count
632-
$warningCount = $StreamResults.Warning.Count
622+
# warningCount accumulates as we walked PipelineItems above
623+
# — Warning records arrive on stream 3 and merge into
624+
# pipelineStream via 3>&1, so they're counted there.
633625
$infoCount = $infoLines.Count
634626
$hasErrors = $errorCount -gt 0
635627
# LastExitReport is 0 when the invocation did not
@@ -717,12 +709,11 @@ if (-not (Test-Path Variable:global:McpTimer)) {
717709
$sections += ""
718710
}
719711

720-
if ($warningText) {
721-
$sections += "=== WARNINGS ==="
722-
$sections += $warningText
723-
$sections += ""
724-
}
725-
712+
# Warning records now interleave inline in pipelineText
713+
# via the 3>&1 stream merge, so the dedicated
714+
# `=== WARNINGS ===` section is gone — the AI sees
715+
# `WARNING: msg` in its actual position relative to
716+
# surrounding output / errors / verbose / debug.
726717
if ($infoText) {
727718
$sections += "=== INFO ==="
728719
$sections += $infoText

0 commit comments

Comments
 (0)