Skip to content

fix(pm): net ci-failure.mjs's walk against mid-walk transport failures - #10361

Merged
os-zhuang merged 2 commits into
mainfrom
claude/issue-10155-ci-failure-transport-net
Aug 20, 2026
Merged

fix(pm): net ci-failure.mjs's walk against mid-walk transport failures#10361
os-zhuang merged 2 commits into
mainfrom
claude/issue-10155-ci-failure-transport-net

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes #10155

rest() throws a plain Error on any non-ok status, and before this nothing between it and the top level caught. Node exits 1 on an uncaught exception — and this file's own exit table assigns exit 1 to RED, "failing checks, and the assertion text was retrieved for EVERY one of them. The output is the answer."

So a container that could not read one byte about the tree handed its caller the code meaning "the tree is red and here is the proof" — to a caller this file's own header instructs to branch on $? (Piping hides all of it. Read $?). That is the #4690 inversion this file exists to refuse, arriving through the one path it did not guard.

#9966's probe does not cover this: it fires once, at the start, so a quota exhausted between it and the last annotations call, an access that changes under the run, a transient 5xx, or a 404 on an annotations URL all arrive behind it.

Reproduced before, measured after

Probe green, walk's first page refused, against main at 2e39181a (post-#10275, post-#9966). No socket opened — fetch is stubbed so the measurement is deterministic:

Error: GET /repos/objectstack-ai/objectstack/commits/2e39181a.../check-runs?per_page=100&page=1 -> HTTP 403
    at rest (.../ci-failure.mjs:694:19)
    at async checkRunsFor (.../ci-failure.mjs:705:18)
    at async walk (.../ci-failure.mjs:776:15)
EXIT=1                          <- RED. Nothing was read.

After, same harness: EXIT=2, and the report says so in words. A second harness that revokes access under the run — the card's "a repo whose access changes under the run" — lands EXIT=3.

The fork, settled by asking rather than by picking

Triage handed the exit-2-vs-3 choice down ("either candidate is honest; the current behaviour is the only wrong answer"). I took neither as a constant, because the causes are not one class and each constant is wrong for half of them:

mid-walk cause honest answer
quota exhausted mid-run; access changed under the run the environment cannot answer ⇒ exit 3
transient 5xx; one 404 on an annotations URL the container is fine, the walk cannot answer ⇒ exit 2

So the transport is re-probed on the way out and its fresh verdict picks. A fresh reading is exactly what distinguishes a real transport failure from a transient status on one page. scripts/pm/check-half-states.mjs's in-loop net makes the identical call for the identical reason — this reuses that decision rather than inventing a second one beside it. An unclassifiable re-probe (a repo-scoped 5xx) deliberately falls to 2: the classifier's narrowness is the point, and a loud undetermined beats a confident wrong claim about someone's container.

Header consistency. Exit 3 previously promised it was decided before anything is read. That sentence is rewritten: exit 3 now promises a classified transport verdict, not an early one. The displaced promise was worth less than the guarantee it cost, because a transport that dies at request 40 classifies the environment exactly as much as one dead at request 1.

The partial-output invariant, in both places it was broken

  1. The net's report carries the swept analogue: it says whether nothing was read or nothing was judged, and counts the check-runs already listed — so a stopped walk cannot read as a completed one.

  2. assertionStatus gains a fourth answer. A refused annotations call leaves the same empty list a check that recorded nothing leaves, and reading it as none printed this, four lines apart, on a check measured to carry annotations_count: 1:

    annotations  RETRIEVAL FAILED: ... -> HTTP 403
                 that is not the same as "the check recorded nothing".
    assertion    NONE — the check carried no annotation with any content in it.

The second sentence is false and is precisely what the first forbids. It now reads NOT RETRIEVED — ... an absence of a READING, not an absence of evidence. The blocked-log-blob paragraph no longer fires either, since it blamed the egress policy for a refusal that happened on plain REST.

Verification

Every exit code captured before any pipe — cmd > file 2>&1; EXIT=$? — since | tail reports the pipe's status, and getting that wrong while fixing this file would be its own joke.

Gates re-derived at the merged head with node scripts/pm/dispatch-gates.mjs (no args — it takes its own change set from the merge base), all green at 8025ba82:

The new pins were shown to fail, not merely to pass. Two ablations, each confirmed on disk by grep-counting the removed text and an injected marker, each restored and re-confirmed:

  • removing the unretrievable branch ⇒ 2 failures, exactly the two unretrievable pins; the none control pin stayed green, which is what makes the fixture a pin rather than a restatement.
  • making midWalkVerdict return EXIT_RED (what node did before) ⇒ 11 failures, the load-bearing one reading expected [false,false,false] / actual [true,true,true] — the defect verbatim.

The zero-hit control-character sweep is backed by a positive control: the identical grep -naP fires (exit 0) on a scratch file carrying \x01 and finds nothing (exit 1) in the changed file.

Notes

  • Changeset: none. Root scripts/ publishes nothing ⇒ skip-changeset, applied additively.
  • Clause ②: not triggered. The path limb is packages/spec/src/** and this is not that; on content, this changes a seat tool's own exit vocabulary — no product contract, no accept/reject behaviour of any spec/Zod schema, no widened authorable surface. assertionStatus and midWalkVerdict have no importers outside this file (grepped: only render and the self-test).
  • Scope: one file, scripts/pm/ci-failure.mjs. Note git diff origin/main --stat (two-dot) reports 13 files here because origin/main advanced from 2e39181a to 128684d5 mid-task; the merge-base diff is one file.

Generated by Claude Code

claude added 2 commits August 20, 2026 16:31
A transport failure arriving after the pre-walk probe passed escaped as an
uncaught throw, and node's exit 1 collides with this file's own EXIT_RED --
so a container that read nothing about the tree handed its caller the code
that means "the tree is red and here is the proof".

Every network read after the probe is now wrapped. The exit-2-vs-3 fork is
settled by re-probing rather than by picking a constant: an unusable fresh
reading is the environment (3), a healthy one is a walk that cannot answer
(2), and an unclassifiable one falls to 2 rather than blaming the container.
Never 1, never 0.

The partial-output invariant rides along, twice: the net's report says
whether nothing was READ or nothing was JUDGED and counts what had been
listed, and assertionStatus gains a fourth answer so a refused annotations
call stops printing as "the check carried no annotation with any content in
it" -- four lines under this same block's own disclaimer.

Fixes #10155

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DdCnBGcHeufjrq7drTD3wt
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 20, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review August 20, 2026 16:42
@os-zhuang
os-zhuang enabled auto-merge August 20, 2026 16:43
@os-zhuang
os-zhuang added this pull request to the merge queue Aug 20, 2026
Merged via the queue into main with commit 6076abf Aug 20, 2026
25 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-10155-ci-failure-transport-net branch August 20, 2026 17:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci-failure.mjs's walk has no transport net: a mid-walk 403 escapes as an uncaught throw, and node's exit 1 collides with this file's own EXIT_RED

2 participants