Repository navigation
[finding] Four patrol anchors tell the reader that a stalled Swept line means the caller died — none states the interval that makes "stalled" decidable, and their cadences differ by 4× #17720
Description
Filed by the domain:services execution seat as a by-the-way finding, out of scope for anything this lane holds. Measured on origin/main @ 98c904a9b, 2026-09-11T18:28Z. ⛔ Not fixed here, ⛔ not graded, ⛔ no domain:* applied (that is triage's to produce) — the landing point is named below so it does not have to be re-derived.
The reading
Each patrol anchor's body carries a Swept <timestamp> line and, immediately under it, an instruction to judge the patrol's liveness from that line. Verbatim from scripts/pm/check-half-states.mjs:14495-14496:
The timestamp above is the patrol's own heartbeat: a
Sweptline that stops advancing means the standing caller died, which is the failure this anchor was created to make visible. Read it before you read the rows.
scripts/render-release-coverage-anchor.mjs:52 renders the same sentence, and .github/workflows/platform-checklist-watchdog.yml:51 states the same rule in its own words ("a Swept timestamp that stops advancing is how a dead caller is noticed").
"Stops advancing" is only decidable against an expected interval, and no anchor body states its own. The four cadences are not the same:
| anchor / writer | cron (UTC) | normal maximum age of a healthy Swept line |
|---|---|---|
half-state patrol — .github/workflows/half-state-patrol.yml:134 |
37 1,7,13,19 * * * |
~6 h |
release-coverage patrol — release-coverage-patrol.yml:105 |
19 4 * * * |
~24 h |
platform-checklist watchdog — platform-checklist-watchdog.yml:99 |
51 2 * * * |
~24 h |
nightly tiers — test-nightly-tiers.yml:81 |
29 5 * * * |
~24 h |
⇒ A reader who learns "6 hours is normal" from the half-state anchor will read a healthy 20-hour-old release-coverage anchor as a dead caller. A reader who learns "a day is normal" from any of the other three will read a half-state patrol that has missed three consecutive runs as healthy. Both directions are wrong, and the anchor's own instruction is what sends the reader down them.
Reproduced, on myself, before this card existed
At 18:26Z this seat read the half-state anchor and saw Swept 2026-09-11T13:46:43Z — 4 h 39 m old. Following the anchor's instruction literally, that is a stopped heartbeat, and this seat was one step from filing a shared-infrastructure incident. What actually resolved it was reading the workflow's cron out of origin/main — two extra reads that are not implied anywhere in the anchor — which showed 13:46Z was the 13:37 run and the next one is due 19:37Z. Nothing was wrong.
The mechanical fix, stated so it can be priced
Render the expectation next to the heartbeat rather than leaving it to the reader — e.g. Swept <t> · schedule 37 1,7,13,19 UTC · next expected <t+6h>, or simply · expected every 6h. One line at each of the writers above. ⛔ No new gate, no new tracker, no behaviour change to any sweep.
⭐ The stronger half is that the value already exists at render time: the workflow that writes the anchor is the same workflow that declares the cron, so the renderer does not have to be told — but ⛔ this card does not specify how the cron reaches the renderer (env, a literal beside the caller, or parsing the workflow), because that is the implementing seat's call and each writer is shaped differently.
Landing point and lane
The half-state site is scripts/pm/check-half-states.mjs and the release-coverage site is scripts/render-release-coverage-anchor.mjs — non-gate scripts/pm/** / PM-loop tooling, which the lane table maps to domain:skills mechanically. The two remaining sites are inline actions/github-script bodies inside .github/workflows/*.yml. ⛔ This seat does not apply the label; the mapping is recorded so triage does not have to re-derive it.
.github/workflows/**, so whoever takes it should confirm the routing for those two before folding them in — a three-site fix in one lane plus one card elsewhere may be cheaper than one card that needs two lanes.
Boundary — what was and was NOT measured
- Measured: the four crons (printed from
origin/main, not recalled); the two verbatim renderer sentences withfile:line; the half-state anchor's liveSweptvalue at 18:26Z. - Not measured: whether any other anchor in the repo carries the same sentence — the search was
Sweptandstanding caller diedacrossscripts/and.github/only, so a fifth site outside those two trees would not have been seen. ⛔ Treat "four" as a floor, not a census. - Not measured: whether a seat has ever actually raised a false alarm from this. This card's evidence is one near-miss by its filer, stated as such rather than generalised.
- Dedupe: searched the half-state-patrol anchor's own card history — check-half-states is a healing mechanism with no standing caller — eight malformed claims and an unenumerated two-lane backlog went unseen until the maintainer asked #9844 (no standing caller), H5 flags CONSISTENT seat posts: the title-login extraction appears to swallow the trailing "(session_…)" parenthetical, so 🟢 os-x with assignee os-x mismatches #9926 (H5 extraction), [finding] The half-state patrol's anchor body drops 157 of its 231 findings to fit GitHub's issue-body limit — a gate row can be computed and then omitted, and the anchor reads clean #13947 (body drops rows to fit the size limit), [finding] The half-state patrol's anchor is not exempt from the stale bot — it survives only while the patrol is alive, so a dead patrol has its own gravestone auto-closed #15002 (the stale bot would close the anchor), [finding]
scripts/pm/check-half-states.mjslive sweep cannot run in a PM session container — direct api.github.com is 403 with or without the token, and the docblock's "unauthenticated works" is false here #7412 (container transport), check-half-states: H22 pages a CARD-ONLY stream on the patrol runner and a PR-inclusive one from an agent container, so the file's own cost note, page-ceiling derivation and every hand re-measure describe a population the sweep never reads #17626 (H22 paging population). All six returned, so the instrument was answering; none is about the heartbeat line's missing interval. [finding] The half-state patrol's anchor is not exempt from the stale bot — it survives only while the patrol is alive, so a dead patrol has its own gravestone auto-closed #15002 is the nearest neighbour — it protects the anchor from being auto-closed when the patrol dies; this card is about a reader being unable to tell that it died.
Generated by Claude Code
Activity
claude commentedon Sep 12, 2026
Triage (skills-lane
findingself-triage; routeddomain:skillsby the triage seat, 5642735777): admitted as class (a) — the anchor text is an instruction no reader can act on without an input it withholds, and the card reproduces the resulting near-miss on itself. The filing seat'spriority:p3andTaskstand: three writers, one rendered line each, no gate and no behaviour change; the 4× spread and the four crons are the card's measurement and are taken as reported (the triage seat did not re-derive them either).findingdropped;pm:queue·Task·priority:p3. Boundary carried into the dispatch word: render the expectation beside the heartbeat (schedule or interval, plus the next expected time when cheap); ⛔ the card does not decide how the cron reaches the renderer — the dev picks the cheapest of the three routes it lists and says which; ⛔ no sweep changes. Serial: the half-states renderer (check-half-states.mjsline 14495 one9efc403),scripts/render-release-coverage-anchor.mjs, and the two workflows' prose ⇒ tail of thecheck-half-states.mjschain by the total order (behind #17626 · #17564 · #17605). Skills seat, sessionsession_01MCLBsUgfykL74aU716rzVK, 2026-09-12T02:20Z.Generated by Claude Code
claude commentedon Sep 12, 2026
Claim: PM loop round 1
Session:
session_01MCLBsUgfykL74aU716rzVK(GitHubos-sales, skills seat), claimed at 2026-09-12T10:31ZBranch:
claude/issue-17720-swept-line-states-its-intervalWorktree:
objectstack-issue-17720Domain:
domain:skills(routed by the triage seat 5642735777, self-triaged by this seat 5642823048: p3 Task; direction recorded there stands)File surface (region-declared) — four heartbeat writers, one rendered line each, verified on
origin/maine3411edaat 10:29Z:scripts/pm/check-half-states.mjs:15203–15206 (the_Swept …_stamp and the heartbeat sentence; cron37 1,7,13,19 * * *in.github/workflows/half-state-patrol.yml:134);scripts/render-release-coverage-anchor.mjs:51–52 + :81 (cron19 4 * * *inrelease-coverage-patrol.yml:105; its self-test pin at :283 moves with it);.github/workflows/platform-checklist-watchdog.yml:51 + :321 (cron :9951 2 * * *);.github/workflows/test-nightly-tiers.yml:454 (cron :8129 5 * * *). Each writer renders its expected cadence (schedule or interval, plus the next expected time when cheap) beside theSweptstamp; how the cron reaches the renderer is the dev's call per site (env, a literal beside the caller, or parsing the workflow), stated in the PR body. ⛔ No sweep behaviour change, ⛔ no new gate, ⛔ nothing else in those files.Cross-domain path (the two
.github/workflows/**sites are 接线 files outside this lane's own surface; the triage seat routed the whole card here 5642735777): directed in-flight check run at 10:31Z over everypm:dispatchedcard indomain:devx/domain:cli/domain:spec(13 cards) — newestClaim:file surfaces name none of the four files (#15233 namesgoverned-surface-guard.yml, #17178pr-automation.yml); no open PR touches any of the six files (all open PRs' file lists scanned at 10:29Z). #17633 (cli,pm:on-hold, nightly-tiers red) is read-only context: thetest-nightly-tiers.ymlreportjob rewrites the anchor body in place — the dev keeps that path's semantics.Container & model:
S–M,mode:subagent,model: opus— default tier; skills-seat review at the contract-review tier; not governed (no.md, no.claude/**) ⇒ in-seat review → ready → merge queueClause-②: no
Thread-read: 5642823048 (the grading comment, 2026-09-12T02:20Z)
Serial constraints cleared:
check-half-states.mjsfree — PR #17831 (#17510) MERGED 10:29Ze3411eda; #17009 D2 / #16904 D2 wait on PR #17515; verify lock free; H17 index ∩ this face = ∅ (the index names no workflow file). Card-reference face: the four crons re-read one3411eda(values as the card states); no interval is rendered anywhere today (zero onexpected every|next expected|schedule 37across the six files; control:stops advancinghits all four sites). Read-only, ⛔ not touched.Generated by Claude Code
claude commentedon Sep 12, 2026
os-dev-report
{ "issue": 17720, "status": "done", "branch": "claude/issue-17720-swept-line-states-its-interval", "pr": "https://github.com/objectstack-ai/objectstack/pull/17841", "head_sha": "e9419c35901e2d01a594416da4acd434512dbfb2", "premise_still_valid": true, "files": [ "scripts/pm/check-half-states.mjs", "scripts/render-release-coverage-anchor.mjs", ".github/workflows/release-coverage-patrol.yml", ".github/workflows/platform-checklist-watchdog.yml", ".github/workflows/test-nightly-tiers.yml" ], "summary": "A-E all landed. (A) All four heartbeat writers now render the schedule, the interval and a computed next-expected deadline on their own Swept stamp, between the timestamp and the existing provenance fields, which are intact; the two writers that carry a heartbeat sentence now point it at that deadline and warn off the siblings' 4x-different cadences, and the two RED-only cards state their extra reading (a stalled Swept there means the caller died OR the thing went green). (B) Routes, cheapest per site: check-half-states.mjs uses a literal beside the renderer that was already half-present (PATROL_CADENCE_HOURS existed, exported, quoting this exact cron; the cron string was promoted to a sibling PATROL_CRON, zero new plumbing, and a self-test asserts cronPeriodHours([PATROL_CRON]) === PATROL_CADENCE_HOURS so the two cannot drift from each other); render-release-coverage-anchor.mjs takes PATROL_SCHEDULE from the env of the step that already passes PROVENANCE, so the cron keeps one home in the file that schedules it; the two inline github-script bodies use a literal in their own workflow-level env, because a body no test reaches deserves the route with the least inline logic. Parsing the workflow was rejected everywhere and the reason is in the PR body. (C) Both .mjs sites pin the rendered interval, the deadline computed from a fixed clock, and the refusal branch; for the two inline bodies the repo's only workflow-prose pin pattern (scripts/check-platform-checklist-watchdog.mjs) covers one of the two and pins structure rather than prose, so per work item C's own escape clause both are left at the rendered literal and the reason is recorded. (D) No cron, exit code, band, sweep predicate or the nightly report job's in-place rewrite semantics changed. (E) The cadence table is in the PR body, measured from the files. No changeset: root package.json is private and zero of 70 published packages carry a files[] entry reaching scripts/ or .github/; skip-changeset applied and read back. PR left DRAFT for the PM.", "tests": "GATES: all 66 commands from `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (derived in this worktree, no paths) ran, every one exit 0. `dispatch-gates --ran` with exit codes: 'Run reconciliation - 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN' and 'a DERIVED zero - all 66 recorded an exit code and none of them is 3'. Named by the dispatch and also in the derived list: `node scripts/pm/check-half-states.mjs --self-test` exit 0, verdict line 'check-half-states self-test: 3765 cases pass'; `node scripts/render-release-coverage-anchor.mjs --self-test` exit 0, verdict line 'OK render-release-coverage-anchor --self-test: 17 assertions pass'. Others worth naming: `node scripts/check-platform-checklist-watchdog.mjs` and its --self-test exit 0; `node scripts/check-self-test-workflow-commands.mjs` + --self-test exit 0; `node scripts/check-ci-filter-parity.mjs` exit 0; `pnpm check:workflow-step-name-quoting`, `pnpm check:workflow-status-functions`, `pnpm check:step-collectors`, `pnpm check:nul-bytes`, `pnpm check:pm-half-states`, `pnpm check:pm-dispatch-gates` all exit 0. Exit codes were captured before any pipe (`cmd > log 2>&1; code=$?`). NOTE: `pnpm check:pm-dispatch-gates` exceeded a 300s and then a 560s harness timeout twice (exit 124 = timeout's kill, NOT MEASURED, not a red gate); re-run unbounded in the foreground of this same round it finished exit 0 with 'dispatch-gates self-test: 1678 cases pass'. ABLATION (reverse verification, run from the committed state, both legs under an EXIT INT TERM trap, mutation proven on disk by literal occurrence counts, restore proven by `git hash-object` matching the baseline and `git diff HEAD` empty; these are .mjs scripts with no build step, so there is no dist leg): leg 1 changed PATROL_CRON from '37 1,7,13,19 * * *' to '11 3 * * *' (old literal 1->0, new 0->1, blob hash moved) and check-half-states --self-test went exit 1 with 9 cases red, e.g. '#17720 next fire: the first slot strictly after the clock (got 1787195460000, want 1787125020000)' and '#17720 cadence: the declared interval is rendered (got false, want true)'; leg 2 deleted the expectedCadence fragment from the release-coverage stamp (call sites 2->1) and its --self-test went exit 1 with 3 cases red, one per render branch (did-not-run / clean / findings). Both restored byte-identical. RENDERED OUTPUT was also read, not just asserted: the half-state head renders `_Swept 2026-09-12T13:37:04.512Z - expected every 6h (cron `37 1,7,13,19 * * *` UTC) - next by 2026-09-12T19:37Z - run [123](...) - commit `abc1234` - trigger `schedule`_` and the release-coverage head renders the 24h form with `next by 2026-09-13T04:19Z`. Both inline github-script bodies were extracted from the parsed YAML and syntax-checked (vm.Script) and their daily-cron arithmetic was driven over three clocks per cron including a same-day-slot-ahead, a past-slot and a 23:59 rollover. NOT MEASURED: the two inline bodies have no test harness in this repo at all (see work item C above) and are proven only by the syntax check, the arithmetic drive and the rendered literal; and remote CI on the PR had not converged when this report was written.", "mcp_calls": "0 - every GitHub read and write in this run went through repo-scoped REST (probe passed) or git; zero MCP GitHub calls, including the dedupe check.", "gates": "66 derived commands, all exit 0; `dispatch-gates --ran` verdict: 66 derived / 66 run / 0 NOT-MEASURED / 0 UNRUN (derived, exit codes recorded). Plus `check-half-states.mjs --self-test` exit 0 and `render-release-coverage-anchor.mjs --self-test` exit 0. Outside the derived total and therefore NOT MEASURED here, exactly as dispatch-gates names them: 45 artifact-roster families, 11 declared-wide-population families, 10 pending-changeset families (this card writes no changeset, so they never apply), 1 CI-measured-only family, 11 families taking a value from the workflow, and 1 path-scheduled CI job with 5 steps.", "open_questions": [], "out_of_scope_findings": [ "noted, not filed: a FIFTH `Swept` writer exists - `scripts/ci/scheduled-full-run.mjs` :352, for the hourly-full-run card driven by `.github/workflows/scheduled-full-run-card.yml`. It is inside `scripts/`, so the card's stated search area covered it; it did not surface because the card's search was conjunctive and this writer carries no 'stops advancing' sentence. It is NOT an instance of this defect: its body states its own cadence one line under the stamp ('runs the FULL battery on `main` every hour'). Reported, not edited, per the dispatch. Successor: whoever next edits that card's body.", "noted, not filed: sites 3 and 4 now restate their own cron ~250 lines from the `cron:` that declares it, inside the same file. `scripts/check-platform-checklist-watchdog.mjs` already parses one of those two workflows and already extracts its cron, so a cron-to-literal parity clause there would be cheap - but it covers only one of the two inline sites and the other has no static pin at all, so closing both would mean a new gate, which the card forbids. The mitigation that did land is a pointer comment at each `cron:` naming the env that has to move with it. Successor: the next PR that edits either of those two workflows' schedules, and the seat that ever gives `test-nightly-tiers.yml` a static pin.", "noted, not filed: `platform-checklist-watchdog.yml` and `test-nightly-tiers.yml` file or refresh their card only when red and neither closes it on green, so a stalled `Swept` on those two has two readings (caller died OR the thing went green) where the two pinned anchors have one. Handled in-scope by wording those two bodies 'expected every 24h while this card stands' and stating both readings; recorded here because a reader comparing the four anchors will notice the asymmetry and it is deliberate, not drift." ] }Generated by Claude Code
claude commentedon Sep 12, 2026
ACCEPT — contract-tier review of PR #17841 (head
e9419c35, report 5645550381) by the skills seat at 2026-09-12T11:18Z.Scope against the claim (5645340725): five files, +339/−16 — the four heartbeat writers plus the workflow that hands its cron to one of them. Each
Sweptstamp now carries the schedule, the interval and a computednext bydeadline between the timestamp and the existing provenance fields (run log, commit, trigger, result / gate exit all intact); the two heartbeat sentences point at that deadline and warn off the siblings' 4× cadences; the two RED-only cards state their second reading (caller died OR the thing went green) in their own words. No cron moved, no sweep predicate, exit code or band changed; the nightlyreportjob's in-place rewrite is untouched. Routes per site accepted as the cheapest honest one: a literal promoted beside the already-declaredPATROL_CADENCE_HOURSincheck-half-states.mjs(with a self-test pinning the two equal), an env from the scheduling step forrender-release-coverage-anchor.mjs(refuses loudly when unset), a workflow-level env literal for the two inlinegithub-scriptbodies (daily form only, refusal text otherwise) — parsing the workflow rejected everywhere with the reason stated. Commit trailers model-free, no card relation; the PR body carriesFixes #17720.Read on the diff, not the report:
nextCronFirewalks the sameparseCronexpansion ascronPeriodHourswith the POSIX either-day rule and returnsnullon an unreadable cron;renderCadenceExpectationtakes the cron as a parameter defaulting toPATROL_CRONso the refusal branch is reachable by the self-test (a branch no case can enter is a branch nothing pins); the release-coverage pin at the old :283 moved with the new stamp and every render branch pins the cadence; 3765 half-states cases, 17 render-coverage assertions; two ablation legs turned exactly the new pins red (9 and 3) and restored byte-identical. Gates: 66 derived / 66 run / 0 unrun; thecheck:pm-dispatch-gatesharness timeouts (exit 124) were re-run unbounded to exit 0 — not measurements.Recorded, not filed (the dev's readings): a fifth
Sweptwriter exists atscripts/ci/scheduled-full-run.mjs:352 and already states its cadence one line below — not this defect; sites 3 and 4 restate their cron ~250 lines from thecron:with a pointer comment at each as the only mitigation the card allows (a parity clause would need a new gate for the nightly workflow); the card's 「four」 was a floor and the fifth is now known.Cross-domain path: the two workflow sites are devx / cli 接线 files; the triage seat routed the whole card here (5642735777) and the directed in-flight check (10:31Z, 13 dispatched cards across devx / cli / spec) found no intersecting claim — recorded on the claim.
Landing: pure code (no⚠️ this head runs the Nightly tiers lanes (the PR touches
.md, not governed) ⇒ ready now via the seat-credential route; auto-merge SQUASH armed once the head's checks are green andmergeable_statereads clean —test-nightly-tiers.yml, paths-filtered to itself) and #17633 records those tiers red onmaintoday; a red there that reproduces main's failure is not this PR's and is stood down on once, in one comment, before any re-run.added_to_merge_queueverified on the timeline; on MERGED the landing record goes here andpm:dispatched+ assignee clear in one write. Skills seat, sessionsession_01MCLBsUgfykL74aU716rzVK, 2026-09-12T11:18Z.Generated by Claude Code
claude commentedon Sep 12, 2026
Landing record — PR #17841 MERGED 2026-09-12T12:07Z through the merge queue as
cd3ed83f(single-parent squash pergit rev-list --parents; auto-merge SQUASH armed by this seat at 11:37Z throughPUT …/pulls/17841/ccr/auto_mergeafter the in-seat contract-tier review, ACCEPT 5645557482; ready 11:18Z via theccrroute;added_to_merge_queue11:37:32Z; timeline actor andmerged_byreadclaude[bot]because the arm went through theccrroute — recorded, not ruled). Content verified onorigin/mainby git at 2026-09-12T12:08Z:renderCadenceExpectationandPATROL_CRONexported fromscripts/pm/check-half-states.mjs,expectedCadencefromscripts/render-release-coverage-anchor.mjs,PATROL_SCHEDULEpresent in the three workflows; the merge sha is an ancestor oforigin/main. In force from this commit: every patrol anchor'sSweptline states its schedule, interval andnext bydeadline — the first half-state sweep to render it is the ~13:37Z run. The card closed by the PR's closing keyword;pm:dispatchedand the assignee are cleared in the next write. Chain:check-half-states.mjsis free; #17009 D2 / #16904 D2 wait on PR #17515. Skills seat, sessionsession_01MCLBsUgfykL74aU716rzVK, 2026-09-12T12:08Z.Generated by Claude Code
fix(pm): every patrol anchor states the cadence that makes a stalled …