Skip to content

ci: sample host state from inside the file where the wedge happens (B8d of #110) - #136

Merged
mobileskyfi merged 6 commits into
mainfrom
ci/b8d-in-file-heartbeat
Aug 3, 2026
Merged

ci: sample host state from inside the file where the wedge happens (B8d of #110)#136
mobileskyfi merged 6 commits into
mainfrom
ci/b8d-in-file-heartbeat

Conversation

@mobileskyfi

@mobileskyfi mobileskyfi commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

B8d of #110. Refs #110, #76. Not linked to close either — this lands an instrument, and per
operating rule 7 an instrument is not a diagnosis. The dispatch that consumes it is running against
this branch; its result goes to #76 as evidence.

The gap this closes

B5's checkpoint marks file boundaries. That is what named #76's wedge — all four known legs die in
provisioning.test.ts — and it is structurally unable to describe it: B8b's live log capture (the only
copy, since runner loss destroys the blob) puts the freeze 3.5–7.6 min inside that file, at its
5th/6th test. So free disk, free memory, load and qemuCount have never been read anywhere in the
window the leg actually dies in
. B8a's reassuring "111 GB free, qemuCount 0" is a boundary reading
from a leg that never reached position 10. The maintainer's disk-space hypothesis, and the memory one,
are open because nothing has looked — not because anything cleared them.

scripts/ci-leg-heartbeat.ts samples the host on an interval and posts a trailing window to a check
run, attributing every sample to the file the leg is inside and how deep into it the reading is —
which is what makes a sample comparable to the 3.5–7.6 min figure at all.

Three decisions worth reviewing

It owns a second check run instead of extending B5's. The obvious implementation adds rows to the
existing checkpoint. It is wrong: mark and a heartbeat would both be read-modify-write writers of one
payload, and the interleaving — heartbeat reads state, mark writes a record and PATCHes, heartbeat
PATCHes its stale copy — drops the newest file record from the server-side output. The window is a
second wide and self-heals on the next tick, except in the one case this program exists for: the
runner dying inside it. Not worth taking to save a check run.

The external_id carries a fifth segment (…/hb) and parseExternalId takes exactly four, so
ci-leg-ledger skips heartbeats with no special case and can never fabricate a runner-lost from one.
test/unit/ci-leg-heartbeat.test.ts asserts that across both modules rather than leaving it to the
reader.

It is opt-in, and plan errors above 3 legs. GITHUB_TOKEN is 1000 requests/hour/repository,
shared with the checkpoint and the aggregate. At 30 s a leg posts 120 writes/hour — fine for the 1–3 leg
dispatches that chase #76 (every known wedge is bounded to ~16–20 min, so those are cheap), and
~1800/hour across a 15-leg sweep, which would throttle the durable record to feed a diagnostic.
That is a refusal rather than a warning because a throttled checkpoint is invisible until someone needs it.

Two HostSnapshot fields that were specified and never built. #77's per-checkpoint list said "free
memory / memory pressure" and only the first half exists. On macOS the first half barely means
anything alone: os.freemem() counts only genuinely free pages, so a host with gigabytes of purgeable
cache and a host swapping to death report the same small number. macOS legs now carry memFreePct
(memory_pressure -Q) and swapUsedMiB (vm.swapusage) — measured working on the maintainer's Intel
Mac, where freemem() said 2758 MiB while the kernel reported 86% free, which is the whole point.
freeDataDiskMiB is likewise separate from freeDiskMiB because on Windows the workspace is on D:
and quickchr's state is on C:, and it is the quickchr volume that fills with images and machine disks.

Safety

  • Same contract as ci-leg-checkpoint.ts: every API failure is a ::warning:: and exit 0, and the
    workflow ignores the process's status. An instrument that reds the leg it measures manufactures the
    masked signal this program exists to stop.
  • Failed posts are counted and declared in the check's own summary, so a gap in the series is
    attributable to the instrument rather than misread as the host going quiet.
  • It self-terminates at the step deadline, and a trap … EXIT stops it however the file loop ends
    (clean exit, red file, or the break after an unclean reap). Without that trap it would keep sampling
    the idle host through the forensic steps and overwrite the last rows before a wedge with reassuring ones.
  • output.text is bounded against the checks API's 65535-char limit by rendered bytes, not just by
    sample count — a rejected PATCH here would not fail loudly, it would silently stop updating and leave
    the wedge unsampled. There is a test that adding a HostSnapshot field cannot break that bound.

Verification

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added optional heartbeat monitoring for integration test runs.
    • Captures per-file progress and host resource metrics, including macOS memory and swap usage.
    • Publishes monitoring results and logs as run artifacts and dedicated check summaries.
    • Added configurable heartbeat intervals for manual and reusable workflows.
  • Bug Fixes

    • Improved diagnostics for identifying resource-related causes of stalled test runs.
  • Tests

    • Added coverage for heartbeat reporting, payload handling, sampling limits, and checkpoint scenarios.

…8d of #110)

B5's checkpoint marks file boundaries, which is what named #76's wedge — every
known leg dies in `provisioning.test.ts` — and is structurally unable to describe
it, because the freeze is 3.5-7.6 min inside that file. Free disk, free memory,
load and qemuCount have never been read anywhere in that window; B8a's "111 GB
free, qemuCount 0" is a boundary reading from a leg that never reached position
10. The disk and memory hypotheses are open because nothing has looked.

`scripts/ci-leg-heartbeat.ts` samples the host on an interval and posts a
trailing window to a check run, attributing each sample to the file the leg is
inside and how deep into it the reading is.

It owns a SEPARATE check run rather than extending B5's. Two read-modify-write
writers of one payload would race, and the worst case is a stale PATCH dropping
the newest file record — the durable evidence B5 exists to preserve, lost exactly
when the runner dies. Its external_id carries a fifth segment, and
parseExternalId takes exactly four, so the ledger skips heartbeats with no
special case and can never build a runner-lost from one. That is asserted.

Opt-in via `heartbeat-interval`, and `plan` errors above 3 legs: GITHUB_TOKEN
allows 1000 requests/hour/repository, shared with the checkpoint and the
aggregate. 30 s is 120 writes/hour per leg — fine for the 1-3 leg dispatches that
chase #76, ~1800/hour across a sweep, which would throttle the durable record to
feed a diagnostic.

Host snapshot gains macOS memory pressure and swap: os.freemem() cannot separate
purgeable cache from swapping to death, and both report a small number. #77's
checkpoint list said "free memory / memory pressure" and only the first half was
ever built. It also gains free disk on the quickchr data-dir volume, which is not
the workspace volume on Windows.

Refs #110, #76.
Copilot AI review requested due to automatic review settings August 3, 2026 22:29
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 86664458-f88e-45b7-81d6-5ae59de83920

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Changes

The PR adds optional in-file CI heartbeat sampling. The sampler records host metrics and checkpoint progress, publishes bounded updates to a dedicated GitHub check run, and uploads local samples and logs. The workflow validates intervals and limits enabled runs to three legs.

CI heartbeat diagnostics

Layer / File(s) Summary
Host snapshot metrics
scripts/ci-host-snapshot.ts
Host snapshots can measure data-volume disk space and macOS memory and swap metrics.
Heartbeat sampling and validation
scripts/ci-leg-heartbeat.ts, test/unit/ci-leg-heartbeat.test.ts
The sampler reads checkpoint progress, records host samples, bounds check-run payloads, handles API failures, and stops at deadlines or stop files. Unit tests cover rendering, retention, parsing, identity, and file attribution.
Workflow controls and diagnostic documentation
.github/workflows/integration.yml, .github/instructions/ci.instructions.md
The workflow accepts and validates heartbeat-interval, limits enabled runs to three legs, manages sampler cleanup, and uploads heartbeat artifacts. CI instructions document the instrumentation.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant IntegrationStep
  participant ciLegHeartbeat
  participant CheckpointState
  participant ciHostSnapshot
  participant GitHubCheckRun
  IntegrationStep->>ciLegHeartbeat: Start with interval and deadline
  ciLegHeartbeat->>CheckpointState: Read file progress
  ciLegHeartbeat->>ciHostSnapshot: Collect host metrics
  ciLegHeartbeat->>GitHubCheckRun: Publish bounded sample payload
  IntegrationStep->>ciLegHeartbeat: Signal stop on exit
  ciLegHeartbeat->>GitHubCheckRun: Complete heartbeat check run neutrally
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: sampling host state from inside the test file where the CI wedge occurs.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/b8d-in-file-heartbeat

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Not ready to approve

It contains a confirmed Windows free-disk parsing bug (empty output becomes 0 MiB) plus a stray “XX” doc-comment artifact, both of which should be fixed before merge.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.

Pull request overview

This PR adds an opt-in “in-file heartbeat” instrument for the integration workflow to periodically sample host state (disk/memory/load/QEMU process count) from inside the currently running test file and publish a trailing window via a dedicated GitHub check run, specifically to help diagnose the macos-x86 runner-loss wedge in provisioning.test.ts (#76 / B8d of #110).

Changes:

  • Add scripts/ci-leg-heartbeat.ts to post periodic host snapshots to a dedicated hb: check run, attributing each sample to the current test file + in-file depth.
  • Extend scripts/ci-host-snapshot.ts with freeDataDiskMiB and macOS-only memFreePct/swapUsedMiB, plus support for measuring disk free space for an arbitrary path.
  • Wire the instrument into integration.yml behind a new heartbeat-interval experiment lever (validated and capped to ≤3 legs) and document it in CI instructions; add unit tests.
File summaries
File Description
test/unit/ci-leg-heartbeat.test.ts New unit tests for heartbeat external_id behavior, payload round-trip, trimming/byte budget, and checkpoint-based file attribution.
scripts/ci-leg-heartbeat.ts New heartbeat sampler that posts to a dedicated check run and maintains a bounded trailing window payload.
scripts/ci-host-snapshot.ts Adds data-dir disk free measurement + macOS memory-pressure/swap readings; generalizes disk-free probing to an arbitrary path.
.github/workflows/integration.yml Adds heartbeat-interval input, validates/caps it in plan, and runs the heartbeat as a background process during the per-file integration loop.
.github/instructions/ci.instructions.md Documents the new heartbeat lever and its operational constraints.
Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 3
  • Review effort level: Lite

We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.

Comment thread scripts/ci-host-snapshot.ts
Comment thread scripts/ci-leg-heartbeat.ts
Comment thread scripts/ci-leg-heartbeat.ts
A cold leg does not create ~/.local/share/quickchr until its first machine, and
`df` on a path that does not exist fails — so the first samples of a leg carried
no `freeDataDiskMiB` at all. Observed live on run 30858745297. A hole there reads
like a failed measurement rather than "same volume, nothing written yet", which
is the opposite of what a disk-exhaustion hypothesis wants from its baseline.

Refs #110, #76.
1. Windows free-disk could report a fabricated 0. A failed PowerShell expression
   leaves stdout empty, and Number("") is 0, which Number.isFinite accepts — so
   an unreadable volume reported "0 MiB free" instead of "not measured". That is
   a manufactured disk-exhaustion signal on the one platform where the workspace
   and quickchr volumes genuinely differ, feeding the exact hypothesis this
   snapshot exists to test. Absent now stays absent.
2. The sample file is appended to, not rewritten. Re-encoding the whole array
   each tick is O(n^2) over a leg, so the sampler's own cost would grow with
   elapsed time and perturb the late-in-the-leg window it exists to measure —
   accumulation it caused itself. It also truncates any stale file at startup,
   since appending now makes a leftover prefix another leg's samples.
3. Repaired a mangled sentence in the header (a bad in-place edit left an 'XX').

Refs #110, #76.
3/3 wedged again — position 10 now 7/7. Disk exhaustion and memory exhaustion
are refuted from inside the fatal window rather than inferred from boundaries:
~108 GB free, kernel memory pressure flat at 89%, zero swap, one QEMU process,
and the leg dies during a VM's life.

Also retires three assumptions. '~16-20 min' is a range, not a deadline — no
constant on any of three clocks, so there is no hidden runner timer to hunt.
Suite growth is why legs reach position 10, not why they die there: windows-x86
and macos-arm64 both run LONGER on the same commit and finish. And a load
precursor visible on one leg does not replicate on the other two, which is worth
writing down because opening one wedged leg makes it look like the answer.

Refs #110, #76.
@mobileskyfi

Copy link
Copy Markdown
Contributor Author

The dispatch this instrument was built for has run, and it worked. Evidence: #76 (comment)

Three full-sequence macos-x86 · stable legs at heartbeat-interval=30, 3/3 wedged in provisioning.test.ts (position 10 now 7/7). The instrument behaved exactly as designed under the condition it exists for: zero failed posts on all three legs, every field populated, and each hb: check run left in_progress with its last sample intact after its runner vanished — which is the whole point, since runner loss destroys the job log and the artifact.

What it found: disk exhaustion and memory exhaustion are both refuted, measured inside the fatal window instead of inferred from file boundaries. ~108 GB free on the quickchr data volume, memory_pressure -Q flat at 89% free, swap 0 MiB, qemuCount 1 at every freeze. The two macOS-only fields added in this PR are what settle the memory half — os.freemem() alone (~4.7 GiB of 14 GiB) cannot separate purgeable cache from swapping to death.

hostUptimeS also paid for itself unexpectedly: it made three independent clocks checkable, and none is constant at the freeze (test-step elapsed 16.8/19.9/24.6 min, host uptime 29.5/33.9/55.1 min, in-file 234/308/531 s). There is no hidden runner-side timer — "~16–20 min" was a range being read as a deadline.

Findings recorded in ci.instructions.md (04b9be4). No code changes from the run — the diff is unchanged since the Copilot fixes.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 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 @.github/workflows/integration.yml:
- Around line 351-355: Update the HEARTBEAT validation in the workflow’s
numeric-check block to reject positive values below 10, matching the stated
message and the 10-second minimum used by ci-leg-heartbeat. Preserve acceptance
of whole-number intervals from 10 upward and the existing invalid-input error
path.

In `@scripts/ci-host-snapshot.ts`:
- Around line 15-16: Replace Node compatibility APIs with Bun or web-standard
equivalents across the affected sites: in scripts/ci-host-snapshot.ts lines
15-16, update existsSync and dirname usage; in scripts/ci-leg-heartbeat.ts lines
53-54, replace existsSync, readFileSync, and rmSync while retaining
appendFileSync; and in scripts/ci-leg-heartbeat.ts line 139 plus
test/unit/ci-leg-heartbeat.test.ts lines 148-157, measure UTF-8 byte length with
Blob.prototype.size or TextEncoder instead of Buffer.byteLength.

In `@scripts/ci-leg-heartbeat.ts`:
- Around line 279-285: Update readCheckpoint to validate the parsed JSON before
returning it: require startedAt to be a string, planned to be an array of
strings, and records to be an array; return undefined for any schema mismatch
while preserving the existing parse-error behavior.
- Around line 144-146: Update the title construction around the samples count to
use singular “sample” when samples.length is one and plural “samples” otherwise,
while preserving the existing no-samples text and file/elapsed details. Update
the corresponding assertion to expect the singular wording.
🪄 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: 24b27098-2c84-417f-abbe-f5d2ac2e0ec1

📥 Commits

Reviewing files that changed from the base of the PR and between 5136c22 and 04b9be4.

📒 Files selected for processing (5)
  • .github/instructions/ci.instructions.md
  • .github/workflows/integration.yml
  • scripts/ci-host-snapshot.ts
  • scripts/ci-leg-heartbeat.ts
  • test/unit/ci-leg-heartbeat.test.ts
📜 Review details
⏰ Context from checks skipped due to timeout. (5)
  • GitHub Check: Unit Tests (windows-latest)
  • GitHub Check: Unit Tests & Coverage
  • GitHub Check: Analyze (javascript-typescript)
  • GitHub Check: Analyze (actions)
  • GitHub Check: Analyze (javascript-typescript)
🧰 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 .ts extensions.

Files:

  • test/unit/ci-leg-heartbeat.test.ts
  • scripts/ci-leg-heartbeat.ts
  • scripts/ci-host-snapshot.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-heartbeat.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, and bun:test. Use ESM with .ts extensions in imports; do not use CommonJS.
For ARM64 virt machines, never use if=virtio for drives; use an explicit -device virtio-blk-pci,drive=drive0 configuration.
When using HVF acceleration, use -cpu host, not cortex-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. --accel and QUICKCHR_ACCEL must 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 as QuickCHRError(code, message, installHint?).
Preserve the documented public API types and behavior: QuickCHR.start(opts) returns ChrInstance; ChrInstance provides stop(), remove(), rest(), monitor(), serial(), and qga(); and MachineState represents persisted machine.json state.

Files:

  • test/unit/ci-leg-heartbeat.test.ts
  • scripts/ci-leg-heartbeat.ts
  • scripts/ci-host-snapshot.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-heartbeat.test.ts
🧠 Learnings (4)
📚 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-heartbeat.test.ts
  • scripts/ci-leg-heartbeat.ts
  • scripts/ci-host-snapshot.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-heartbeat.ts
  • scripts/ci-host-snapshot.ts
📚 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/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/integration.yml
🪛 OpenGrep (1.26.0)
scripts/ci-host-snapshot.ts

[ERROR] 133-133: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)


[ERROR] 143-143: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

Comment thread .github/workflows/integration.yml
Comment thread scripts/ci-host-snapshot.ts
Comment thread scripts/ci-leg-heartbeat.ts
Comment thread scripts/ci-leg-heartbeat.ts
1. A parseable-but-partial checkpoint is now rejected rather than cast. That
   file is written by ANOTHER process while the sampler reads it, so a valid-JSON
   object missing `records` or `planned` is reachable — and locate() reads
   .length on both, so it would throw and take the sampler down at the one moment
   it is the only instrument still reporting. locate()'s own doc promised a torn
   read costs a sample's attribution and never the sample; parseCheckpoint() is
   what makes that true, and it is pure so the guard is tested directly.
2. heartbeat-interval now rejects 1-9 instead of silently running them at 10.
   The error message already claimed a 10s minimum and the script floors there,
   so a dispatch could be labelled with an interval it never used — the same
   flavour of quiet lie the other levers are validated against.
3. '1 samples' -> '1 sample'.

Declined: replacing node:fs sync calls and Buffer.byteLength with Bun/web APIs.
node:fs sync imports are the established pattern in all eight scripts/*.ts,
including ci-leg-checkpoint.ts which this file pairs with and reads from —
converting one file would break the consistency that makes the pair legible.
Buffer.byteLength also does not allocate, where TextEncoder would copy the whole
~60 KB payload on every tick of a sampler whose cost must not grow.

Refs #110, #76.
@mobileskyfi

Copy link
Copy Markdown
Contributor Author

All four review findings addressed — three fixed, one declined with a reason.

Fixed

  1. Partial checkpoint was cast, not checked (CodeRabbit, ci-leg-heartbeat.ts:285) — the important one. That file is written by another process while the sampler reads it, so a valid-JSON object missing records or planned is reachable, and locate() reads .length on both. It would have thrown and killed the sampler at the one moment it is the only instrument still reporting. locate()'s own doc already promised a torn read costs a sample's attribution and never the sample; parseCheckpoint() is what makes that true. Pure, so the guard is tested directly — including that every rejected shape reaches locate() as undefined rather than as a throw.
  2. heartbeat-interval accepted 1–9 and silently ran them at 10 (the script floors there). The error message already claimed a 10 s minimum, so a dispatch could be labelled with an interval it never used — the same quiet lie the other experiment levers are validated against. Now rejected; verified 9 → reject, 10 → accept, abc → reject, empty → off.
  3. 1 samples1 sample.

Copilot's three from the earlier round were fixed in a128f83 (fabricated Windows 0 MiB free, O(n²) sample rewrite, mangled comment).

Declined: replacing node:fs sync calls and Buffer.byteLength with Bun/web APIs.

node:fs sync imports are the established pattern in all eight scripts/*.ts — including ci-leg-checkpoint.ts, which this file pairs with and reads from. Converting one file would break the consistency that makes the pair legible, and Bun.file().exists() is async where readCheckpoint is deliberately synchronous on the sampling path. Buffer.byteLength also does not allocate, where TextEncoder would copy the whole ~60 KB payload on every tick of a sampler whose cost specifically must not grow with elapsed time — that was Copilot's O(n²) point, and it applies here too.

Full unit suite green (982 pass / 0 fail), biome / tsc / cspell / markdownlint clean, all 41 workflow run: steps re-checked with bash -n.

…ver has

A one-factor contrast: three macos-x86 legs identical to the B8d dispatch except
accel=tcg. One completed 12/12 all-pass in 1939 s — the first recorded full-suite
completion on this platform — with provisioning.test.ts passing AT POSITION 10 in
604 s, and a second leg passing the same file at the same position in 810 s.
HVF is 0/7 through that position; TCG is 2/2.

The speed confound does not apply, which is what makes the contrast usable: TCG
per-file cost is within noise of HVF here (nine files, 910 s vs 769/883/937 s),
because this suite is not CPU-bound — it is I/O and RouterOS startup waits, so
emulation costs ~7%. The elapsed-load condition was reproduced and the same ten
in-file VM boots performed; only the accelerator differed.

Triangulated against B7, which bounds the claim to something narrower than 'HVF
is broken': the maintainer's Intel Mac runs the same suite under HVF to
completion in 1818 s. It is HVF ON THE HOSTED RUNNER.

Records the rule that follows: do not pin accel=tcg on macos-x86 to green the
platform. That deletes the only reproduction of a real host-level defect — the
move this file already forbids for quarantining provisioning.test.ts, arriving
through a different door — and costs the project its only HVF coverage.

Refs #110, #76.
@mobileskyfi
mobileskyfi merged commit b31b6ed into main Aug 3, 2026
9 checks passed
@mobileskyfi
mobileskyfi deleted the ci/b8d-in-file-heartbeat branch August 3, 2026 23:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants