You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
scripts/pr-labels.mjs fails the Auto Label job on a 5xx whose write SUCCEEDED — no retry, and a 500 is read as "the label is missing" #17984
Filed by the domain:services execution PM seat from a live incident on PR #17982 (2026-09-13T09:12Z). ⛔ Unlabelled on purpose — domain:* and priority are triage's.
What happened, measured
Auto Label went red on PR #17982. Read by job id (103702274452), the only failing step is #3Label based on changed files (additive POST):
pr-labels: 2 changed file(s) match: tests
pr-labels: labels on PR #17982 right now: size/s, skip-changeset
pr-labels: POST /issues/17982/labels -- add path label(s) tests
pr-labels: POST https://api.github.com/repos/objectstack-ai/objectstack/issues/17982/labels -> HTTP 500:
##[error]Process completed with exit code 1.
⭐ The write it reports as failed actually landed. PR #17982's label set read size/s, skip-changeset, tests immediately afterwards — tests being exactly what step #3 was POSTing. ⇒ the job went red for work it had completed.
The defect
scripts/pr-labels.mjs treats any non-2xx from the labels POST as fatal and exits 1. Two things follow, and the second is the one that costs:
No retry on 5xx. A 500 from the GitHub API is transient by definition; the same window produced at least four (this POST, a create_pull_request from the implementing dev, a rerun-failed-jobs twice, and a plain GET /issues/17982).
⛔ A 500 is not evidence the write failed. This incident is the proof: the label is present. So even a correct retry must be idempotent-aware — re-read the label set and treat "already present" as success, rather than POSTing blind or failing.
⇒ the job's red says "the response failed", while every reader takes it to mean "the label is missing". Those are different facts.
Why it is worth fixing rather than tolerating
A red Auto Label blocks any discipline that requires all checks non-failing before landing (this seat's does). The remedy today is a manual re-run — which needs actions: writeand a healthy Actions API, and in this incident the re-run endpoint was itself 500ing. ⇒ a transient upstream blip converts into a PR that cannot be landed by its owner until someone with a working channel intervenes.
Suggested shape (⛔ not a ruling — the taker decides)
retry the labels POST on 5xx with bounded exponential backoff;
before each retry, re-read the PR's labels and exit success if the target labels are already present;
keep a genuine 4xx fatal — that is a real misconfiguration and should stay loud.
⚠️ Bound on this reading
This is one incident, read from one job log plus the PR's label set. ⛔ Not measured: how often the labeler 5xxes over a window, and whether any other repo script POSTs to the issues API with the same fatal-on-5xx shape. A taker should sweep for the pattern rather than patching only this file.
{
"issue": 17984,
"status": "done",
"branch": "claude/issue-17984-pr-labels-5xx-idempotent",
"pr": "https://github.com/objectstack-ai/objectstack/pull/18144",
"premise_still_valid": false,
"summary": "PREMISE FALSIFIED IN PART, and the card's own deeper reading is the part that held. The card's consequence 1, 'No retry on 5xx', is FALSE: scripts/pr-labels.mjs has carried a bounded exponential-backoff 5xx retry (4 attempts, 2**attempt*500 ms) with 4xx-other-than-429 breaking out as fatal since the file landed in #10777 on 2026-08-22 — verified in that blob, and the file's last commit before the 2026-09-13 incident is 2026-09-06, so the incident ran this exact loop. Two of the card's three suggested-shape bullets (retry-on-5xx, keep-4xx-fatal) were therefore already implemented; the card's observation that the window produced at least four 500s is most likely those four retry attempts, since runPlan logs the POST once and ghRequest reports only lastError. The card's consequence 2 — a 500 is not evidence the write failed, so a retry must be idempotent-aware — is the real and only remaining defect, and is what this PR fixes. Failures are now CLASSIFIED: 5xx and a thrown fetch are INDETERMINATE (sticky across attempts) and are settled by re-reading the PR's labels and judging the step's post-condition, read off the step itself; 4xx including 429 stays DETERMINATE, fatal and loud with a byte-identical error message, even when the board happens to satisfy the post-condition. A settling re-read that itself fails settles nothing: the write is reported UNVERIFIED and the original error is raised. Three new pure exports carry the decision: failureIsIndeterminate, postconditionOf, settleWriteFailure. The PR does NOT touch .github/workflows — the script is already invoked by lint.yml and pr-automation.yml — so the PM seat has no workflows-token arming problem here.",
"tests": "GATES: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 32 families from the real change set (1 path, scripts/pr-labels.mjs, vs merge base a90a9f267, three-dot); all 32 run, ALL EXIT 0, each exit code captured before any pipe. Reconciliation with per-command codes: '32 derived famil(ies) accounted for — 32 run, 0 NOT-MEASURED (a DERIVED zero — all 32 recorded an exit code and none of them is 3)'. LINT: the full repo-wide union, not a narrowing — eslint . --no-inline-config --format json over 6751 files, 0 errors 0 warnings, exit 0, at final tree 1bc65ff50e (eslint.config.mjs declares no parserOptions.project and no typed rules, so the narrowing question is moot anyway). SELF-TEST: node scripts/pr-labels.mjs --self-test exit 0, 'VERDICT: pr-labels self-test PASSED'; new 16-case battery 'the #17982 indeterminate write, settled against the board' pinned in SELF_TEST_BATTERIES, SELF_TEST_BATTERY_FLOOR raised 6 to 7 so it cannot be silently deleted. TWO-DIRECTION TEST, both legs ABLATED, each proven to land on disk by an occurrence count on the mutated anchor BEFORE the run — never by an editor exit code. Ablation A, delete the 4xx-stays-loud leg, anchor 1 to 0 old / 1 new, self-test EXIT 1 with 'FAIL a determinate 4xx is NOT settled, even with the label present' and 'FAIL ...and it says so in those words'. Ablation B, make 5xx read as determinate, anchor 1 to 0 old / 1 new, self-test EXIT 1 with 'FAIL a 500 is indeterminate' and 'FAIL so is a 503'. Both restores settled by git hash-object against the HEAD blob 24e4c5098d6ba9ae923fb221c519905af8efafe5 (matched) plus an empty git diff HEAD — deliberately NOT by a trap and NOT by a restore command's exit code. CONTROL-CHAR SELF-SCAN: grep -naP over the changed file, exit 1 (none), read before any pipe. The amend that removed the card trailer from the commit message was proven byte-neutral: tree 4fcc9fae5b98 identical before and after, so every reading above holds at the pushed head.",
"mcp_calls": "3 — issue_read(get_comments) 1, create_pull_request 1, add_issue_comment 1. Every other GitHub read and write went through git or direct REST.",
"open_questions": [],
"out_of_scope_findings": [
"SWEEP, criterion counted by: a tracked file that (a) names a GitHub API host (api.github.com or GITHUB_API_URL) AND (b) issues a non-GET verb, then judged on whether its write-failure handling treats the response status as the verdict with no post-condition re-read. POPULATION: 23 tracked files under scripts/, 3 elsewhere (.claude hooks and settings), 0 under packages/; of the 23, 7 carry a write verb. CONTROLS: firing control scripts/pr-labels.mjs (known positive, still present, 38953 bytes) appears in both the population and the write-verb narrowing; nonsense control api.gitlab.com or GITLAB_API_URL reads 0.",
"not filed, already correct: scripts/pm/label-write.mjs is the in-repo REFERENCE for exactly this cure — nothing there throws on an HTTP status, classifyHttp routes, and exit 0 requires that the write landed AND the read-back matched the target. scripts/pm/sweep-stale-finding.mjs is a deliberate documented no-retry design that stops on the first 403/429 and carries a read-back-mismatch stop reason. Neither is this shape.",
"not writers, excluded with evidence: scripts/check-whole-set-label-write.mjs (its 8 verb hits are literals in its own detector vocabulary and fixtures), .claude/settings.json and .claude/hooks/guard-governed-enqueue.selftest.sh (verb hits are matcher patterns for a read-only guard hook; the hook itself has 0 verb hits).",
"CARD CANDIDATE for the PM, deliberately NOT widened into this PR: three files carry the same fatal-on-5xx shape — scripts/pm/post-stamped.mjs and scripts/pm/sweep-closed-cards.mjs (byte-identical bare 'if (!res.ok) throw' rest() helper, no retry, no post-condition re-read; post-stamped's existing read-back verifies stored BYTES on the success path only) and scripts/release-github-releases.mjs (POST and PATCH release, release-lane, Prime Directive 15 territory). I did NOT extract a shared helper and swap it in: all three are SEAT-INVOKED CLI tools where a human or agent reads the output and re-runs, not unattended CI jobs whose red blocks a PR, so the severity that makes this card worth fixing does not carry over. Reported as a candidate rather than fixed.",
"noted, not filed: .github/workflows inline issue-API writes via actions/github-script exist in 5 workflow files (cross-repo-issue-closer, docs-drift-check, merge-queue-triage and two comment-only hits). Not swept further — outside this card's file surface. 承接者: whoever takes the card candidate above.",
"noted, not filed: ghRequest's 4-attempt and 2**attempt*500 ms budget is a hard-coded literal with no env override. Not a defect and not in scope. 承接者: 无 — no queued PR touches this file."
]
}
本席上一条(5659821390)写「分支 tip 等于 origin/main ⇒ 零产出」。那是 06:0x 那一刻的真读数,但几分钟后就不成立了 —— 交班快照期间那个 dev 仍在跑,随后推了一笔:
git log --oneline origin/main..origin/claude/issue-17984-pr-labels-5xx-idempotent
1bc65ff50e fix(ci): settle an indeterminate label write against the board, not the response
scripts/pr-labels.mjs | 246 +++++++++++++++++++++++++++++++++++--- (1 file)
⭐ 提交标题本身值得接手人注意:settle an indeterminate label write against the board, not the response —— 这正是卡的核心("500 不是写失败的证据",要回读标签集而不是信响应)。⚠️ 但标题不是验证:接手席位必须自己验两个方向 —— 写已落地时要成功,真 4xx 时要继续大声失败;只证快乐路径的测试,就是本卡要消灭的那个 exit-0-by-construction 形状换了层皮。
⚠️ 还要确认卡里点名的第二件事做了没有:除修这一个文件外,卡明确要求扫一遍同形状(其它对 issues API POST 而把非 2xx 一律当致命的脚本),并报出总体数与计数口径。分支只改了一个文件 ⇒ 那次扫描的结论(若有)只能在 dev 的报告里,而那份报告随 dev 一起没了。⇒ 接手人需要自己重扫。
Left untouched: bug, ci/cd, priority:p2, domain:devx — ownership, priority and outcome are not state claims.
The label set was read back after the write and matched.
A state label claims work is in flight. This card is closed on a merged delivery, so the claim
is stale; every other label is left exactly as it was found. Nothing here is a judgement about
the card, and no verdict-bearing label is ever touched by this sweep.
posted by half-state-patrol run 34819691675 · trigger schedule
Activity
claude commentedon Sep 14, 2026
Claim: PM loop round R11
Session:
session_012GKcPZbMoGq7WPzKLfRBTUBranch:
claude/issue-17984-pr-labels-5xx-idempotentWorktree:
objectstack-issue-17984Domain:
domain:devxFile surface:
scripts/pr-labels.mjs(+ 同形状扫描) (stop on breach; explain in the report)Container & model:
M,mode:subagent,model: default (opus)Clause-②: no
Thread-read: none
Serial constraints cleared: none
本卡由
domain:devx执行 PM 席派发;assignee 与本Claim:由 PM 写,承接的 dev ⛔ 不再补第二条 claim、⛔ 不动 assignee。Generated by Claude Code
claude commentedon Sep 14, 2026
交班状态标注 ——
domain:devx执行 PM 席停席session_012GKcPZbMoGq7WPzKLfRBTU· 2026-09-14T06:0xZ · 维护者指令:转交给其它 session。交班简报在座位贴 #6023。状态:⚠️ 零产出 —— 分支上没有任何工作提交
claude/issue-17984-pr-labels-5xx-idempotent存在,但其 tip 等于当时的origin/main(a90a9f26)⇒git log origin/main..<branch>为空⇒ 这张卡等于没被做过。 本席已把它剥回
pm:queue并清掉 assignee,接手席位可以当作全新卡直接派,⛔ 不必去捞那个空分支(也可以让有权限者删掉它)。⭐ 重派时别漏卡里点名的第二件事:除了修
scripts/pr-labels.mjs的 5xx 重试 + 幂等复读,卡明确要求扫一遍同形状(其它对 issues API POST 而把非 2xx 一律当致命的脚本),并报出总体数与计数口径——「顺便也看了别的」不是一次读数。给接手席位的规矩
Claim:留在卡上作为历史,⛔ 不表示本席仍在占卡。接手席位可以接这张卡:按本仓规矩,发你自己的Claim:(含独立成行的Branch:与Clause-②:),⛔ 不要改本席那条,也 ⛔ 不要沿用本席的分支名以外的旧假设。Generated by Claude Code
claude commentedon Sep 14, 2026
os-dev-report
{ "issue": 17984, "status": "done", "branch": "claude/issue-17984-pr-labels-5xx-idempotent", "pr": "https://github.com/objectstack-ai/objectstack/pull/18144", "premise_still_valid": false, "summary": "PREMISE FALSIFIED IN PART, and the card's own deeper reading is the part that held. The card's consequence 1, 'No retry on 5xx', is FALSE: scripts/pr-labels.mjs has carried a bounded exponential-backoff 5xx retry (4 attempts, 2**attempt*500 ms) with 4xx-other-than-429 breaking out as fatal since the file landed in #10777 on 2026-08-22 — verified in that blob, and the file's last commit before the 2026-09-13 incident is 2026-09-06, so the incident ran this exact loop. Two of the card's three suggested-shape bullets (retry-on-5xx, keep-4xx-fatal) were therefore already implemented; the card's observation that the window produced at least four 500s is most likely those four retry attempts, since runPlan logs the POST once and ghRequest reports only lastError. The card's consequence 2 — a 500 is not evidence the write failed, so a retry must be idempotent-aware — is the real and only remaining defect, and is what this PR fixes. Failures are now CLASSIFIED: 5xx and a thrown fetch are INDETERMINATE (sticky across attempts) and are settled by re-reading the PR's labels and judging the step's post-condition, read off the step itself; 4xx including 429 stays DETERMINATE, fatal and loud with a byte-identical error message, even when the board happens to satisfy the post-condition. A settling re-read that itself fails settles nothing: the write is reported UNVERIFIED and the original error is raised. Three new pure exports carry the decision: failureIsIndeterminate, postconditionOf, settleWriteFailure. The PR does NOT touch .github/workflows — the script is already invoked by lint.yml and pr-automation.yml — so the PM seat has no workflows-token arming problem here.", "tests": "GATES: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 32 families from the real change set (1 path, scripts/pr-labels.mjs, vs merge base a90a9f267, three-dot); all 32 run, ALL EXIT 0, each exit code captured before any pipe. Reconciliation with per-command codes: '32 derived famil(ies) accounted for — 32 run, 0 NOT-MEASURED (a DERIVED zero — all 32 recorded an exit code and none of them is 3)'. LINT: the full repo-wide union, not a narrowing — eslint . --no-inline-config --format json over 6751 files, 0 errors 0 warnings, exit 0, at final tree 1bc65ff50e (eslint.config.mjs declares no parserOptions.project and no typed rules, so the narrowing question is moot anyway). SELF-TEST: node scripts/pr-labels.mjs --self-test exit 0, 'VERDICT: pr-labels self-test PASSED'; new 16-case battery 'the #17982 indeterminate write, settled against the board' pinned in SELF_TEST_BATTERIES, SELF_TEST_BATTERY_FLOOR raised 6 to 7 so it cannot be silently deleted. TWO-DIRECTION TEST, both legs ABLATED, each proven to land on disk by an occurrence count on the mutated anchor BEFORE the run — never by an editor exit code. Ablation A, delete the 4xx-stays-loud leg, anchor 1 to 0 old / 1 new, self-test EXIT 1 with 'FAIL a determinate 4xx is NOT settled, even with the label present' and 'FAIL ...and it says so in those words'. Ablation B, make 5xx read as determinate, anchor 1 to 0 old / 1 new, self-test EXIT 1 with 'FAIL a 500 is indeterminate' and 'FAIL so is a 503'. Both restores settled by git hash-object against the HEAD blob 24e4c5098d6ba9ae923fb221c519905af8efafe5 (matched) plus an empty git diff HEAD — deliberately NOT by a trap and NOT by a restore command's exit code. CONTROL-CHAR SELF-SCAN: grep -naP over the changed file, exit 1 (none), read before any pipe. The amend that removed the card trailer from the commit message was proven byte-neutral: tree 4fcc9fae5b98 identical before and after, so every reading above holds at the pushed head.", "mcp_calls": "3 — issue_read(get_comments) 1, create_pull_request 1, add_issue_comment 1. Every other GitHub read and write went through git or direct REST.", "open_questions": [], "out_of_scope_findings": [ "SWEEP, criterion counted by: a tracked file that (a) names a GitHub API host (api.github.com or GITHUB_API_URL) AND (b) issues a non-GET verb, then judged on whether its write-failure handling treats the response status as the verdict with no post-condition re-read. POPULATION: 23 tracked files under scripts/, 3 elsewhere (.claude hooks and settings), 0 under packages/; of the 23, 7 carry a write verb. CONTROLS: firing control scripts/pr-labels.mjs (known positive, still present, 38953 bytes) appears in both the population and the write-verb narrowing; nonsense control api.gitlab.com or GITLAB_API_URL reads 0.", "not filed, already correct: scripts/pm/label-write.mjs is the in-repo REFERENCE for exactly this cure — nothing there throws on an HTTP status, classifyHttp routes, and exit 0 requires that the write landed AND the read-back matched the target. scripts/pm/sweep-stale-finding.mjs is a deliberate documented no-retry design that stops on the first 403/429 and carries a read-back-mismatch stop reason. Neither is this shape.", "not writers, excluded with evidence: scripts/check-whole-set-label-write.mjs (its 8 verb hits are literals in its own detector vocabulary and fixtures), .claude/settings.json and .claude/hooks/guard-governed-enqueue.selftest.sh (verb hits are matcher patterns for a read-only guard hook; the hook itself has 0 verb hits).", "CARD CANDIDATE for the PM, deliberately NOT widened into this PR: three files carry the same fatal-on-5xx shape — scripts/pm/post-stamped.mjs and scripts/pm/sweep-closed-cards.mjs (byte-identical bare 'if (!res.ok) throw' rest() helper, no retry, no post-condition re-read; post-stamped's existing read-back verifies stored BYTES on the success path only) and scripts/release-github-releases.mjs (POST and PATCH release, release-lane, Prime Directive 15 territory). I did NOT extract a shared helper and swap it in: all three are SEAT-INVOKED CLI tools where a human or agent reads the output and re-runs, not unattended CI jobs whose red blocks a PR, so the severity that makes this card worth fixing does not carry over. Reported as a candidate rather than fixed.", "noted, not filed: .github/workflows inline issue-API writes via actions/github-script exist in 5 workflow files (cross-repo-issue-closer, docs-drift-check, merge-queue-triage and two comment-only hits). Not swept further — outside this card's file surface. 承接者: whoever takes the card candidate above.", "noted, not filed: ghRequest's 4-attempt and 2**attempt*500 ms budget is a hard-coded literal with no env override. Not a defect and not in scope. 承接者: 无 — no queued PR touches this file." ] }Generated by Claude Code
claude commentedon Sep 14, 2026
session_012GKcPZbMoGq7WPzKLfRBTU· 2026-09-14T06:1xZ本席上一条(
5659821390)写「分支 tip 等于origin/main⇒ 零产出」。那是 06:0x 那一刻的真读数,但几分钟后就不成立了 —— 交班快照期间那个 dev 仍在跑,随后推了一笔:⇒ ⛔ 不要按上一条去重做这张卡。 现在的真实状态与 #17712 同类:
claude/issue-17984-pr-labels-5xx-idempotent@1bc65ff5scripts/pr-labels.mjs(+246 / -6)⇒ ⛔ 不触.github/workflows/**,所以它的 PR 是可以被 PM 武装的(不会撞 HTTP 422)⭐ 提交标题本身值得接手人注意:⚠️ 但标题不是验证:接手席位必须自己验两个方向 —— 写已落地时要成功,真 4xx 时要继续大声失败;只证快乐路径的测试,就是本卡要消灭的那个 exit-0-by-construction 形状换了层皮。
settle an indeterminate label write against the board, not the response—— 这正是卡的核心("500 不是写失败的证据",要回读标签集而不是信响应)。⭐ 本席把这次更正留在卡上而不是改掉上一条,是因为它本身是一条读数纪律:交班快照会在你写字的这几分钟里过期。上一条自己就写了这个风险,然后当场撞上了。
标签维持
pm:queue、assignee 已清 —— 接手席位可直接认领,⛔ 但不要当作新卡从零做。Generated by Claude Code
github-actions commentedon Sep 14, 2026
os-closed-card-sweep — machine-findable marker for this generated comment.
Removed the pm-loop state label(s) this closed card no longer claims:
pm:dispatched.d72c4c0824, merged intomain.bug,ci/cd,priority:p2,domain:devx— ownership, priority and outcome are not state claims.A state label claims work is in flight. This card is closed on a merged delivery, so the claim
is stale; every other label is left exactly as it was found. Nothing here is a judgement about
the card, and no verdict-bearing label is ever touched by this sweep.
posted by half-state-patrol run 34819691675 · trigger
scheduleGenerated by Claude Code
fix(ci): settle an indeterminate label write against the board, not t…