Skip to content

[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

@os-sales

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 Swept line 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 near-miss is the defect: the instrument asks for a judgment and withholds the only input that judgment needs. Every seat that reads the anchor each round pays this, and for the half-state patrol the ambiguous window is up to six hours out of every six.

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.

⚠️ Cross-lane note: fixing all four sites in one pass touches .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


Generated by Claude Code

Activity

  1. added theissue type on Sep 11, 2026
  2. claude commented on Sep 12, 2026

    @claude
    Contributor

    Triage (skills-lane finding self-triage; routed domain:skills by 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's priority:p3 and Task stand: 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). finding dropped; 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.mjs line 14495 on e9efc403), scripts/render-release-coverage-anchor.mjs, and the two workflows' prose ⇒ tail of the check-half-states.mjs chain by the total order (behind #17626 · #17564 · #17605). Skills seat, session session_01MCLBsUgfykL74aU716rzVK, 2026-09-12T02:20Z.


    Generated by Claude Code

  3. claude commented on Sep 12, 2026

    @claude
    Contributor

    Claim: PM loop round 1
    Session: session_01MCLBsUgfykL74aU716rzVK (GitHub os-sales, skills seat), claimed at 2026-09-12T10:31Z
    Branch: claude/issue-17720-swept-line-states-its-interval
    Worktree: objectstack-issue-17720
    Domain: 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/main e3411eda at 10:29Z: scripts/pm/check-half-states.mjs :15203–15206 (the _Swept …_ stamp and the heartbeat sentence; cron 37 1,7,13,19 * * * in .github/workflows/half-state-patrol.yml :134); scripts/render-release-coverage-anchor.mjs :51–52 + :81 (cron 19 4 * * * in release-coverage-patrol.yml :105; its self-test pin at :283 moves with it); .github/workflows/platform-checklist-watchdog.yml :51 + :321 (cron :99 51 2 * * *); .github/workflows/test-nightly-tiers.yml :454 (cron :81 29 5 * * *). Each writer renders its expected cadence (schedule or interval, plus the next expected time when cheap) beside the Swept stamp; 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 every pm:dispatched card in domain:devx / domain:cli / domain:spec (13 cards) — newest Claim: file surfaces name none of the four files (#15233 names governed-surface-guard.yml, #17178 pr-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: the test-nightly-tiers.yml report job 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 queue
    Clause-②: no
    Thread-read: 5642823048 (the grading comment, 2026-09-12T02:20Z)
    Serial constraints cleared: check-half-states.mjs free — PR #17831 (#17510) MERGED 10:29Z e3411eda; #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 on e3411eda (values as the card states); no interval is rendered anywhere today (zero on expected every|next expected|schedule 37 across the six files; control: stops advancing hits all four sites). Read-only, ⛔ not touched.


    Generated by Claude Code

  4. claude commented on Sep 12, 2026

    @claude
    Contributor

    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

  5. claude commented on Sep 12, 2026

    @claude
    Contributor

    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 Swept stamp now carries the schedule, the interval and a computed next by deadline 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 nightly report job's in-place rewrite is untouched. Routes per site accepted as the cheapest honest one: a literal promoted beside the already-declared PATROL_CADENCE_HOURS in check-half-states.mjs (with a self-test pinning the two equal), an env from the scheduling step for render-release-coverage-anchor.mjs (refuses loudly when unset), a workflow-level env literal for the two inline github-script bodies (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 carries Fixes #17720.

    Read on the diff, not the report: nextCronFire walks the same parseCron expansion as cronPeriodHours with the POSIX either-day rule and returns null on an unreadable cron; renderCadenceExpectation takes the cron as a parameter defaulting to PATROL_CRON so 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; the check:pm-dispatch-gates harness timeouts (exit 124) were re-run unbounded to exit 0 — not measurements.

    Recorded, not filed (the dev's readings): a fifth Swept writer exists at scripts/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 the cron: 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 .md, not governed) ⇒ ready now via the seat-credential route; auto-merge SQUASH armed once the head's checks are green and mergeable_state reads clean — ⚠️ this head runs the Nightly tiers lanes (the PR touches test-nightly-tiers.yml, paths-filtered to itself) and #17633 records those tiers red on main today; 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_queue verified on the timeline; on MERGED the landing record goes here and pm:dispatched + assignee clear in one write. Skills seat, session session_01MCLBsUgfykL74aU716rzVK, 2026-09-12T11:18Z.


    Generated by Claude Code

  6. claude commented on Sep 12, 2026

    @claude
    Contributor

    Landing record — PR #17841 MERGED 2026-09-12T12:07Z through the merge queue as cd3ed83f (single-parent squash per git rev-list --parents; auto-merge SQUASH armed by this seat at 11:37Z through PUT …/pulls/17841/ccr/auto_merge after the in-seat contract-tier review, ACCEPT 5645557482; ready 11:18Z via the ccr route; added_to_merge_queue 11:37:32Z; timeline actor and merged_by read claude[bot] because the arm went through the ccr route — recorded, not ruled). Content verified on origin/main by git at 2026-09-12T12:08Z: renderCadenceExpectation and PATROL_CRON exported from scripts/pm/check-half-states.mjs, expectedCadence from scripts/render-release-coverage-anchor.mjs, PATROL_SCHEDULE present in the three workflows; the merge sha is an ancestor of origin/main. In force from this commit: every patrol anchor's Swept line states its schedule, interval and next by deadline — the first half-state sweep to render it is the ~13:37Z run. The card closed by the PR's closing keyword; pm:dispatched and the assignee are cleared in the next write. Chain: check-half-states.mjs is free; #17009 D2 / #16904 D2 wait on PR #17515. Skills seat, session session_01MCLBsUgfykL74aU716rzVK, 2026-09-12T12:08Z.


    Generated by Claude Code

  7. added a commit that references this issue on Sep 17, 2026
    cd3ed83
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions