fix/3483-cache-reclaim · closes #3483lifecycle/stale-cache-cleanup is registered in both
hooks.json and entries/lifecycle.ts, so it fired at every SessionStart. It had
reclaimed nothing, ever. Its CACHE_DIR pointed at
.../cache/orchestkit/ork; every prerelease install is orchestkit/ork-alpha.
readdirSync threw ENOENT, a bare catch {} swallowed it, and the hook returned
success.
This is the #959 dead-hook class inverted: fully wired and still dead. No registry-closure test can catch it, because the registration is correct and only the path is wrong.
$ ls ~/.claude/plugins/cache/orchestkit/ ork-alpha # the hook looked for "ork" $ ls ~/.claude/plugins/cache/orchestkit/ork-alpha | wc -l 15 # versions, none reclaimable $ du -sh ~/.claude/plugins/cache/orchestkit/ork-alpha 184M
.in_use is not a marker. It is a directory of live pids.#3483 reads it as a boolean written on use and never cleared. It is not:
$ cat ork-alpha/10.0.0-alpha.35/.in_use/34263
{"pid":34263,"procStart":"Sun Aug 16 21:34:06 2026"}
Census across all 15 versions: 39 entries, 11 live and 28 stale, with live sessions spread over two different versions (alpha.34 and alpha.35). The old policy, "keep the 2 newest", consulted this not at all.
So the path bug was hiding a second one. Had CACHE_DIR been correct, that
policy would have deleted a plugin out from under a running session. The `procStart` field is there
precisely to survive pid reuse, which is a strong hint the mechanism was designed to be read.
| before | after |
|---|---|
one hardcoded channel, ork | enumerates channels under the marketplace root |
bare catch {}, ENOENT reads as "clean" | "not installed" and "unreadable" are distinct outcomes |
| deletes by recency alone | a version held by a live pid survives regardless of age |
| no failure posture | fails closed: unreadable .in_use counts as held |
Fail-closed is deliberate: over-retaining costs disk, under-retaining breaks a live session.
compareVersions is deliberately unchangedIt looks broken for 10.0.0-alpha.35, since Number('0-alpha') is
NaN. But NaN is falsy, so || 0 maps it to 0 on both sides, the comparison falls through,
and the alpha ordinal in the next position decides. Verified against alpha.{9,21,30,35}: correct order. It
works by accident, so it is now documented in place, because a well-meaning strict-parsing "fix" would break
the ordering it currently gets right.
The test drives the shipped bundle through run-hook.mjs against a fixture
HOME, not the TS source. That is the point: the defect was a constant, and a source-level unit
test carrying its own fixture would have mirrored the wrong constant instead of catching it.
--- 1: prunes old versions under the installed channel (ork-alpha) ---
FAIL: 1.0.0 (oldest, stale) pruned expected present=no, got present=yes
FAIL: 1.0.1 (stale) pruned expected present=no, got present=yes
--- 2: a live holder survives regardless of age ---
FAIL: 1.0.1 (stale) pruned expected present=no, got present=yes
--- 3: works for the stable channel as well ---
FAIL: 8.0.0 pruned expected present=no, got present=yes
RESULT: FAIL exit 1
--- 1: prunes old versions under the installed channel (ork-alpha) --- PASS: 1.0.0 (oldest, stale) pruned PASS: 1.0.2 (recent) kept PASS: 1.0.1 (stale) pruned PASS: 1.0.3 (newest) kept --- 2: a live holder survives regardless of age --- PASS: 1.0.0 (oldest but LIVE) kept # the one with teeth PASS: 1.0.1 (stale) pruned PASS: 1.0.3 (newest) kept --- 3: works for the stable channel as well --- PASS: 8.0.0 pruned PASS: 8.0.3 kept RESULT: PASS exit 0
CH=$(make_fixture ...) ran the helper in a subshell, so the global
FIX stayed empty in the parent. HOME="" then resolves to /.claude/...,
the hook finds nothing, and the test fails in a way that looks exactly like the bug under test. Nothing was
lost (the real cache was verified intact at 15 versions afterwards), but a green run under that mistake would
have operated on the real $HOME. The test now sets globals directly and refuses to run if
the fixture is missing rather than falling back.