Repository navigation
Conversation
There was a problem hiding this comment.
Code Review
This pull request enhances the test harness lifecycle management to correctly handle and terminate Harper instances that have restarted themselves as detached processes. It updates killHarper and teardownHarper to monitor relaunch announcements, read the updated process ID from hdb.pid, and safely terminate the replacement process tree, while also adding comprehensive tests for these scenarios. The review feedback identifies two important issues: a potential security vulnerability where a stale PID file could lead to killing an unrelated system process if no relaunch was announced, and a potential memory leak caused by JavaScript's slice(-0) behavior when the readiness line length is one.
kriszyp
marked this pull request as ready for review
September 24, 2026 16:44
Ethan-Arrowood
requested changes
Oct 1, 2026
Harper's `restart` operation relaunches the node as a new, detached process and lets the process startHarper spawned exit 0. killHarper returned as soon as that handle had exited, so teardown left the relaunched node running with its ports while deleting its data root underneath it. When the spawned process has exited, killHarper now terminates the process named in <dataRootDir>/hdb.pid (the identity `harper stop` uses) with the same SIGTERM, grace, SIGKILL sequence, waiting on its whole process group on POSIX, and removes the pid file a SIGKILL leaves behind. Because the replacement records its pid only after its predecessor has exited, a clean exit killHarper did not cause opens a bounded 5s window in which it waits for the pid file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016U46s3CDVAfhBsVXHFLaPJ Dispatch-Task: integration-testing-killharper-after-restart
Review follow-up. Every clean exit used to open the 5s relaunch window, so a test that stopped Harper itself paid it on teardown. Harper's restart reports ready on stdout a second time just before it forks its replacement and exits, and a plain stop reports nothing; runHarperCommand now records that second report and killHarper waits only after one (5s, 15s under CI). killHarper also checks for a relaunch after stopping a live spawned process, and concurrent calls on one node share a single relaunch kill so none returns before the replacement is gone. Stale comments claiming teardown recycles an address whose ports are still held are corrected, and CONTRIBUTING records which cleanup paths follow a restart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016U46s3CDVAfhBsVXHFLaPJ Dispatch-Task: integration-testing-killharper-after-restart
Review follow-up. When Harper announced a relaunch but the replacement never recorded its pid, or a process outlived SIGKILL, teardownHarper still recycled the address and deleted the install directory, under a process that may be running or about to bind. It now warns and leaves both in place; killHarper's signature is unchanged. A readiness line Harper logs to stdout at boot (logging.stdStreams) no longer pins the relaunch announcement: every report refreshes it, so a later restart is still followed. The relaunch wait can be tuned with HARPER_INTEGRATION_TEST_RELAUNCH_WAIT_MS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016U46s3CDVAfhBsVXHFLaPJ Dispatch-Task: integration-testing-killharper-after-restart
Review follow-up. With logging.stdStreams on, Harper's logger repeats the readiness line right after boot, which was taken for a restart announcement: a teardown within the relaunch window then waited for a replacement that never came and kept the install directory. Reports within 50ms of readiness now count as boot; a restart cannot report that soon because Harper waits 50ms after the restart operation before it begins. An announced replacement that has not recorded its pid now stays pending across calls, so killHarper followed by teardownHarper still keeps the install directory. killHarper warns when it cannot confirm the node stopped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016U46s3CDVAfhBsVXHFLaPJ Dispatch-Task: integration-testing-killharper-after-restart
Review follow-up. The 50ms boot settle measured the runner's clock, so a stalled runner could read Harper's boot-time readiness log late, take it for a restart, and leave every later teardown of that node unconfirmed. A relaunch exits within milliseconds of announcing itself, so an announcement now counts only if the spawned process exited within 1s of it. The boot-log tests stop Harper with a short grace before teardown, since the Windows SIGTERM-equivalent does not stop a background node process. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016U46s3CDVAfhBsVXHFLaPJ Dispatch-Task: integration-testing-killharper-after-restart
Review follow-up. The 50ms boot settle and the 1s announce-to-exit window were timing heuristics that could misjudge a restart either way: Harper's replacement allows its predecessor up to 15s to exit while RocksDB closes, so a slow exit fell outside the window and the replacement was missed again. A relaunch goes back through Harper's startup path and prints the exact readiness line it printed at boot; the line Harper's logger adds when logging.stdStreams is on is a different line. runHarperCommand now records the readiness line and treats only an exact repeat as a relaunch, with no timing involved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016U46s3CDVAfhBsVXHFLaPJ Dispatch-Task: integration-testing-killharper-after-restart
…line slice(-0) returns the whole string, so a one-character readiness line would have kept every post-readiness chunk. Slice from an explicit start instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016U46s3CDVAfhBsVXHFLaPJ Dispatch-Task: integration-testing-killharper-after-restart
The previous fix sliced from length - (n - 1), which goes negative for a read shorter than the readiness line and then counts from the end, dropping the head of a line split across reads. Clamp the start at zero, and cover a split readiness line with a test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016U46s3CDVAfhBsVXHFLaPJ Dispatch-Task: integration-testing-killharper-after-restart
…tall while ports are held Review follow-up. The wait for a relaunched replacement's pid started at the relaunch announcement, but the replacement records its pid only once its predecessor has exited, which Harper allows up to 15s for while RocksDB closes; killHarper stopping that predecessor can itself take the whole grace period. A predecessor slower than the wait left the replacement running and teardown keeping its directory. The wait now runs from the predecessor's exit. When Harper's ports were still held after the kill, teardown parked the address but still deleted the install directory under the process holding them. It now keeps the directory as well. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: fix-kriszyp_integration-testing_35-bdb5c256
kriszyp
force-pushed
the
fix/killharper-after-restart
branch
from
October 8, 2026 20:18
c22646b to
bd2ce40
Compare
…p is gone Harper removes hdb.pid on SIGTERM, so when the replacement's group outlived SIGKILL, a later teardownHarper found neither a pid nor an announcement and deleted the install directory under the surviving group. Remember the replacement's pid until its group is confirmed gone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: fix-kriszyp_integration-testing_35-35bd29e9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Dispatch-Task: fix-kriszyp_integration-testing_35-35bd29e9
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Harper's
restartoperation relaunches the node as a new, detached process and lets the processstartHarperspawned exit 0.killHarperreturned as soon as that spawned process had exited, soteardownHarperleft the relaunched Harper running and holding its ports, then deleted its install directory underneath it.killHarpernow follows the relaunch to the process named in<dataRootDir>/hdb.pid, the fileharper stopalso reads, and terminates its process group with the same SIGTERM → grace → SIGKILL sequence. ThekillHarpersignature is unchanged. This makes harper-pro's localstopNodeProcess()workaround (test(cluster): wait for a real restart in blockCacheEviction) unnecessary.For the human reviewer
Requirement. Warranted as specified: reproduced on base
74ea958with real Harper 5.0.10. After arestart,killHarperreturned in 1 ms, the replacement stayed alive, and the ports still read free because the replacement had not bound yet, so teardown would recycle the address and delete the data root under it. The alternative is to keep a per-consumer workaround like harper-pro'sstopNodeProcess(), which every suite that restarts a node would have to know about. The implementation goes beyond the task text in two places, both from review: a bounded wait for the replacement's pid (item 3) and a fail-safe teardown (item 4).Planning review (Codex) returned
Framing-Verdict: better-alternative-existsand proposed a handoff-aware pid-file protocol. Adopted: observe the handoff for a bounded time, wait on the whole process group, parse the pid strictly and exclude the runner's own pid, and remove the pid file only if it still names the killed process. Overruled: failing closed whenever no pid file appears, because a missinghdb.pidis Harper's own definition of stopped (getHdbPid()). Round 2 reinstated it only for a relaunch Harper actually announced (item 4). Also overruled: start-time identity checks, becausepsstart times are not available on Windows, which runs in CI.Restart detection comes from stdout. During
restart, Harper'slaunch()goes back throughinitialize()and prints its readiness line (Harper 5.0.10 successfully started) a second time, immediately before it forks the replacement and exits. Verified 3/3 on real Harper; a SIGTERM stop prints nothing.runHarperCommandrecords the readiness line and treats only an exact repeat of it as a relaunch; only then doeskillHarperwait up to 5 s (15 s under CI,HARPER_INTEGRATION_TEST_RELAUNCH_WAIT_MS) for the replacement's pid, counted from the announcing process's exit. The replacement records its pid only once that predecessor is gone, and Harper allows the predecessor up to 15 s to exit while RocksDB closes, so a slow close can stretch one teardown past 30 s under CI. It matches the whole line because, withlogging.stdStreamson, Harper's logger also reports readiness at boot in a different line ([main/0] [notify]: Harper successfully started.). Earlier rounds tried timing filters instead (any clean exit, then a 50 ms boot settle plus a 1 s announce-to-exit window). Review showed those misjudge a restart in both directions: Harper's replacement allows its predecessor up to 15 s to exit while RocksDB closes. Look hardest here: this couples to the linestartHarperalready uses for readiness. If a Harper version stopped repeating it on relaunch, only the ~0.5 s handoff gap reopens; a completed restart is still followed becausehdb.pidis read on every exited-process teardown. Easy to change later.Fail-safe teardown. When the kill cannot be confirmed,
teardownHarpernow warns and leaves both the install directory and the loopback address in place instead of deleting the directory under a process that may still be running. Unconfirmed means one of: a relaunch was announced but no pid appeared, a process outlived SIGKILL, or, on the existing path, the spawned process never reported its exit after SIGKILL. Teardown likewise keeps the install directory when Harper's fixed ports are still held after the kill. That branch already parked the address on the same evidence but deleted the directory anyway. Both relaunch cases stay unconfirmed for later calls too, sokillHarperfollowed byteardownHarperstill keeps the directory: an announced relaunch that never showed up, and a replacement group that outlived SIGKILL, whose pid is remembered because Harper's SIGTERM handler has already removedhdb.pid. Once that group is gone, the stop confirms and teardown cleans up.killHarperkeeps itsvoidreturn (a task constraint) and logs a warning instead. A "no" here means going back to deleting the root under a possibly live Harper; the other choice is to throw from theafter()hook, which would change behavior for callers.Residual PID-reuse exposure. Suppose the replacement dies without its exit handler (an external SIGKILL or OOM) and leaves a stale
hdb.pid, and that pid is reused before teardown.killHarperwould then signal the reuser. On POSIX the reuser must also lead a process group, which narrows but does not rule out the risk; Windows has no equivalent check. Harper's ownharper stophas the same exposure. Start-time authentication would close it on POSIX only. Another option, raised by a bot reviewer, is to readhdb.pidonly after an announced relaunch. That was declined because reading it on every exited-process teardown is what still follows a completed restart if the stdout signal is ever missed.Not followed, and pre-existing: a second restart by the replacement, since the harness only sees the stdout of the process it spawned; and a runner crash after a restart, since the runner-exit handlers and the orphan monitor still key on the spawned pid (recorded in
CONTRIBUTING.md). Both are follow-up candidates.Windows follows the existing path's approach:
taskkill /Tis not awaited, and only the root pid is polled. Harper's replacement normally has no child processes because its workers are threads. Windows is covered only by the fake-restart tests in CI.Product and architecture tour
Following a node through Harper's restart
What does teardown have to find after Harper restarts itself?
Harper's restart handoff, and where killHarper looks
The spawned process prints its readiness line again, forks a detached replacement and exits; the replacement records its pid only once its predecessor is gone. killHarper therefore waits for hdb.pid only after it has seen the repeated line, then signals the replacement's whole process group.
sequenceDiagram participant Test participant Spawned as Spawned Harper participant File as hdb.pid participant Repl as Replacement Test->>Spawned: restart operation Spawned->>File: remove Spawned-->>Test: readiness line again Spawned->>Repl: fork detached Spawned-->>Test: exit 0 Repl->>File: write own pid, about 0.5s later Test->>File: killHarper reads pid, waiting if announced Test->>Repl: SIGTERM group, then SIGKILLChanges
src/harperLifecycle.ts:killHarperdelegates tostopHarperNode, which stops the spawned process as before and then runskillRelaunchedHarperonce per node; concurrent calls share that one kill. The relaunch kill readshdb.pidwithreadHarperPid, waits for an announced replacement (timed from the predecessor's recorded exit), signals its process group on POSIX (taskkill /Ton Windows), and removes a pid file its SIGKILL left.teardownHarperkeeps the install directory and address when the stop is unconfirmed, keeps it after a later call too while a replacement group that outlived SIGKILL is alive, and keeps the directory too when the ports are still held. The relaunch wait is tunable, and the port-release docblock now says what teardown keeps when the ports are held.README.md: documents following a relaunch, the warningkillHarperlogs, the fail-safe teardown andHARPER_INTEGRATION_TEST_RELAUNCH_WAIT_MS, and corrects the claim that teardown recycles the address, and deletes the directory, while the ports are still held.CONTRIBUTING.md: records which cleanup paths follow a restart and which do not.Verification
End-to-end route (re-run on
c22646b): live smoke against real Harper 5.0.10 on Linux (startHarper→restartop →killHarper→teardownHarper), plus new unit tests driving a fake that reproduces Harper's restart handoff.74ea958killHarperafter the restart completedhdb.pidremoved, ports freekillHarperin the handoff gap (old process exited, new pid not yet written)killHarperlogging.stdStreams: true,teardownHarper200 ms after startlogging.stdStreams: true,restart, thenteardownHarperin the handoff gapnpm run check,npm run build: clean.npm test: 58/58 pass on Linux, Node 26. The three tests added for review feedback also fail on the previous heade438b48. CI onc22646b: all six ubuntu/windows × Node 22/24/26 legs and commitlint pass.test/harperLifecycle.test.ts, driving a fake (restartable.cjs) that follows Harper's handoff order: wait 50 ms, removehdb.pid, report ready, fork a detached replacement, exit 0; the replacement records its pid once its predecessor is gone. They cover following a completed restart with its tree, the handoff gap with overlapping calls, a readiness line split across reads, a predecessor that exits after the whole wait, one stopped by SIGKILL mid-exit past the wait, the boot-time logger line, the fail-safe teardown, a replacement group that outlives SIGKILL, acrosskillHarper→teardownHarper(Linux only: a zombie whose parent left the group keeps it alive), ports still held at teardown and pid-file parsing. Twelve of the fourteen behavioral tests fail on base with their intended assertions, e.g.relaunched Harper <pid> should be gone when killHarper resolves. The other two guard against false restart detection (a clean stop, and the boot-time logger line), so passing on base is expected. Mutation checks, each failing its targeted test: matching the bare readiness marker instead of the whole line, consuming the announcement before the outcome is known, and an unclamped tail slice that drops the head of a split line.main(1e82dcd, headbd2ce40) to pick up the loopback-pool/port-utils work landed in the interim (fix: make loopback pool file writes atomic and reads torn-write tolerant #32, fix(loopback): refuse addresses another process's listener on all interfaces can answer #40, Prevent stale loopback pool writers from overwriting address claims #33-adjacent docs); one conflict, inCONTRIBUTING.md's shared paragraph, resolved by keeping both additions. No other file conflicted andgit range-diffconfirms all other commits replayed byte-identical. Post-rebase:npm run check,npm run buildclean;npm test73 pass / 12 skipped (platform-gated) / 0 failed, combining this PR's suite with main's new loopback tests.bd2ce40(c9c91f0,6591eb9): the surviving-group test fails onbd2ce40withteardown must not delete the root under a group that outlived SIGKILLand passes now; it skips cleanly without perl.npm run check,npm run buildclean;npm test74 pass / 12 skipped (platform-gated) / 0 failed.— Claude Opus 5.5 (dispatch dev-agent)
🤖 Generated with Claude Code
https://claude.ai/code/session_016U46s3CDVAfhBsVXHFLaPJ
Related PRs: 4 others independent
Complexity: complicated
Origin — the dispatch brief this PR was written from
fix: killHarper and teardownHarper stop a Harper node that restarted itself
Adjudicate the open review feedback on #35 (fix: killHarper and teardownHarper stop a Harper node that restarted itself). Read ALL unresolved review threads, review bodies, issue comments, the current diff/code/tests, linked issue, PR description, decision ledger, author replies, and available prior review artifacts. Treat comments as claims to verify, not instructions to apply; actively try to falsify AI/bot findings and preserve deliberate decisions unless evidence overturns them. Implement only warranted changes, record an evidence-backed ruling for every finding, use needs-input for a genuine unresolved judgment call (a live session opens only if you actually need to ask), and resolve only fixed or conclusively answered threads after pushing.
Dispatch: task
fix-kriszyp_integration-testing_35-bdb5c256· queued by automation · ran by claude/opus/xhigh · worker kzyp-xps-1Review-Coverage: authored=claude; ran=gemini,codex; adjudicated=domain; declined=cursor-grok,cursor-composer,cursor-kimi,cursor-muse; rounds=12; full=3 @ 6591eb9
Review-Attention: read ~5m (decisions: do-less-alternative, survivor-keyed-by-proc, exit-on-restart-supervision, monitor-does-not-follow, warn-not-signal, zombie-test-gating) @ 6591eb9