ScheduleWakeup reports a scheduled time outside /loop, then never fires

Status Open
Maintainer reply None cached
Activity 4 comments · opened Jul 30, 2026

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

  1. Start a normal Claude Code session (no /loop).
  2. Have the model call ScheduleWakeup with a plain prompt (not a /loop input, not the

<<autonomous-loop-dynamic>> sentinel), e.g. delaySeconds: 1200.

  1. Observe the tool result.
  2. Wait past the stated time.

Expected

One of:

  • the call is refused with a clear reason ("no active /loop; ScheduleWakeup only 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:

  1. It states a specific time. 10:31:00 reads as a scheduling confirmation, not a best effort.
  2. 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.

  1. 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)

  1. Refuse the call outside an active dynamic /loop, with a reason. Cheapest, and it makes the

documented constraint enforceable rather than advisory.

  1. 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.

  1. 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 /loop dynamic 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 Workflow that 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.

View original on GitHub ↗

3 Comments

wshallwshall · 1 month ago

Still relevant. ScheduleWakeup still 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.

wshallwshall · 1 month ago

Still open. The defect here is the return value, not the scheduling. ScheduleWakeup accepts 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.

wshallwshall · 22 days ago

Still open. Three other reports now describe ScheduleWakeup outside /loop behaving three different ways. #87146 reports it never firing in Remote Control child sessions. #88205 reports one malformed call outside /loop being 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 /loop already 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 /loop would fix the part of this report that costs time.

Showing cached comments. Read the full discussion on GitHub ↗