Repository navigation
fix(claude): count stale nodes off post-edit's critical path (#483) - #672
johnybradshaw wants to merge 2 commits into
Conversation
🌱 graft blast radius2 areas changed → 3 areas can be affected. 15 dependent symbols, depth 2. flowchart TB
A0(("Skill Lifecycle Hooks<br/>8 symbols"))
A1(("Lesson Gating<br/>6 symbols"))
A2(("Skill Session Tracing<br/>1 symbol"))
classDef reached fill:#D9EDF3,stroke:#3AA7C9,stroke-width:1.5px,color:#0E313C;
class A0,A1,A2 reached;
Who knows this code — 2 people across 5 areas
Ownership is git history over each area's own files, weighted towards recent work (120-day half-life). Merge commits and bots are dropped, and you are dropped from your own PR. A name with no All 15 dependent symbols, grouped by areaSkill Lifecycle Hooks — 8 symbols in 8 files
Lesson Gating — 6 symbols in 6 files
Skill Session Tracing — 1 symbol in 1 file
Test signal per changed area — 1 ✓ · 1 ✗Reached = a node under a test path has a resolved edge into the changed symbol. It undercounts anything called indirectly — through a CLI, a spawned process or a dynamic import — so read a low ratio as “look here”, never as a coverage gate.
52 test suites also reference this code52 symbols, kept out of the diagram and the table so they cannot crowd out the areas a reviewer has to look at.
Open the interactive graph → — click an area to see its dependent symbols at file:line. |
`graft check` exited with process.exit(1) straight after console.log. On a pipe, Node writes stdout asynchronously, so exit() dropped whatever was still queued: a reader got the first 64 KiB (146 KiB through spawnSync's socket) and could not parse it. pendingIds lists every unsummarised node, so a repo of a few thousand symbols built without --deep is past that. post-edit's stale count is one such reader, and wrote 0 whenever its parse failed. Set process.exitCode instead, as other commands here already do. One side effect: the postAction hook now also runs for a failing check, so the `query` telemetry event counts stale checks as well as clean ones. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#483) post-edit ran `graft check` synchronously under an 8s child timeout to fill staleCount for the statusline. On a ~900-file repo the check takes 6.5-9.5s, so every Write/Edit blocked for about 8s, and a timeout was written as staleCount: 0. post-edit now marks the graph dirty and spawns stale-count-run.js detached, the way Stop spawns sync-run.js. The runner checks under a 30s budget and writes the count back, with these guards: - it holds its own lock, so one check runs at a time and the sync lock is never held; - it keeps the last count when the check times out or prints nothing parseable; - it skips the write if the graph is clean, a sync is running, or a sync finished after the check started. Measured on a 553-file repo with the built dist: the hook takes 220-250ms, and the count lands about 10s later, matching `graft check`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
29b1c87 to
bd77195
Compare
Summary
On mid-size repos, the post-edit hook blocks every
Write/Editfor about 8 seconds and then reports0 stale(#483). This PR moves the stale count off the edit path. It also fixes a second bug:graft check --jsontruncates its own output on a pipe.1.
fix(claude): count stale nodes in a detached child (#483)handlePostEditrangraft checksynchronously under the 8sCHILD_TIMEOUT_MS. On a ~900-file repo the check alone takes 6.5–9.5s, CPU-bound. Across 127 recorded post-edit runs, each took 6.3–8.4s, and a timeout was written asstaleCount: 0.Now
handlePostEditsetsdirtyandlastFileand spawnsdist/claude/stale-count-run.jsdetached, the wayhandleStopspawnssync-run.js. It uses aGRAFT_TEST_STALE_COUNT_RUNseam, likeGRAFT_TEST_SYNC_RUN. The runner works as follows:.cache/stale-check.lock, so one check runs at a time and the sync lock is never held. A lock older than the budget plus 10s is reclaimed, and the runner only removes a lock whose pid is its own.syncedAtis later than the check's start.2.
fix(cli): letcheck --jsonfinish writing before a stale exitI found this while verifying the change end to end. Even when the check finished inside the budget, the count stayed at 0.
graft checkcalledprocess.exit(1)straight afterconsole.log. On POSIX, stdout to a pipe is asynchronous, so whatever was still queued was dropped:| wc -cexecFileSync/spawnSyncgraph.pendingIdslists every unsummarised node, so any repo of a few thousand symbols built without--deepproduces output past that size. post-edit's parse then failed and wrote 0. Anyone pipinggraft check --jsonintojqhits the same thing.The fix is
process.exitCode = 1, which other commands incli.tsalready use. One side effect to be aware of:postActionnow runs for a failing check, so thequerytelemetry event counts stale checks as well as clean ones. Before this, only passing checks were counted.Relation to #485 and #450
checkTimedOut). On a repo whose check takes around 8s, that still blocks the first edit after every sync, and the count is still subject to the truncation above. This PR replaces that part of fix: hook timeout units are seconds not ms (#283, #480); post-edit check budget and retry-storm (#366, #483) #485; the timeout-units fixes there (settings-merge: hook timeouts written in ms (Claude Code reads seconds) and footerLinksRegexes written to project settings where it is ignored #283/Claude Code hook timeouts written in milliseconds; Claude Code reads seconds #480) are independent. Both PRs touchhandlePostEdit, so whichever lands second needs a small rebase. I'm happy to do it.Test plan
claude-hookstests. post-edit spawns the runner with the repo dir and returns without waiting on it, leavingstaleCountfor the child.runStaleCountcovers: recovering JSON from a non-zero exit; releasing its lock; keeping the last count on null; each of the three sync guards, plus a sync from before the check not blocking; the live lock; and reclaiming a stale lock. The existing GRAFT_DIR--dirtest moved to the runner.--dir, or the spawn, or making the spawn synchronous, fails at least one test.cli-check-pipetest. A 12,000-symbol repo is made stale,graft check --jsonis read throughspawnSync, and the test asserts exit status 1 and that the JSON parses. Without the fix it fails withSyntaxError: Unterminated string in JSON at position 146176. A size assertion makes the fixture fail loudly if it ever shrinks below what a pipe can hide.mainin the same environment (unvendored grammars, symlink and git-tracking cases), apart from wall-clock-bound tests that failed under full-suite load and pass alone, before and after:trace-shell,skills-answers-prompt,upkeep-hooks.dist, on a copy of a real 553-file repo after an edit: the hook took 220–250ms, against 8.27s for unpatched 0.23.1 on a similar repo. The background check finished in about 11s, released its lock, and wrotestaleCount: 1, matchinggraft check.Fixes #483
🤖 Generated with Claude Code