Skip to content

feat(cli): cancel the followed job on Ctrl-C; add --detach#171

Merged
Sootopolis merged 1 commit into
mainfrom
wip
Jul 19, 2026
Merged

feat(cli): cancel the followed job on Ctrl-C; add --detach#171
Sootopolis merged 1 commit into
mainfrom
wip

Conversation

@Sootopolis

Copy link
Copy Markdown
Owner

What

Completes the IntelliJ-graceful-stop analog from #168. Two follow-related changes plus a server-side polish:

  • Ctrl-C during a follow now cancels the server job (default). A follow interrupt fires POST /api/jobs/{id}/cancel instead 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 candidates Deferred (never burns invites). The .onInterrupt sits outside .timeout, so a real maxWait expiry 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-safe renderer.logLine in bars mode, stderr otherwise — so it can't tear a live progress bar.
  • --detach fire-and-forget on membership / history / stats: submit, print the job id, return; reattach with ccas logs <id> (replays the full log from offset 0, so nothing is lost). Aimed at "keep the data fresh, don't watch" — notably history, whose output isn't user-facing (StatsApp consumes it). Not offered on recruit (invite delivery/confirmation needs the follow) or logs (which is the follow).
  • Cancellation-aware conflict message. Making Ctrl-C-cancel easy exposes a race: a resubmit of the same (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. submit now checks cancelRequested and returns finishing cancellation — retry in a moment for 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--detach parses and defaults false on membership/history/stats.
  • TestJobRunner — a resubmit racing an in-flight cancel gets the retry wording (real JobRunner.live).
  • TestJobCancelWire — real JobFollower vs a real HTTP server + real client: interrupt once the follow fiber is Suspended (deterministic readiness, no sleep) and assert the job reaches Cancelled — proves the finalizer's POST lands over real HTTP during teardown.
  • completions/ccas.bash regenerated; TestCcasCompletion drift guard passes.
  • Full sbt test: 1065 passed, 0 failed.

🤖 Generated with Claude Code

#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>
@Sootopolis
Sootopolis merged commit 0ebfd24 into main Jul 19, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CLI: Ctrl-C during a job follow should optionally cancel the server job (follow-up to #168)

1 participant