Skip to content

Stop the tag stamp from laundering a stale GhosttyKit as pinned - #222

Merged
thdxg merged 2 commits into
mainfrom
claude/recursing-hopper-c171c5
Aug 3, 2026
Merged

Stop the tag stamp from laundering a stale GhosttyKit as pinned#222
thdxg merged 2 commits into
mainfrom
claude/recursing-hopper-c171c5

Conversation

@thdxg

@thdxg thdxg commented Aug 3, 2026

Copy link
Copy Markdown
Owner

.ghosttykit-tag records which thdxg/ghostty release the on-disk fork artifacts came from, so a pin bump can notice they disagree. It was written whenever the two artifact dirs merely existed — never checking whether this run downloaded anything.

Why

A run that downloaded nothing still stamped the current pin over whatever was already there. Only zmx missing is enough: the fork artifacts pass their presence checks, need_zmx skips the early exit, and the write lands. From then on every run sees a stamp agreeing with GHOSTTYKIT_TAG and never refreshes — the staleness becomes invisible and permanent.

This contradicted the AGENTS.md claim that a pre-pin checkout "adopts one on its next real download". It adopts one with no download at all.

Observed, not theoretical. A real checkout stamped build-2026-08-02:

on disk build-2026-08-02
ghostty.h 36357 bytes 36582 bytes
terminfo mtime Jul 14

Two artifacts from two different releases under one stamp — worse than a single stale tag. The framework lacks the release's GHOSTTY_ACTION_EXPORT_TERMINAL_IO, inserted mid-ghostty_action_tag_e, which renumbers 24 constants Macterm references. Swift imports enum constants by name, so it compiles silently either way:

./scripts/ghosttykit-api-diff.sh GhosttyKit.xcframework/macos-arm64_x86_64/Headers/ghostty.h <release-header> on-disk build-2026-08-02

The gate

The write now requires $need_xcframework && $need_resources. Both, because the stamp is one scalar and can only honestly describe a tree where both artifacts came from one release — the observed mixed state is exactly what a looser "did anything download" gate would still launder. A pin bump sets both flags, so the case the stamp exists for still records itself. The dir checks stay as belt-and-braces for the property that a partial setup never stamps (set -e already aborts long before the write; they keep that true if an edit ever softens it).

Why an unstamped tree warns instead of refreshing

Gating alone fixes only half the bug. tag_changed needs a stamp to disagree with, so an unstamped tree still never adopts a bumped pin — honest, but permanently stale in silence.

Forcing a refresh on a missing stamp is the obvious alternative and it's ruled out by CI: the GhosttyKit cache restores precisely that state on every hit, so setup would re-download every run. CI doesn't need the stamp anyway — its cache key hashes setup.sh, which already pins which release the artifacts came from.

So: warn, with rm -rf remediation, and add .ghosttykit-tag to the four cache path lists so a restored tree arrives stamped and the warning stays meaningful rather than firing on every CI run. Transitional wrinkle: existing caches predate this, so each job prints the note once until the weekly key rolls. Self-healing.

The gate stops new launderings but cannot unwind one already written — a tree whose bogus stamp agrees with the pin takes the settled no-op path and stays quietly stale. Curing it is a one-time manual rm -rf GhosttyKit.xcframework Macterm/Resources/terminfo && mise run setup, now documented.

Testing

setup.sh has no test suite, so I built a matrix with a stubbed gh that tags each artifact with the release it came from, then asserts the stamp against on-disk provenance:

ok   fresh-checkout               fw=build-PIN  res=build-PIN  stamp=build-PIN  QUIET
ok   launder-only-zmx-missing     fw=build-OLD  res=build-OLD  stamp=NONE       WARN
ok   settled-unstamped            fw=build-OLD  res=build-OLD  stamp=NONE       WARN
ok   pin-bump                     fw=build-PIN  res=build-PIN  stamp=build-PIN  QUIET
ok   settled-stamped              fw=build-PIN  res=build-PIN  stamp=build-PIN  QUIET
ok   resources-missing-unstamped  fw=build-OLD  res=build-PIN  stamp=NONE       WARN
ok   abi-refresh-unstamped        fw=build-PIN  res=build-OLD  stamp=NONE       WARN
ok   framework-missing-stamped    fw=build-PIN  res=build-PIN  stamp=build-PIN  QUIET
ok   bad-release-rejected         exit 1, stamp still build-OLD, framework untouched
passed 9, failed 0

Run against the pre-fix script it fails 4 — three with LAUNDERED (a stamp naming a tag the artifacts aren't from: only-zmx-missing, resources-missing, abi-refresh), one on the missing warning. So the matrix reproduces the bug rather than passing both ways.

Also ran the real script end-to-end on a fresh worktree: downloaded both, stamped build-2026-08-02, produced the correct 36582-byte header. Re-runs are quiet; removing the stamp produces the note. bash -n clean, and all four workflows parse with exactly 5 cache paths — a comment initially landed inside a path: | block scalar, where it would have become a literal path entry; fixed and verified by the parse.

I skipped mise run test: no Swift changed and the unit suite doesn't exercise setup.sh, so it couldn't validate this. CI is the real gate — editing setup.sh changes the cache key, so these runs download the pin from scratch.

Separate from the GH_PAT issue in bump-ghosttykit.yml, deliberately not bundled.

The stamp is meant to record which thdxg/ghostty release the on-disk fork
artifacts came from, so a pin bump can notice they disagree. It was written
whenever the two artifact dirs merely EXISTED, which meant a run that
downloaded nothing — say only zmx was missing — still stamped the current
pin over whatever was already there. From then on every run saw a stamp
agreeing with GHOSTTYKIT_TAG and never refreshed: the staleness became
invisible and permanent.

A real checkout was found in that state, stamped build-2026-08-02 while
holding terminfo three weeks older than the tag beside a framework whose
ghostty.h was 36357 bytes against the release's 36582 — two artifacts from
two different releases. That framework is missing the release's
GHOSTTY_ACTION_EXPORT_TERMINAL_IO, which renumbers 24 ghostty_action_tag_e
constants Macterm references; Swift imports those by name, so it compiles
silently either way.

Gate the write on the need_* flags. Both must be true because the stamp is
one scalar and can only honestly describe a tree where both artifacts came
from one release. A pin bump sets both, so the case the stamp exists for
still records itself. The dir checks stay as belt-and-braces for the
property that a partial setup never stamps.

Gating alone fixes only half of it: tag_changed needs a stamp to disagree
with, so an unstamped tree still never adopts a bumped pin — honest, but
permanently stale in silence. Forcing a refresh instead is ruled out by CI,
which restores exactly that state from its GhosttyKit cache on every hit.
So warn, and cache .ghosttykit-tag alongside the artifacts it describes:
CI's cache key already hashes setup.sh, so a restored tree is stamped and
the warning stays meaningful rather than firing on every run.

The gate stops new launderings but cannot unwind one already written — a
tree whose bogus stamp agrees with the pin takes the settled no-op path, so
curing it is a one-time manual rm -rf and re-run.
@thdxg thdxg added bug Something isn't working area:ci CI workflows, dev tooling area:docs Documentation labels Aug 3, 2026
@github-actions github-actions Bot added the area:release Release workflow, Info.plist label Aug 3, 2026
Conflict was the pin line: main bumped it to build-2026-08-03 (#221) while
this branch rewrote the comment block above it. Kept both — the new pin and
the rewritten $TAG_STAMP rationale.
@github-actions github-actions Bot added the benchmark:regression CI benchmark: significant resource regression vs main label Aug 3, 2026
@thdxg
thdxg merged commit 1a30a21 into main Aug 3, 2026
10 checks passed
@thdxg
thdxg deleted the claude/recursing-hopper-c171c5 branch August 3, 2026 16:36
@github-actions github-actions Bot added the benchmark:improvement CI benchmark: significant resource improvement vs main label Aug 3, 2026
@github-actions

github-actions Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Window-state benchmark

State Metric main@e868f688f this branch Δ
focused CPU % 0.80 0.90 +12%
Memory (RSS MB) 109.3 114.6 +5%
CPU ms/s (powermetrics) 7.5 8.0 +7%
Wakeups/s (powermetrics) 159.4 200.7 +26%
workload-focused CPU % 3.20 2.10 -34% 🔻
Memory (RSS MB) 163.8 171.1 +4%
CPU ms/s (powermetrics) 29.1 19.6 -33% 🔻
Wakeups/s (powermetrics) 214.6 313.3 +46% 🔺
workload-unfocused CPU % 3.00 2.10 -30% 🔻
Memory (RSS MB) 162.1 174.2 +7%
CPU ms/s (powermetrics) 28.6 20.4 -29% 🔻
Wakeups/s (powermetrics) 194.3 281.3 +45% 🔺

⚠️ Labeled benchmark:regression

This PR is labeled benchmark:regression because ≥2 metrics regressed by ≥25% vs main@e868f688f (beyond each metric's noise floor), at least one under workload:

  • workload-focused — Wakeups/s (powermetrics): 214.6 → 313.3 (+46%)
  • workload-unfocused — Wakeups/s (powermetrics): 194.3 → 281.3 (+45%)

🎉 Labeled benchmark:improvement

This PR is labeled benchmark:improvement because ≥2 metrics improved by ≥25% vs main@e868f688f (beyond each metric's noise floor), at least one under workload:

  • workload-focused — CPU %: 3.20 → 2.10 (-34%)
  • workload-focused — CPU ms/s (powermetrics): 29.1 → 19.6 (-33%)
  • workload-unfocused — CPU %: 3.00 → 2.10 (-30%)
  • workload-unfocused — CPU ms/s (powermetrics): 28.6 → 20.4 (-29%)

Reported value is the median of 3×10s windows per state (splitting the window and taking the median keeps one co-scheduled spike from skewing a state); CPU % is the process CPU-time delta over a window. Runs land on different shared runners, so treat small deltas as noise — 🔺/🔻 marks changes ≥25% that also clear the metric's absolute noise floor (CPU % ≥0.5, Memory (RSS MB) ≥25, CPU ms/s ≥5, Wakeups/s ≥50); CPU deltas off a noise-dominated baseline aren't flagged (CPU % baseline ≥1.5, CPU ms/s baseline ≥15). The benchmark:regression / benchmark:improvement label needs corroboration — ≥2 flagged metrics in the same direction, at least one under workload — so a lone noisy cell shows its arrow here without tagging the PR.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:ci CI workflows, dev tooling area:docs Documentation area:release Release workflow, Info.plist benchmark:improvement CI benchmark: significant resource improvement vs main benchmark:regression CI benchmark: significant resource regression vs main bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant