You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[P2] Warn or notify when a scheduled report is skipped because another report is running #34
While any run is generating, a due occurrence is permanently marked skipped ("Skipped overlap…") — never deferred, even inside the 2h grace window. notifyFailure covers only failed/uncertain/ready, so the owner gets no message, and the confirm view doesn't warn about collisions. README's "One report executes at a time" reads as serialized, not dropped. (The skip itself is intentional per test/schedules.test.ts:337.)
Failure scenario
Two daily jobs at 08:00 ("News", "PR digest"): every day one runs and the other is silently lost; the only trace is "⚠ skipped" in \schedule.
Evidence
Local probe cc_mnt/review-20261011/commands/same-time.test.ts → ['News:running:', 'PR digest:skipped:Skipped overlap...'].
Suggested fix
Prefer deferring (leave the occurrence unclaimed) while within grace so both run in sequence. Minimum: DM on overlap skips and warn in scheduleConfirmView when another enabled job's occurrence falls within this job's timeout window; fix the README wording.
Acceptance
A same-minute pair either runs both in sequence, or the owner is warned at creation and DMed on the skip.
Found in the 2026-10-11 codebase review of commit 22ce7ee (typecheck clean, 1,142/1,142 tests passing). Probe paths refer to the maintainer's local, gitignored review ledger.
Approach (recommend: defer): When another report is generating, leave the due occurrence unclaimed. Write no skip record and don't advance nextRunAt. Each 5s poll re-evaluates it, and the first poll after the active report finishes claims it, still inside the existing 2-hour grace window. This keeps "one report executes at a time" as a real serialization guarantee instead of a silent drop. Both 08:00 reports arrive, the second a few minutes late. I recommend defer over notify-on-skip because notifying would only tell the owner, every day, that a report they configured was thrown away. Defer removes the loss itself, and the change is one line. Report timeouts are capped at 60 minutes and uncertain runs stop blocking at their deadline, so a deferred occurrence normally starts well within grace. Only chains of three or more long same-time reports can run past the window, and they still get the existing "outside the 2-hour grace window" skip.
Changes:
src/schedules/scheduler.ts, pollOnce() job loop:
if(!existing){if(now-latest>SCHEDULE_GRACE_MS)store.skipOccurrence(job.id,latest,"Missed occurrence outside the 2-hour grace window.");// Defer: no record and the pointer stays put, so a later poll claims this occurrence// once the active report ends (within grace). One report still generates at a time.elseif(store.runs().some(r=>isGenerationActive(r,now)))continue;elsestore.claim(job.id,latest);}
continue skips the nextRunAt advance and step. The earlier skipOccurrence(job.id, job.nextRunAt, …) for older missed occurrences is idempotent (createRun returns the existing record without committing), so it can run on every deferred poll. Claim order among several deferred jobs follows store.jobs() (creation order).
README.md:172: "One report executes at a time." becomes:
One report generates at a time; a report that comes due while another is generating starts when that one finishes (within the same two-hour window).
src/schedules/controls.tsscheduleConfirmView, last section: append " Reports due at the same time run one after another."
Tests: I prototyped this in cc_mnt/review-20261011/fixes/proto-34/: a copy of scheduler.ts plus defer.test.ts, which passes.
test/schedules.test.ts, replace "allows only one global generation, not one per job": two jobs at the same time. After the first poll only the first is running and the second has no run record. After the first is marked delivered and the clock moves 20 minutes, a poll claims the second with scheduledAt equal to the original occurrence. Both jobs' nextRunAt then advance to the next day.
Update it.each(["running","waiting","uncertain"])("skips overlapping automatic work while a manual run is …") to assert deferral instead: store.run(automaticRunId(job.id, now)) is undefined and nextRunAt is unchanged. A further case: once now - latest > SCHEDULE_GRACE_MS while still blocked, the occurrence is skipped with the grace error.
Those 4 cases are the only existing scheduler/runner tests that change. The other 115 pass against the prototype.
Risks / interactions:
\schedule "next …" keeps showing the due time until the deferred occurrence is claimed. That is accurate, since it is still pending.
\schedule run (manual) still refuses while anything is active. Unchanged.
A \schedule edit/pause during deferral recomputes nextRunAt and drops the pending occurrence. That is consistent with "changes apply to future occurrences".
The deferred run's startedAt/deadlineAt start when it is claimed, so the full timeout is available.
Restarts are safe: nothing is persisted for a deferred occurrence, and recovery follows the existing claim rules.
Alternatives considered:
Keep skipping and DM on overlap skips: the owner learns of the loss but still loses the report every day.
Allow concurrent generation: breaks the single-generation design (approval routing and shared uncertainty guard).
Effort: S. Confidence: high
Proposal only, not implemented. Sketches are against 22ce7ee.
Activity
MatthewS65537 commentedon Oct 11, 2026
Proposed fix
Approach (recommend: defer): When another report is generating, leave the due occurrence unclaimed. Write no skip record and don't advance
nextRunAt. Each 5s poll re-evaluates it, and the first poll after the active report finishes claims it, still inside the existing 2-hour grace window. This keeps "one report executes at a time" as a real serialization guarantee instead of a silent drop. Both 08:00 reports arrive, the second a few minutes late. I recommend defer over notify-on-skip because notifying would only tell the owner, every day, that a report they configured was thrown away. Defer removes the loss itself, and the change is one line. Report timeouts are capped at 60 minutes and uncertain runs stop blocking at their deadline, so a deferred occurrence normally starts well within grace. Only chains of three or more long same-time reports can run past the window, and they still get the existing "outside the 2-hour grace window" skip.Changes:
src/schedules/scheduler.ts,pollOnce()job loop:continueskips thenextRunAtadvance andstep. The earlierskipOccurrence(job.id, job.nextRunAt, …)for older missed occurrences is idempotent (createRunreturns the existing record without committing), so it can run on every deferred poll. Claim order among several deferred jobs followsstore.jobs()(creation order).README.md:172: "One report executes at a time." becomes:src/schedules/controls.tsscheduleConfirmView, last section: append " Reports due at the same time run one after another."Tests: I prototyped this in
cc_mnt/review-20261011/fixes/proto-34/: a copy ofscheduler.tsplusdefer.test.ts, which passes.test/schedules.test.ts, replace "allows only one global generation, not one per job": two jobs at the same time. After the first poll only the first isrunningand the second has no run record. After the first is markeddeliveredand the clock moves 20 minutes, a poll claims the second withscheduledAtequal to the original occurrence. Both jobs'nextRunAtthen advance to the next day.it.each(["running","waiting","uncertain"])("skips overlapping automatic work while a manual run is …")to assert deferral instead:store.run(automaticRunId(job.id, now))is undefined andnextRunAtis unchanged. A further case: oncenow - latest > SCHEDULE_GRACE_MSwhile still blocked, the occurrence is skipped with the grace error.Risks / interactions:
\schedule"next …" keeps showing the due time until the deferred occurrence is claimed. That is accurate, since it is still pending.\schedule run(manual) still refuses while anything is active. Unchanged.\schedule edit/pause during deferral recomputesnextRunAtand drops the pending occurrence. That is consistent with "changes apply to future occurrences".startedAt/deadlineAtstart when it is claimed, so the full timeout is available.Alternatives considered:
Effort: S. Confidence: high
Proposal only, not implemented. Sketches are against
22ce7ee.