Skip to content

fix(health): report what could not be observed as unknown (#537) - #629

Merged
raylei50653 merged 9 commits into
mainfrom
fix/537-health-cli
Oct 25, 2026
Merged

raylei50653 merged 9 commits into
mainfrom
fix/537-health-cli

Conversation

@raylei50653

Copy link
Copy Markdown
Owner

#537 health CLI:取不到的不算健康(REQ-535-07-03)

方案紀錄:#537 comment 6367964691(選 A)。內容寫在 failure_degradation_537.md §24。

src/saccade/pipeline/health.py 是 saccade_ctl.sh health 執行的 health CLI,orchestrator 啟動時也會在 process 內跑一次並印出(SE-48)。修復前在 main@5aa63ee5 上重現到以下問題:

  • NVML 失敗、其餘檢查正常時,報告寫 Saccade Status — OK。
  • Redis 連不上時,queue depth 報 0 items。
  • 不存在的 user unit 被報成 inactive/failed。
  • Loop Latency 與 Fragmentation 量的是 health process 自己,報告沒有標示。
  • 報告為 DEGRADED 時 exit 仍是 0。

改了什麼

  • 三態檢查:每項檢查回報 ok、degraded 或 unknown,並附來源。
    • systemd:改讀 systemctl --user show -p LoadState,ActiveState。not-found、systemctl 不存在、逾時或非零 exit 都是 unknown。
    • NVML:GPU 0 讀不到時是 unknown,並保留錯誤類型。只有 init 成功才呼叫 shutdown,查詢失敗時也會呼叫。
    • Redis:連不上是 degraded,queue depth 為 None。
  • 整體狀態:DEGRADED 優先於 UNKNOWN,UNKNOWN 優先於 OK;overall_ok 只在 OK 時為真。
  • REDIS_URL 只在確認安全時顯示:
    • 只去掉 netloc 裡的 userinfo,其餘照原文保留,整段必須全是純 ASCII 的 URL 字元,否則印 <unparseable URL>。
    • REDIS_URL 本身不是純字元時,Redis 錯誤訊息只印例外類型。
  • stress metrics:標明是本 process 的數值、不計入狀態;沒有配置時印 n/a,不印 0。
  • 不改:exit code(維持 0,不選 readiness policy)、unit 清單、VRAM_WARN_PCT、orchestrator、ResourceManager。

審查(Codex,綁定 head)

輪 head verdict 處置
r1 2b120bf6 NOT-READY 不合法 port 讓 render() 丟例外(MAJOR);IPv6 方括號被拿掉、門檻測試依賴環境變數(MINOR)。三項都已修正
r2 1368913b NOT-READY 沒有 scheme:// 的 URL 印出密碼;redis-py 的解析錯誤經 detail 印出密碼(MAJOR×2)。已修正
r3 3486a7be NOT-READY 拆不開的 userinfo、全形 @ 繞過 denylist(MAJOR×2);測試讀環境中的 REDIS_URL(MINOR)。改用 allowlist
r4 bc8e3780 NOT-READY 密碼中的 ?/# 把 @host 推出 netloc(MAJOR)。已修正
r5 60c2ddb2 NOT-READY 全形 @ 加 ?/# 的組合(MAJOR)。改成不丟棄任何部分就檢查整段
r6 d9e949d1 READY-WITH-CHANGES urlsplit 刪 tab 造成顯示錯位(MINOR,沒有外洩)。改從原始字串切 netloc
r7 2d0b2834 READY-TO-MERGE findings: none

測試

  • tests/unit/test_pipeline_health_contract_537.py(新增,88 個案例)與 tests/unit/test_pipeline_health.py(3 個):91 passed。另外在 VRAM_WARN_PCT=10、REDIS_URL=redis://u:s3cret@x:1/0 兩種環境下都是 91 passed。全程 host-only,不碰真實的 systemctl、NVML、Redis 或 CUDA(唯一例外是用真實 redis-py 解析器在連線前拒絕的兩個 URL)。
  • 修復前:只補三個常數名稱讓測試能 import,結果是 89 failed/2 passed。多數失敗來自介面改變,行為上的重現以 §24 的直接執行為準。
  • Mutation:18 個隔離 mutation 都被攔住,包括退回 r1/r4/r5 各版的 redacted_url;共享的 health.py SHA-256 前後相同。
  • 真實 smoke @ 2d0b2834:Redis 未啟動、沒有安裝 user units,報告為 DEGRADED;三個 unit 顯示 ? unknown: no such user unit,queue depth 為 unknown,Fragmentation 為 n/a,exit 0。
  • pre_push @ 3486a7be:6010 passed,4 failed,都是 coordinate 落後時預期會失敗的 identity 測試(test_research_lock ×2、test_runtime_identity_staleness ×2)。

Runtime identity

health.py 是 decision_relevant。座標由 stacked PR 重新發佈(base = 本分支),審完後先 merge 進本分支,再由本 PR 原子落地。只有 implementation 軸改變。

未驗證

  • 沒有在真實部署上跑過:system 或 user units 實際執行中、Redis 有資料時。
  • 沒有 orchestrator 的端到端驗證;多 GPU 只看 device 0。
  • REQ-535-07-03 仍是 candidate,exit code 不代表 readiness。
  • 不取得 online-service qualification、不部署。

Refs #537

🤖 Generated with Claude Code

raylei50653 and others added 8 commits October 23, 2026 23:10
The health CLI (saccade_ctl.sh health; printed once by the orchestrator at
start) reported an NVML failure as OK, an unreachable Redis as a queue
depth of 0 and a user unit that does not exist as inactive/failed, and
showed its own loop latency and allocator as stress metrics without
saying so. REQ-535-07-03 asks that a caller can tell unknown from healthy
and see where each reading comes from.

Each check is now ok / degraded / unknown with its source: systemd reads
LoadState/ActiveState of the user unit (not-found or a systemctl that
cannot answer is unknown), NVML GPU 0 that cannot be read is unknown with
its error, Redis that cannot be reached is degraded with no queue depth.
The status is DEGRADED when any check is degraded, else UNKNOWN when any
is unknown, else OK; overall_ok only for OK. The Redis URL is shown
without credentials. The exit code stays 0: no readiness policy is chosen.

Docs: failure_degradation_537.md §24.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex r1 (2b120bf) NOT-READY:
- MAJOR: redacted_url() read parts.port, which raises ValueError on an
  invalid or out-of-range port, so render() failed -- and with it the
  orchestrator's startup report. Credentials and the query are now cut
  from the URL text and host:port is shown as written; a URL that cannot
  be split shows as <unparseable URL>.
- MINOR: IPv6 hosts lost their brackets; they are kept now.
- MINOR: the threshold tests read VRAM_WARN_PCT from the environment;
  they pin it.

Tests: 11 redaction cases and a render with an invalid URL. Mutants 10
(the first redaction) and 11 (keeping the query) are caught; §24 counts
updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex r2 (1368913) NOT-READY, two MAJORs:
- A REDIS_URL without scheme:// (user:pw@host:6379/0) was split with the
  userinfo in the path and printed. Such a URL is now shown only when it
  has no '@', '?' or '#'; otherwise <unparseable URL>.
- redis-py's URL parse error quotes the whole netloc, and the report
  printed it as the Redis detail. When REDIS_URL may carry secrets ('@',
  '?' or '#'), the detail is the error type only.

Tests: six more redaction cases, the real redis-py parser refusing a
fullwidth-slash URL (no connection), and the hidden/kept message rule.
Mutants 12-14 are caught; §24 counts updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex r3 (3486a7b) NOT-READY:
- MAJOR: userinfo the split leaves in place (redis://u:pw/x@h,
  redis:///u:pw@h) was printed.
- MAJOR: a fullwidth '@' passed the '@?#' check, and redis-py's parse
  error then printed the password as the Redis detail.
- MINOR: two tests read REDIS_URL from the environment.

Three rounds of denylist patches still leaked, so the rule is now an
allowlist: after userinfo, query and fragment are cut, the URL is shown
only when it is plain ASCII scheme/host/port/path characters
(alphanumerics and "+-._~:/[]"); otherwise <unparseable URL>. The Redis
error message is shown only when REDIS_URL itself is plain; otherwise
the error type. Both test modules pin REDIS_URL.

Tests: a whole-report regression over 12 password-bearing URLs whose
error quotes the URL, the real parser refusing a fullwidth '@', five
more redaction cases. 16 mutants caught; §24 updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex r4 (bc8e378) NOT-READY, one MAJOR: an unescaped '?' or '#' in a
password (redis://user:pw?x@host) moves '@host' into the discarded query
or fragment, and the remaining user:pw passed the allowlist. A URL with
an '@' anywhere outside its netloc is now <unparseable URL>.

Tests: three redaction cases and two whole-report URLs; mutant 17 is
caught; §24 updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex r5 (60c2ddb) NOT-READY, one MAJOR: redis://user:pw?x@host
(fullwidth '@') passed both the '@' count and the allowlist, because the
query or fragment holding the look-alike was dropped before the check.

redacted_url() now cuts only the netloc's userinfo and keeps the rest as
written; the whole result must be plain, so nothing is discarded before
the check. A URL with a query or fragment is therefore never shown. The
'@' count check is redundant under this rule and is removed.

Tests: the '@' + '?'/'#' combinations in the redaction and whole-report
lists, an uppercase scheme; two expectations change (a URL with a query
or fragment is now <unparseable URL>). 17 mutants caught, among them the
r4 version; §24 updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex r6 (d9e949d) READY-WITH-CHANGES, one MINOR: urlsplit drops tabs
and newlines, so slicing the input by its netloc length misaligned and
redis://u:pw\t@cache.local:6379/0 showed port 63799 (no leak). The
netloc is now cut from the input up to the first '/', '?' or '#'.

Tests: tab/newline in userinfo and host, a URL without a path. 18
mutants caught, among them the r5 version; §24 updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fresh capture on the implementation head 2d0b283 (clean tree, own
build/runtime_identity, --run-probe --require-complete under the gpu0
lease):

- implementation: ab1bc4ea82d70131 -> 4ad5ae2c7f2a7d2c; 317 -> 317 files,
  only src/saccade/pipeline/health.py changed (the health CLI's
  ok/degraded/unknown report).
- decision_surface, environment, identity_semantics, runtime_inputs:
  recomputed, unchanged.
- probe: rerun, not reused; digest 2dabed0b... unchanged. Probe equality
  is not an equivalence claim; equivalence stays unproven.

The outgoing canonical is archived as
runtime_identity.2026-10-23.ab1bc4ea82d70131.json (byte-identical to
origin/main's). check_runtime_identity_staleness.py --mode attested exits
0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-identity

docs(runtime): republish the runtime coordinate for #537 health CLI
@raylei50653

Copy link
Copy Markdown
Owner Author

Self-check(#550 §0A 合併條件 1–5)

head:c91d665fd0e716918e1d08fe6149fc0a6f10176c(2d0b2834 實作與 #630 座標 30dbef82 的 merge;merge tree f2e47f32 與審過的 30dbef82 tree 相同)。base origin/main = 5aa63ee5 是 head 的祖先,所以合併後的 tree 與 head tree 相同。

  • 1. 範圍與審查:範圍與 architecture(C): define capability failure, fallback and runtime degradation contracts #537 comment 6367964691(選 A)一致,exit code 與 readiness policy 不變。Codex 審查:

  • 2. CI 與負控制:merged head c91d665f 的 CI 8/8 綠,都是 attempt 1:

    第一次失敗分類:run 39981363209 @ 2d0b2834(只有實作)pytest 有 4 個失敗,結果是 5985 passed/4 failed。這是座標落後時預期會出現的中間態:test_runtime_identity_staleness.py 2 個、test_research_lock.py 2 個,訊息為 implementation moved and was not republished: published ab1bc4ea… recomputed 4ad5ae2c…。合入 docs(runtime): republish the runtime coordinate for #537 health CLI #630 後轉為 5989 passed,不是靠重跑轉綠。

    負控制:

    • 修復前 89 failed/2 passed。
    • 18 個隔離 mutation 都被攔住,包括退回 r1、r4、r5 的 redacted_url。
  • 3. Runtime identity:health.py 是 decision_relevant。docs(runtime): republish the runtime coordinate for #537 health CLI #630 在 controlled host 做了 fresh capture:

    • implementation 軸 ab1bc4ea → 4ad5ae2c,317 → 317 檔,只有 health.py 不同。
    • 其餘四軸與 probe digest 不變,equivalence=unproven。
    • 座標與實作在同一次 merge 原子落地。
    • pre_push @ 30dbef82 6045 passed/0 failed;--mode attested exit 0。
  • 4. 真實環境:真實 smoke @ 2d0b2834,在本機 WSL host 上用真實的 systemctl --user、NVML 與 redis-py:

    • Redis 未啟動、沒有安裝 user units,報告為 DEGRADED。
    • 三個 unit 都顯示 ? unknown: no such user unit。
    • VRAM 讀到 NVML GPU 0(0.3GB/11.9GB)。
    • queue depth 為 unknown,Fragmentation 為 n/a,exit 0。

    證據放在 ~/.local/state/saccade/evidence/537-health-cli/20261023T231059/,sha256 清單見 docs(runtime): republish the runtime coordinate for #537 health CLI #630 描述(smoke_2d0b2834.txt 為 5ccbbf8a…)。沒有在真實部署(units 執行中、Redis 有資料)上驗證,也沒有完整 online service,因此不升級 qualification。

  • 5. 本 comment。

剩餘 OPEN:

Rollback/拒絕路徑:revert 本 PR 的 merge commit,再依 runbook 重新 capture 並出版座標,不直接恢復舊 canonical 冒充 current。Owner 若否決方案 A,依此路徑回退。

合併指令(需 owner 執行):gh pr merge 629 --merge --match-head-commit c91d665fd0e716918e1d08fe6149fc0a6f10176c

@raylei50653
raylei50653 merged commit 3375e7d into main Oct 25, 2026
8 checks passed
@raylei50653
raylei50653 deleted the fix/537-health-cli branch October 25, 2026 02:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant