ScheduleWakeup reports a scheduled time outside /loop, then never fires
Bug: ScheduleWakeup reports success outside /loop, then never fires
Component: Claude Code — ScheduleWakeup tool
Severity: Medium–High for unattended/long-running work
Reproducibility: Consistent (observed repeatedly)
Summary
ScheduleWakeup accepts a call made outside an active /loop session, returns a success message
naming a concrete wake time, and then never fires. The success string also tells the model to stop
working for the turn, so the model ends its turn waiting on a wakeup that structurally cannot occur.
Repro
- Start a normal Claude Code session (no
/loop). - Have the model call
ScheduleWakeupwith a plainprompt(not a/loopinput, not the
<<autonomous-loop-dynamic>> sentinel), e.g. delaySeconds: 1200.
- Observe the tool result.
- Wait past the stated time.
Expected
One of:
- the call is refused with a clear reason ("no active
/loop;ScheduleWakeuponly resumes a
dynamic loop"), or
- it works as a general deferred re-invocation.
Either is fine. What matters is that the return value reflects reality.
Actual
The tool returns, verbatim:
Next wakeup scheduled for 10:31:00 (in 1221s). Nothing more to do this turn — the harness
re-invokes you when the wakeup fires or a task-notification arrives.
No wakeup occurs. The session sits idle until the user speaks. Confirmed by the transcript: there is no
model turn between the ScheduleWakeup call and the user's next message, so no re-invocation happened.
Why this is worse than a plain no-op
The failure is actively misleading in three ways:
- It states a specific time.
10:31:00reads as a scheduling confirmation, not a best effort. - It instructs the model to stand down — "Nothing more to do this turn". A correct-behaving model
should stop after a successful schedule. So the message converts a silent no-op into a deliberate,
justified stall.
- There is no observable signal of failure. Nothing is logged, no later error surfaces. The only way
to detect it is to notice that time passed and nothing happened — which requires a human.
Impact
In an unattended or long-running session this is a silent indefinite stall. Concrete instance:
- A background workflow was running and produced no journal file, so a completion notification was the
only passive signal — and that notification had already failed to arrive once (a prior run of the same
workflow hung for ~8 hours without notifying).
- Precisely because passive signals had proven unreliable, the model scheduled an active check.
- The wakeup never fired, so the workflow went unmonitored for 23 minutes until the user intervened.
The pattern is self-defeating: the tool is most attractive exactly when passive notification is untrusted,
which is exactly when its silent failure is most costly.
Suggested fixes (in preference order)
- Refuse the call outside an active dynamic
/loop, with a reason. Cheapest, and it makes the
documented constraint enforceable rather than advisory.
- If refusal is undesirable, change the return string so it cannot be read as a confirmation —
e.g. No active /loop; nothing scheduled. Do not end your turn expecting a wakeup. The current
wording is the harmful part.
- Longer term, consider a general deferred re-invocation primitive. The underlying need — "check
back on background work in N minutes" — is real and currently has no reliable mechanism when a task
emits no notification.
Notes
- The tool's own description does scope it to
/loopdynamic mode, so a model calling it outside that
context is misusing it. The bug is not that it declines to work — it is that it claims to have
worked. A tool documented as loop-only should not return a scheduled timestamp when no loop exists.
- Related gap that made this matter: a background
Workflowthat hangs produces no completion
notification and no result file, and is therefore indistinguishable from one still running. The
only reliable liveness signal found was working-tree file mtimes.
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Still relevant.
ScheduleWakeupstill accepts a call made outside an active/loop, returns a success string naming a concrete wake time, and then never fires.The compounding harm is in that success string: it tells the model to stop working for the turn. So the failure is not a missed wakeup on its own. The model ends its turn waiting for an event that will never arrive, and unattended work stops there with everything reading as successful.
Either the call should be refused outside
/loop, or the wakeup should fire. Reporting success for neither is the worst of the three.Still open. The defect here is the return value, not the scheduling.
ScheduleWakeupaccepts a call made outside an active/loop, returns a string naming a concrete wake time, and no wakeup occurs.The reason this costs real time is the second half of that string. It also tells the model there is nothing more to do for the turn and that the harness will re-invoke it. So the model ends its turn correctly, by its own instructions, waiting on an event that cannot happen. The session then sits idle until a human speaks, and the transcript shows no model turn in between, so nothing looks broken.
Either outcome would be fine. Refuse the call with a reason, or make it a general deferred re-invocation. What I am asking for is that the return value describe what actually happened.
Still open. Three other reports now describe
ScheduleWakeupoutside/loopbehaving three different ways. #87146 reports it never firing in Remote Control child sessions. #88205 reports one malformed call outside/loopbeing refused with "prompt is required when stop is not true". #86245 was closed as not planned.#88205 is the telling one: a refusal path for calls outside
/loopalready exists. It just does not apply to a well-formed call, which is accepted, reported as scheduled, and dropped. Using that same refusal for every call outside/loopwould fix the part of this report that costs time.