diff --git a/agent99-control-plane.ps1 b/agent99-control-plane.ps1 index 6ccd58a62..03787a7a5 100644 --- a/agent99-control-plane.ps1 +++ b/agent99-control-plane.ps1 @@ -3795,13 +3795,14 @@ function Test-AgentRecentTelegramDelivery { continue } if (-not $data) { continue } - $allAttempts = @($data.telegram) + $allAttempts = @($data.telegram | Where-Object { $null -ne $_ }) $attempts = @($allAttempts | Where-Object { - -not ($_.PSObject.Properties["suppressed"] -and $_.suppressed -eq $true) + $_ -and -not ($_.PSObject.Properties["suppressed"] -and $_.suppressed -eq $true) }) if ($attempts.Count -eq 0) { continue } $sent = @($attempts | Where-Object { + $_ -and $_.sent -eq $true -and $_.relay -and $_.relay.ok -eq $true -and diff --git a/apps/api/tests/test_agent99_transport_recovery_deploy_contract.py b/apps/api/tests/test_agent99_transport_recovery_deploy_contract.py index d640f9ba7..b0062591f 100644 --- a/apps/api/tests/test_agent99_transport_recovery_deploy_contract.py +++ b/apps/api/tests/test_agent99_transport_recovery_deploy_contract.py @@ -454,6 +454,8 @@ def test_agent99_selfhealth_ignores_suppressed_lifecycle_attempts() -> None: assert 'PSObject.Properties["suppressed"]' in function assert "$_.suppressed -eq $true" in function assert "$_.relay.durableAck -eq $true" in function + assert '$data.telegram | Where-Object { $null -ne $_ }' in function + assert '$_ -and -not ($_.PSObject.Properties["suppressed"]' in function def test_agent99_object_reader_preserves_ordered_callback_identity() -> None: diff --git a/docs/operations/agent99-host112-host111-recovery-readback-2026-07-15.snapshot.json b/docs/operations/agent99-host112-host111-recovery-readback-2026-07-15.snapshot.json index 3d9104972..1d160a52b 100644 --- a/docs/operations/agent99-host112-host111-recovery-readback-2026-07-15.snapshot.json +++ b/docs/operations/agent99-host112-host111-recovery-readback-2026-07-15.snapshot.json @@ -67,6 +67,15 @@ "legacy_text_reuse_false_green_blocked": true, "raw_visual_stored": false }, + "self_health_null_telegram_row_guard": { + "status": "source_verified_not_deployed", + "powershell5_replay_terminal": "verified", + "input_rows": 3, + "non_null_rows": 2, + "actionable_rows": 1, + "durably_sent_rows": 1, + "runtime_write_performed": false + }, "github_used": false }, "secret_value_read": false, diff --git a/docs/runbooks/FULL-STACK-COLD-START-SOP.md b/docs/runbooks/FULL-STACK-COLD-START-SOP.md index d63b9ce3d..96caeb354 100644 --- a/docs/runbooks/FULL-STACK-COLD-START-SOP.md +++ b/docs/runbooks/FULL-STACK-COLD-START-SOP.md @@ -2770,3 +2770,8 @@ Windows99 原本已能產生 1200x760 PNG,但 canonical lifecycle ingress 只 仍必須回 `visual_sent=false`,不得假綠。 - 圖片渲染或傳輸失敗時文字 lifecycle 仍可送達,但 terminal 只能是 partial/degraded, `visualError` 與 `visual_delivery_status` 必須留在 evidence,待同一 incident 後續修復。 +- SelfCheck 讀取舊 evidence 時,`telegram[]` 可能含 JSON `null`。必須先排除 null row, + 再判斷 `suppressed / sent / relay.durableAck`;不得對 null 直接取 + `PSObject.Properties[...]`。這類 parser 錯誤即使 process exit 0 仍算 self-health defect, + 必須由 Windows PowerShell 5 replay 證明 `null + suppressed + sent` 只留下單一 actionable + sent receipt,不能靠壓掉 stderr 或只看排程 LastTaskResult=0 關閉。