Deactivate one-shot Claude jobs after firing#119
Conversation
One-shot Claude jobs (schedule_type=once) were never deactivated in the database after firing. APScheduler removed them from the in-memory queue, but the DB row stayed active=1 until the next restart. The reminder path had the correct check; the Claude job path was missing it in two branches. Add the schedule_type=once deactivation check to: - CONDITION_NOT_MET path (auto-remove jobs whose condition wasn't met) - Non-conditional else path (plain one-shot Claude jobs) The CONDITION_MET path already deactivates unconditionally via auto_remove logic. Fixes #112
Review by KaiPR Review: #119 — Deactivate one-shot Claude jobs after firingOverall: Clean fix. One warning worth discussing, two minor suggestions. Warning
The new except Exception:
log.exception("Failed to send job %d result", job_id)
# new code — always runs, even after caught exception above
if data["schedule_type"] == "once":
await sessions.deactivate_job(job_id)If sending fails (network error, bot error, etc.), the exception is swallowed, and the job is silently deactivated anyway — the user never receives their one-shot response and gets no indication it was lost. Since APScheduler already removed the job from its queue, the job won't retry regardless, so this doesn't create a data-integrity bug. But it's a silent failure mode worth acknowledging. The CONDITION_NOT_MET path has the same structure and is consistent, so this appears to be a deliberate pattern — but it's worth confirming: is "fire-and-forget, deactivate even on delivery failure" the intended behavior for one-shot jobs? If yes, a brief comment would make the intent explicit. Suggestions
Both new calls to
self.ctx.job.schedule_removal.assert_not_called()The assertion is correct (the description explains APScheduler handles queue removal for What looks good
|
Add comments explaining that one-shot deactivation runs even after delivery failure (intentional - job can't retry regardless) and why schedule_removal is not called for run_once jobs.
Review by KaiThe prior review concerns have been addressed. Here's what changed: Resolved from prior review:
New findings: Suggestion — Otherwise the PR is clean. The fix is minimal and correct, the three branches are now consistent, the regression guard is solid, and the comments make the intentional silent-failure behavior explicit and readable. |
Summary
One-shot Claude jobs (
schedule_type == "once",job_type == "claude") were never deactivated in the database after firing. APScheduler'srun_onceremoved them from the in-memory scheduler queue, but the DB row stayedactive=1until the next restart when_register_new_jobscaught it as an "expired one-shot."The reminder path had the correct check since the beginning (
if data["schedule_type"] == "once": await sessions.deactivate_job(job_id)). The Claude job path was missing it in two of its three branches.What changed
Added the one-shot deactivation check to two branches in
_job_callback:CONDITION_NOT_MET path - A one-shot auto-remove job whose condition is not met on its single firing will never fire again. Now correctly deactivated.
Non-conditional else path - A plain one-shot Claude job (no auto_remove) fires, delivers its response, and now correctly deactivates. This was the main bug.
The CONDITION_MET path already deactivates unconditionally via auto_remove logic - no change needed.
What did NOT change
schedule_removal()calls added - APScheduler handles that forrun_oncejobsFixes #112
Test plan
test_one_shot_claude_deactivates_after_sending- one-shot Claude job deactivates after delivery, no schedule_removal calledtest_recurring_claude_does_not_deactivate- daily/interval jobs NOT deactivated (regression guard)test_one_shot_condition_not_met_deactivates- one-shot auto-remove job deactivates even when condition not met