ci: give a lost runner a name, a last file, and a row (B5 of #110) - #124
Conversation
A leg whose runner stops talking left nothing behind: no artifact, no metrics record, no row anywhere. The 2026-07-31 sweep planned 15 integration legs and ci-data received 12 ndjson files, with no marker for the missing three. Two instruments, and the artifact outranks both: - ci-leg-checkpoint.ts opens a check run per leg and PATCHes it after every file, carrying the last completed file, what is running now, outcome/duration/cap and a resource sample. A leg that dies leaves it un-closed — the absence of a terminal is the signal. - The jobs API is the free half: a vanished leg still records its test step as in_progress with a start time. - ci-leg-ledger.ts reconciles plan's matrix against the legs that reached ci-data/runs, into ci-data/attempted-legs.json. A leg is `complete` because it produced a metrics record, never because a check run says so — the checkpoint is best-effort and exits 0 on any API failure, so trusting it first would let instrument failure fabricate a runner-lost for a leg that finished cleanly. Recorded in a separate file by decision: the version scheduler reads tested-versions.json as a presence test (ros-versions.yml:99), so an `incomplete` conclusion there would make an aborted run look tested and silently stop rescheduling that version. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
📝 WalkthroughWalkthroughChangesCI runner-loss recovery
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant IntegrationWorkflow
participant ci-file-watchdog
participant ci-leg-checkpoint
participant GitHubChecks
participant ci-leg-ledger
participant MetricsArtifacts
IntegrationWorkflow->>ci-leg-checkpoint: Open leg checkpoint
ci-leg-checkpoint->>GitHubChecks: Create in-progress check run
ci-file-watchdog->>IntegrationWorkflow: Write per-file sidecar
IntegrationWorkflow->>ci-leg-checkpoint: Mark file outcome and host snapshot
ci-leg-checkpoint->>GitHubChecks: Update check-run output
IntegrationWorkflow->>ci-leg-checkpoint: Close leg checkpoint
IntegrationWorkflow->>ci-leg-ledger: Reconcile planned legs
ci-leg-ledger->>MetricsArtifacts: Read completed leg records
ci-leg-ledger->>GitHubChecks: Inspect checkpoint runs
ci-leg-ledger->>ci-leg-ledger: Classify and persist attempted legs
Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
CodeQL js/incomplete-sanitization: the table escaped `|` but not the backslash that introduces the escape, so a value containing one would still break the cell. Splitting platform and target into separate columns removes the need to encode a separator at all.
Flagged by github-code-quality. `last` was left over from an earlier draft where the title named the file just finished rather than the one now running.
There was a problem hiding this comment.
Pull request overview
Adds CI instrumentation to make “runner lost communication” cases diagnosable by (1) pushing per-file progress to a server-side check run during the leg, and (2) writing a planned-vs-completed ledger (attempted-legs.json) during aggregation so missing legs get an explicit terminal classification instead of vanishing from ci-data.
Changes:
- Introduces per-leg check-run checkpointing (
ci-leg-checkpoint.ts) and an aggregate reconciliation/ledger (ci-leg-ledger.ts) persisted toci-data/attempted-legs.json. - Refactors host resource snapshotting into a shared module (
ci-host-snapshot.ts) and wires the watchdog to emit a sidecar consumed by the checkpoint marker. - Updates integration workflows and callers to grant the needed
checks: write/actions: readpermissions and to run/close the checkpoints.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| test/unit/fixtures/sweep-30665449265-jobs.json | Adds real jobs-API fixture data from the cited sweep run for replay tests. |
| test/unit/ci-leg-ledger.test.ts | Anchor tests for leg classification and ledger building (including replay of the real runner-loss fixture). |
| test/unit/ci-leg-checkpoint.test.ts | Anchor tests for the checkpoint external_id and payload round-trip contract. |
| scripts/ci-leg-ledger.ts | Implements planned-vs-completed reconciliation, runner-loss classification, and attempted-legs.json writing + orphan check-run finalization. |
| scripts/ci-leg-checkpoint.ts | Implements per-leg check-run open/mark/close with machine-readable payload embedded in check output. |
| scripts/ci-host-snapshot.ts | Extracts and centralizes host snapshot + QEMU process counting used by watchdog and checkpoints. |
| scripts/ci-file-watchdog.ts | Writes a “last file” sidecar for each file run and consumes shared host snapshot/QEMU counting. |
| project-words.txt | Adds new vocabulary used by the new CI/docs text for cSpell. |
| DESIGN.md | Documents what evidence survives runner loss and the resulting instrumentation design constraints. |
| .github/workflows/sweep.yml | Grants required permissions to the reusable workflow for checkpoints/ledger. |
| .github/workflows/main.yml | Grants required permissions to the reusable workflow for checkpoints/ledger. |
| .github/workflows/integration.yml | Wires checkpoint open/mark/close into each leg, and runs the ledger step in aggregate; adjusts permissions accordingly. |
| .github/instructions/ci.instructions.md | Documents the new incomplete-leg ledger, sidecar, and new data flow. |
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@scripts/ci-leg-checkpoint.ts`:
- Around line 254-256: Update saveState to use asynchronous Bun.write() instead
of writeFileSync, and make it return/await the write promise. Update every
saveState call site in open(), mark(), and close() to await the operation,
preserving the existing state serialization and execution order.
In `@scripts/ci-leg-ledger.ts`:
- Line 463: Update the summary write in the step-summary flow to use
appendFileSync directly, preserving the existing `${lines.join("\n")}\n` content
and avoiding the read-modify-write pattern currently using readFileSync and
writeFileSync.
- Line 437: Replace the full-file write using writeFileSync in the async build()
flow with awaited Bun.write(), preserving the existing path and serialized
content, including indentation and trailing newline.
- Around line 350-354: The JSON parsing in scripts/ci-leg-ledger.ts lines
350-354 and 435-435 must not abort reconciliation. In the per-line parser within
the NDJSON loop, catch parse failures, emit a ::warning::, and continue
processing subsequent lines; in the attempted-legs.json parsing flow, catch
failures, emit a warning, and fall back to an empty object so build() can still
record the ledger and run finalizeLostCheckRuns.
- Around line 370-398: Update the PATCH request in the check-run completion flow
to retain the fetch response, inspect its ok status, and log the HTTP status
when the response is unsuccessful. Keep the existing request payload and timeout
behavior unchanged, and ensure non-2xx responses produce diagnostic output
instead of being silently ignored.
In `@test/unit/ci-leg-ledger.test.ts`:
- Around line 285-301: Extract the duplicated join logic from realJobMap into an
exported joinJobsByLeg function in ci-leg-ledger.ts, accepting the job-name map
and planned legs and returning the leg-keyed job map. Update fetchJobs to call
joinJobsByLeg and update the test helper to call the same production function,
so the assertion exercises the actual join implementation.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 54ee2331-b9b0-4213-802f-7eda3a7d3f08
📒 Files selected for processing (13)
.github/instructions/ci.instructions.md.github/workflows/integration.yml.github/workflows/main.yml.github/workflows/sweep.ymlDESIGN.mdproject-words.txtscripts/ci-file-watchdog.tsscripts/ci-host-snapshot.tsscripts/ci-leg-checkpoint.tsscripts/ci-leg-ledger.tstest/unit/ci-leg-checkpoint.test.tstest/unit/ci-leg-ledger.test.tstest/unit/fixtures/sweep-30665449265-jobs.json
📜 Review details
⏰ Context from checks skipped due to timeout. (6)
- GitHub Check: copilot-pull-request-reviewer
- GitHub Check: Unit Tests (windows-latest)
- GitHub Check: Unit Tests & Coverage
- GitHub Check: Analyze (javascript-typescript)
- GitHub Check: Analyze (javascript-typescript)
- GitHub Check: Analyze (actions)
🧰 Additional context used
📓 Path-based instructions (4)
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
In Bun-based TypeScript code, use
Bun.spawn(),Bun.write(),Bun.sleep(),bun:test, and ESM imports with.tsextensions.
Files:
test/unit/ci-leg-checkpoint.test.tsscripts/ci-leg-ledger.tstest/unit/ci-leg-ledger.test.tsscripts/ci-host-snapshot.tsscripts/ci-leg-checkpoint.tsscripts/ci-file-watchdog.ts
test/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
Do not turn a red integration test green by broadening timeouts, skipping it, or platform-gating it before reproducing and root-causing the failure.
Files:
test/unit/ci-leg-checkpoint.test.tstest/unit/ci-leg-ledger.test.ts
**/*.ts
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
**/*.ts: Use Bun APIs and tooling rather than Node.js equivalents:Bun.spawn(),Bun.write(),Bun.sleep(),bun test, andbun:test. Use ESM with.tsextensions in imports; do not use CommonJS.
For ARM64virtmachines, never useif=virtiofor drives; use an explicit-device virtio-blk-pci,drive=drive0configuration.
When using HVF acceleration, use-cpu host, notcortex-a710.
For arm64 guests on macOS, automatically select TCG with-cpu cortex-a710; HVF cannot run the CHR image's 32-bit ARM userspace on Apple Silicon.--accelandQUICKCHR_ACCELmust override this selection for testing.
UEFI pflash code and vars units must be identical in size.
QGA is x86-only; do not assume the guest agent starts for arm64 CHR.
Use tabs for indentation.
Do not add unnecessary comments to obvious code.
Errors must be thrown asQuickCHRError(code, message, installHint?).
Preserve the documented public API types and behavior:QuickCHR.start(opts)returnsChrInstance;ChrInstanceprovidesstop(),remove(),rest(),monitor(),serial(), andqga(); andMachineStaterepresents persistedmachine.jsonstate.
Files:
test/unit/ci-leg-checkpoint.test.tsscripts/ci-leg-ledger.tstest/unit/ci-leg-ledger.test.tsscripts/ci-host-snapshot.tsscripts/ci-leg-checkpoint.tsscripts/ci-file-watchdog.ts
test/unit/**/*.ts
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
Unit tests must be fast and must not require QEMU.
Files:
test/unit/ci-leg-checkpoint.test.tstest/unit/ci-leg-ledger.test.ts
🧠 Learnings (4)
📚 Learning: 2026-07-28T00:12:59.340Z
Learnt from: mobileskyfi
Repo: tikoci/quickchr PR: 101
File: .github/workflows/integration.yml:342-342
Timestamp: 2026-07-28T00:12:59.340Z
Learning: For tikoci/quickchr, review workflow changes to ensure GitHub Actions are pinned to immutable commit SHAs as a repository-wide policy. If a PR introduces SHA pinning for only part of a workflow (or only some workflows) without extending the same policy across the relevant workflow files, flag it. When adding/changing SHA pinning, also verify Dependabot is configured to update the `github-actions` ecosystem so action version bumps are managed consistently (e.g., in the repo’s Dependabot configuration), rather than doing an isolated pinning change in an unrelated PR.
Applied to files:
.github/workflows/main.yml.github/workflows/sweep.yml.github/workflows/integration.yml
📚 Learning: 2026-07-31T11:56:27.561Z
Learnt from: mobileskyfi
Repo: tikoci/quickchr PR: 117
File: .github/workflows/integration.yml:188-188
Timestamp: 2026-07-31T11:56:27.561Z
Learning: For the tikoci/quickchr repository, do not request isolated SHA pinning of first-party GitHub Actions in a single workflow or pull request. Treat action pinning as repository-wide work: first configure Dependabot for the github-actions ecosystem, then mechanically pin all workflow action references, and finally decide whether to enforce zizmor in CI. Apply this guidance to workflow files while issue `#118` tracks the rollout.
Applied to files:
.github/workflows/main.yml.github/workflows/sweep.yml.github/workflows/integration.yml
📚 Learning: 2026-07-31T11:56:40.455Z
Learnt from: mobileskyfi
Repo: tikoci/quickchr PR: 117
File: scripts/ci-cache-key.ts:68-68
Timestamp: 2026-07-31T11:56:40.455Z
Learning: In Bun TypeScript files, do not flag use of `node:fs.appendFileSync` when append semantics are required and `Bun.write()` cannot provide them. This applies to cases such as appending multiple entries to `$GITHUB_OUTPUT` or writing to the boot log; use append-capable file operations rather than overwriting existing content.
Applied to files:
test/unit/ci-leg-checkpoint.test.tsscripts/ci-leg-ledger.tstest/unit/ci-leg-ledger.test.tsscripts/ci-host-snapshot.tsscripts/ci-leg-checkpoint.tsscripts/ci-file-watchdog.ts
📚 Learning: 2026-07-31T11:56:38.870Z
Learnt from: mobileskyfi
Repo: tikoci/quickchr PR: 117
File: scripts/ci-cache-key.ts:110-110
Timestamp: 2026-07-31T11:56:38.870Z
Learning: In Bun CI scripts under scripts/, use a plain Error for validation failures when the script catches the error and renders it as a GitHub ::error:: annotation. QuickCHRError is intended for programmatic library callers and is not required for this CLI-only error path.
Applied to files:
scripts/ci-leg-ledger.tsscripts/ci-host-snapshot.tsscripts/ci-leg-checkpoint.tsscripts/ci-file-watchdog.ts
🪛 zizmor (1.28.0)
.github/workflows/main.yml
[warning] 46-46: permissions without explanatory comments (undocumented-permissions): needs an explanatory comment
(undocumented-permissions)
.github/workflows/sweep.yml
[warning] 39-39: permissions without explanatory comments (undocumented-permissions): needs an explanatory comment
(undocumented-permissions)
.github/workflows/integration.yml
[warning] 427-427: permissions without explanatory comments (undocumented-permissions): needs an explanatory comment
(undocumented-permissions)
[warning] 869-869: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
🔇 Additional comments (22)
scripts/ci-leg-ledger.ts (5)
44-135: LGTM!
141-221: LGTM!
226-254: LGTM!
258-286: LGTM!
296-334: LGTM!test/unit/ci-leg-ledger.test.ts (5)
1-61: LGTM!
63-151: LGTM!
153-218: LGTM!
220-259: LGTM!
352-364: LGTM!test/unit/fixtures/sweep-30665449265-jobs.json (1)
1-569: LGTM!scripts/ci-host-snapshot.ts (1)
1-113: LGTM!scripts/ci-file-watchdog.ts (1)
65-67: LGTM!Also applies to: 206-233, 289-289, 319-319, 349-349
.github/instructions/ci.instructions.md (1)
153-272: LGTM!DESIGN.md (1)
230-256: LGTM!project-words.txt (1)
59-59: LGTM!Also applies to: 429-429
scripts/ci-leg-checkpoint.ts (1)
1-243: LGTM!Also applies to: 258-350
test/unit/ci-leg-checkpoint.test.ts (1)
1-149: LGTM!.github/workflows/integration.yml (2)
422-427: LGTM! Verified thechecks: write/actions: readscoping matches the aggregate job's needs,mark/closeshell calls use|| truecorrectly underset -eo pipefail, and the reconcile step runs after the fold, consistent with the artifact-first precedence documented inci.instructions.md. The zizmorundocumented-permissionshint (line 427) andtemplate-injectionhint (line 869,steps.run.outcomeis a fixed GitHub-generated enum) are false positives here — the permissions block already carries an explanatory comment, andsteps.run.outcomeis not attacker-controllable.Also applies to: 771-790, 809-809, 834-842, 855-870, 1045-1046, 1067-1068, 1097-1103, 1127-1146, 1155-1158
583-583: LGTM!Also applies to: 594-597
.github/workflows/main.yml (1)
42-48: LGTM! The zizmorundocumented-permissionshint on line 46 is a false positive — the block already carries an explanatory comment directly above it..github/workflows/sweep.yml (1)
34-41: LGTM! The zizmorundocumented-permissionshint on line 39 is a false positive — the block already carries an explanatory comment directly above it.
CodeRabbit review of #124. The load-bearing one: `completedLegs` parsed every ndjson line unguarded, so one truncated record would throw out of `build()` — no ledger written, and `finalizeLostCheckRuns` never reached, leaving lost-runner check runs stuck `in_progress`. The script would have failed shut in exactly the circumstances it was built for. Now a bad line costs that line, with a warning naming the file; same guard on the persisted `attempted-legs.json`, which would otherwise stay broken forever after one corrupt write. Also: check the PATCH response in `finalizeLostCheckRuns` (fetch resolves on 403, so a rejected finalize was silent), export `mapJobsToLegs` so the anchor test drives the production join instead of a copy that could pass while the real one broke, append to the step summary rather than read-modify-write, and use Bun.write per the repo's Bun-API rule.
|
All six CodeRabbit findings applied in The one that mattered. The same guard now covers My own The rest:
Re-verified: One note for the record — this PR is deliberately linked to |
Copilot review of #124. classifyLeg treated ANY check run left `in_progress` as `runner-lost`, including on a job whose steps all reached a terminal state. But the `close` PATCH is best-effort and can fail transiently — so a plain API hiccup was being reported as runner loss, which is the precise confusion this design was built to prevent (the same reasoning that put `not-started` in the vocabulary). The jobs API is server-side and unconditional, so it now wins that disagreement: an un-closed check run only means runner loss when no job corroborates otherwise. Also, the watchdog's sidecar wrote `cap_s: 0` for a `not-run` file, which rendered in the checkpoint table as a 0-second timeout — a wedge that never happened rather than 'the budget was already spent'. The field is now omitted when no cap was ever applied.
|
Copilot's two findings applied in
The rule is now explicit: when the two instruments disagree, the jobs API wins. It is server-side and unconditional; the checkpoint posts best-effort and silently tolerates its own failures, so it is the weaker witness by construction. An un-closed check run means runner loss only when no job corroborates otherwise — and that case now reports The sidecar's Full suite: 945 pass / 0 fail, All eight review threads resolved. Ready for your call on merge — the only open question is the deliberate |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.
Suppressed comments (2)
scripts/ci-leg-ledger.ts:8
- The header comment documents a
--artifactsflag, butbuild()doesn’t accept it and the workflow invokes this script without it. This can mislead maintainers when running the script locally; please align the documented invocation with the actual CLI.
* bun scripts/ci-leg-ledger.ts build --data <ci-data-dir> \
* --artifacts <dir> --matrix <plan matrix json file>
*
scripts/ci-leg-ledger.ts:415
finalizeLostCheckRuns()forces the check run conclusion tofailure, which contradicts the checkpoint script’s stated goal of avoiding a second red check for an already-red leg (seeci-leg-checkpoint.tsclose()). Consider closing these orphaned check runs asneutraland relying on the job itself + the check title/summary to carry the signal, so PR/status views don’t double-count failures.
body: JSON.stringify({
status: "completed",
conclusion: "failure",
completed_at: new Date().toISOString(),
Two findings Copilot suppressed rather than posted, both correct. finalizeLostCheckRuns closed orphaned check runs as `failure`, which contradicts the rule ci-leg-checkpoint.ts:327 states and follows: use `neutral`, because the leg's own job already carries the red. A lost runner is not the exception it looks like — GitHub does conclude the job, ~45 min late but red (`job_conclusion: "failure"` in run 30750979859's own ledger entry), so `failure` here showed one dead leg twice in every branch-protection and PR-status view. What this check run carries is its title and summary, not its color. The header also documented an `--artifacts` flag that build() never accepted and no workflow passes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Final pass before merge. Three things closed, one of them a new finding. Copilot's two suppressed comments were both correct — The load-bearing one: That is the third finding in this PR of the same shape: a rule I wrote down and applied in one place but not the neighbouring one. The other two were The
Unit tests green locally (31 pass), all 8 checks green on the pushed head. |
B5 of #110 — part 2 of #77, the half B4 explicitly could not do. B4 bounded a file that hangs; this makes a lost runner diagnosable.
A leg whose runner stops talking left nothing behind: no artifact, no metrics record, no row anywhere. The 2026-07-31 sweep planned 15 integration legs and
ci-datareceived 12 ndjson files, with no marker for the missing three. A reader comparing platforms saw silence and had to guess whethermacos-x86had never run or had died trying.Both open design decisions got a maintainer call first
Recorded on #110 before any code was written, per the bite's instruction not to pick silently.
Decision 1 — a separate ledger, not
tested-versions.json. The version scheduler reads that file as a presence test (ros-versions.yml:99), so writingincompleteintoconclusionwould make an aborted run look tested and silently stop rescheduling that version, forever, with no error. Today'smacos-x86case would not trip it (the scheduler only readslinux-x86), but the contract is one platform away from breaking. So: a newci-data/attempted-legs.json, and that jq is untouched.foldTestedVersions'sscope:"full"rule is unchanged.Decision 2 — a per-leg check run. Rejected alternatives, one of them on evidence gathered before claiming.
What survives losing a runner — measured, not assumed
The test loop already prints
::notice::Running <file>per file, so scraping the job log would have cost nothing. It does not survive. On run 30665449265:The contrast with the normally failing windows leg is the load-bearing half: failure preserves the log; runner loss destroys it. The jobs API step ledger, by contrast, does survive — a vanished leg still records its test step as
in_progresswith a start time. That is free and now used, but it stops at step granularity and can never name the file. This table is recorded inDESIGN.mdso the next instrument does not re-derive it.The design
scripts/ci-leg-checkpoint.tsscripts/ci-leg-ledger.tsaggregate: comparesplan's matrix against the legs that reachedci-data/runs/, writesattempted-legs.json, annotates the run summary, and closes check runs the runner never got to.scripts/ci-host-snapshot.tsCOLD_DOWNLOAD_FLOOR_BYTES_PER_Sgot in B13. These numbers get compared across the two instruments, so a second copy free to drift would make the comparison noise.The artifact outranks both instruments. A leg is
completebecause it produced a metrics record — never because a check run says so. The checkpoint is best-effort (warns and exits 0 on any API failure, so a fork PR's read-only token or a dropped PATCH cannot red a leg), which means trusting it first would let instrument failure fabricate arunner-lostfor a leg that finished cleanly. There is an anchor test for exactly that.Verdicts:
complete,runner-lost,attempted-incomplete,not-started. That last one is not in #77's vocabulary and is deliberate — without it a leg that died inInstall QEMUis indistinguishable from one whose runner vanished mid-suite, and mislabelling a setup failure asrunner-lostsends #76 chasing a mechanism that was never involved.Verification
1. Replay against the real sweep (committed as an anchor test). The classifier, fed the actual job records from run 30665449265 — 15 planned, 12 complete, the three
macos-x86legs namedrunner-lost, all 15 jobs matched by name. Fixture trimmed to the fields classification reads. This is the only test driven by a genuine runner-loss event rather than one written to match its own expectations.2. Happy path on a real runner — run 30750868899, green. Check run posted, marked and closed; ledger wrote
1/1 legs completeand committed toci-datawith noincompletemap.3. An intentionally killed runner — the bite's stated exit criterion, which cannot be verified locally. Run 30750979859, dispatched from a temporary commit that
sudo halt -f -ps the runner after the first file. That commit was dropped and the branch force-pushed before this PR was opened — it is not in the diff, and no such lever ships.The reproduction matched #76 exactly: step 17 stuck
in_progress, steps 18+ never started,if: always()never fired, no artifact, and the job log blob returnsBlobNotFound— a second, self-controlled confirmation of the log finding above. What the ledger recorded anyway:and the aggregate closed the orphaned check run as
runner-lost — last file disk.test.ts.The gap between the wedge and the verdict is ~45 minutes, and the checkpoint closes it
This run measured something #110 had listed as uncharacterized. The runner was halted at 13:57:26; GitHub declared the job failed at 14:42:03. The service waited ~44.6 minutes before deciding the runner was gone.
last_checkpoint_tsis 13:57:25 — within two seconds of the actual wedge, and 44.6 minutes earlier thancompleted_at. That is the whole value of the instrument:job_elapsed_s: 2760is an upper bound that overstates the wedge by ~16x here, and the checkpoint replaces it with a timestamp.Do not carry the 44.6 min figure across platforms or loss modes (#110 rule 6). It is one measurement, on
ubuntu-latest, of a clean halt — a silent network partition or a wedged-but-alive runner may well get a different verdict latency. What it does establish is that the interval is tens of minutes, not seconds, so readingcompleted_atas the wedge time is wrong by a lot. Whether #76'smacos-x86legs go quiet far earlier than their 62-65 mincompleted_atis now a measurable question rather than an unanswerable one — B8a is the bite that should answer it, and it now has an instrument that can.Notes for review
checks: write; aggregate needschecks: write+actions: read. Reusable-workflow permissions are capped by the calling job, somain.ymlandsweep.ymlgrant both. A caller that forgets them gets warnings and an empty ledger, not a red run.integrationJobName()is its one home and is asserted against production job names by a test; ifintegration.yml'sname:is edited without it, every entry degrades tojob_matched: false.hostUptimeSreplaces B4'suptimeS, which reportedprocess.uptime()— the watchdog process's own life, duplicating itselapsed_sand reading ~0 in every checkpoint. Renamed rather than reused so nothing reads a checkpoint's number as if it meant the watchdog's.ci-dataREADME updated — done,54a2192on the data branch.attempted-legs.jsonhas been written by the aggregate job since run 30750868899 but was undocumented; the README now carries the four terminals, the artifact-outranks-the-instrument rule, whylast_checkpoint_tsrather thanjob_elapsed_sis the timestamp to read, and a query recipe verified against the real file.This PR is deliberately linked to #77 — check that before merging
Per the
#112→#79hazard, a PR should link its sub-issue only when it satisfies that issue's whole done-when. This one does, so the link is intentional rather than accidental:runner-lostand last completed/current file2899be4data, checked in, documentedThe one judgement call is resolved: the link stays, and the residue is filed as #125. #77's Proposed approach §4 also lists
operation-timeout,infra-download/infra-cacheandcancelled/superseded, which are not implemented — they need output classification neither bite does, andci-file-watchdog.ts:113says so rather than leaving it silent. They are not in the done-when, and #110's rule is that a PR links its sub-issue when it satisfies that issue's whole done-when — which this does, all five rows above. So closing #77 here is correct, and #125 exists so the unbuilt half does not evaporate with it.Closes #77.
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Documentation