feat(cli): cancel the followed job on Ctrl-C; add --detach#171
Merged
Conversation
#169 shipped cancel-by-id but Ctrl-C during a follow still only detached the log stream while the server job ran on. That is the surprising behaviour: for a mistyped club/alias you want out fast, and Ctrl-C == stop is the universal CLI expectation. So a follow interrupt now cancels the server job by default. No prompt -- the keystroke is the intent, and prompting would mean reading stdin inside an interrupt finalizer as the process tears down (fragile). It is also cheap to be wrong: CCAS jobs are idempotent/resumable, so an accidental cancel just stops early and the re-run resumes; a cancelled recruit already leaves candidates Deferred, so cancel never burns invites. The `.onInterrupt` is attached OUTSIDE `.timeout`, so a genuine `maxWait` expiry (which returns None on the success path) still detaches rather than cancelling -- only a real fiber interruption fires the cancel POST, which is `.ignore`d so a 404 (job already terminal) or a teardown transport error can't turn a clean Ctrl-C into a stack trace. The notice routes through the follow's own sink -- render-lock-safe `renderer.logLine` in bars mode, stderr otherwise -- so it can't tear a live progress bar (the finalizer runs before withBars' `renderer.clear`). For "keep the data fresh, don't watch" (notably history, whose output is not user-facing -- StatsApp consumes it), add `--detach` on membership/history/ stats: submit, print the job id, return; reattach with `ccas logs <id>`, which replays the full log from offset 0 so nothing is lost. Not offered on recruit (its invite delivery/confirmation needs the follow) or logs (which IS the follow). Making Ctrl-C-cancel easy exposes a rough edge: a resubmit of the same (kind, club) that races the in-flight cancel would hit the unique-running index and read the baffling "already running" for a job the operator just killed -- the fiber flips to Cancelled only after its current blocking statement finishes. `submit` now checks `cancelRequested` and, for a blocker that is being cancelled, returns "finishing cancellation -- retry in a moment" so the collision reads as self-resolving. Tested: interrupt-during-follow fires the cancel POST and interrupt-during the post-follow recruit confirm does not (stub follower); `--detach` parses and defaults false on the three commands; a resubmit racing an in-flight cancel gets the retry wording (real JobRunner.live); and an end-to-end wire test drives the real JobFollower against a real HTTP server, interrupts it once the follow fiber is Suspended, and asserts the job reaches Cancelled -- proving the finalizer's POST lands over real HTTP during teardown. Completions regenerated (TestCcasCompletion drift guard). Fixes #170 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
What
Completes the IntelliJ-graceful-stop analog from #168. Two follow-related changes plus a server-side polish:
POST /api/jobs/{id}/cancelinstead of only detaching the log stream. No prompt — the keystroke is the intent, and prompting would mean reading stdin inside an interrupt finalizer during teardown (fragile). Safe to default: CCAS jobs are idempotent/resumable so an accidental cancel just re-runs, and a cancelled recruit already leaves candidatesDeferred(never burns invites). The.onInterruptsits outside.timeout, so a realmaxWaitexpiry still detaches — only a genuine interrupt cancels. The POST is.ignored (a 404 for an already-terminal job, or a teardown transport error, must not crash a clean Ctrl-C). The notice routes through the follow's own sink — render-lock-saferenderer.logLinein bars mode, stderr otherwise — so it can't tear a live progress bar.--detachfire-and-forget onmembership/history/stats: submit, print the job id, return; reattach withccas logs <id>(replays the full log from offset 0, so nothing is lost). Aimed at "keep the data fresh, don't watch" — notablyhistory, whose output isn't user-facing (StatsApp consumes it). Not offered onrecruit(invite delivery/confirmation needs the follow) orlogs(which is the follow).(kind, club)right after a cancel would read the baffling "already running" while the just-killed job's fiber is still finishing its in-flight blocking statement.submitnow checkscancelRequestedand returnsfinishing cancellation — retry in a momentfor a blocker that's being cancelled, so the collision reads as self-resolving.Fixes
Fixes #170.
Testing
TestJobFollower— interrupt-during-follow fires the cancel POST; interrupt during the post-follow recruit confirm does not (stub recording client).TestCliParser—--detachparses and defaults false on membership/history/stats.TestJobRunner— a resubmit racing an in-flight cancel gets the retry wording (realJobRunner.live).TestJobCancelWire— realJobFollowervs a real HTTP server + real client: interrupt once the follow fiber isSuspended(deterministic readiness, no sleep) and assert the job reachesCancelled— proves the finalizer's POST lands over real HTTP during teardown.completions/ccas.bashregenerated;TestCcasCompletiondrift guard passes.sbt test: 1065 passed, 0 failed.🤖 Generated with Claude Code