Summary
Session-scoped scheduled wakeups — ScheduleWakeup, CronCreate, and the /loop dynamic-pacing mode — never fire when Claude runs through this ACP adapter, even though they work reliably in the interactive terminal CLI. The scheduled task is registered (the SDK reports it in session_crons), but the wakeup prompt never gets delivered: the session simply sits idle until the next client-driven session/prompt, at which point the schedule is effectively moot.
This makes any self-paced background cadence unusable from ACP clients (e.g. Zed's external-agent panel). /loop <task> with no interval, /loop 5m <task>, and direct ScheduleWakeup calls all silently no-op.
Environment
@agentclientprotocol/claude-agent-acp 0.55.0
@anthropic-ai/claude-agent-sdk 0.3.198 (pinned in package.json)
- Client: Zed external agents (ACP), but the cause is client-independent — any ACP client is affected.
Steps to reproduce
- Start a Claude session through the adapter in any ACP client.
- Send a prompt that schedules a near-term wakeup, e.g. ask Claude to call
ScheduleWakeup with a ~60s delay, or run /loop in dynamic mode.
- Let the turn finish and leave the session idle.
Expected: at the scheduled time, the session wakes itself and starts a new turn (the wakeup prompt is replayed), streaming as session/update notifications — the same self-initiated turn you get in the terminal CLI.
Actual: nothing fires. The session stays idle indefinitely. The wakeup only ever "arrives" incidentally, folded into whatever the user types next, because that session/prompt is what finally advances the loop.
Root cause (analysis)
The adapter's plumbing is actually ready to surface a self-initiated turn — the gap is that nothing ever advances the SDK's scheduler while the session is idle.
- The per-session consumer (
runConsumer in src/acp-agent.ts) is a long-lived while (true) loop that continuously awaits session.query.next() and forwards every message as ACP session/updates — its own comment says this is so "background/between-turn output streams live, not just while a prompt is awaiting." So if the SDK generator ever yielded wakeup-driven messages, the consumer would already relay them correctly.
- The SDK clearly models these tasks:
sdk.d.ts exposes session_crons?: SessionCronSummary[] described as "Session-scoped cron tasks (CronCreate, ScheduleWakeup, /loop) that will wake this session later", plus the cron schedule / one-shot / prompt-text fields and asyncRewake. So the task is registered in session state.
- What's missing is the tick: in streaming-input mode the query generator parks in
next() awaiting client input, and the scheduler is never advanced to the point of injecting the due wakeup. In the terminal REPL there's a loop that pumps the scheduler between iterations; the SDK/ACP path has no equivalent, so a fully idle session never fires its due crons. (CLAUDE_CODE_DISABLE_CRON gates the scheduler, confirming it's a harness-loop concern.)
Net: the tasks are recorded but never delivered under ACP because the query loop is only ever advanced by a client session/prompt.
Impact
/loop (dynamic, self-paced) — core use case, completely non-functional from ACP.
/loop <interval> and ScheduleWakeup / CronCreate — same.
- Silent failure: no error surfaces to the client, so it reads as "scheduled, will fire" when it never will — the worst failure mode for a timer.
Possible directions
- Adapter-side idle ticker: when a session has pending
session_crons, run a timer to the earliest due time and, at that point, advance the query loop so the SDK delivers the due wakeup (the existing consumer already forwards it). Open question: whether the loop can be pumped without submitting a synthetic user message, or whether the adapter must read session_crons and submit the cron's prompt text itself.
- Upstream (SDK): have
@anthropic-ai/claude-agent-sdk run the scheduler in streaming-input mode so due crons are yielded on the generator without an external pump. This is the cleaner fix if the SDK owns firing.
- At minimum, document the limitation so clients don't present
/loop / ScheduleWakeup as working.
Happy to prototype (1) if maintainers can confirm whether the SDK will auto-fire a due cron once the loop is advanced, or whether the adapter is expected to resubmit the prompt from session_crons.
Summary
Session-scoped scheduled wakeups —
ScheduleWakeup,CronCreate, and the/loopdynamic-pacing mode — never fire when Claude runs through this ACP adapter, even though they work reliably in the interactive terminal CLI. The scheduled task is registered (the SDK reports it insession_crons), but the wakeup prompt never gets delivered: the session simply sits idle until the next client-drivensession/prompt, at which point the schedule is effectively moot.This makes any self-paced background cadence unusable from ACP clients (e.g. Zed's external-agent panel).
/loop <task>with no interval,/loop 5m <task>, and directScheduleWakeupcalls all silently no-op.Environment
@agentclientprotocol/claude-agent-acp0.55.0@anthropic-ai/claude-agent-sdk0.3.198 (pinned inpackage.json)Steps to reproduce
ScheduleWakeupwith a ~60s delay, or run/loopin dynamic mode.Expected: at the scheduled time, the session wakes itself and starts a new turn (the wakeup prompt is replayed), streaming as
session/updatenotifications — the same self-initiated turn you get in the terminal CLI.Actual: nothing fires. The session stays idle indefinitely. The wakeup only ever "arrives" incidentally, folded into whatever the user types next, because that
session/promptis what finally advances the loop.Root cause (analysis)
The adapter's plumbing is actually ready to surface a self-initiated turn — the gap is that nothing ever advances the SDK's scheduler while the session is idle.
runConsumerinsrc/acp-agent.ts) is a long-livedwhile (true)loop that continuously awaitssession.query.next()and forwards every message as ACPsession/updates — its own comment says this is so "background/between-turn output streams live, not just while a prompt is awaiting." So if the SDK generator ever yielded wakeup-driven messages, the consumer would already relay them correctly.sdk.d.tsexposessession_crons?: SessionCronSummary[]described as "Session-scoped cron tasks (CronCreate, ScheduleWakeup, /loop) that will wake this session later", plus the cronschedule/ one-shot / prompt-text fields andasyncRewake. So the task is registered in session state.next()awaiting client input, and the scheduler is never advanced to the point of injecting the due wakeup. In the terminal REPL there's a loop that pumps the scheduler between iterations; the SDK/ACP path has no equivalent, so a fully idle session never fires its due crons. (CLAUDE_CODE_DISABLE_CRONgates the scheduler, confirming it's a harness-loop concern.)Net: the tasks are recorded but never delivered under ACP because the query loop is only ever advanced by a client
session/prompt.Impact
/loop(dynamic, self-paced) — core use case, completely non-functional from ACP./loop <interval>andScheduleWakeup/CronCreate— same.Possible directions
session_crons, run a timer to the earliest due time and, at that point, advance the query loop so the SDK delivers the due wakeup (the existing consumer already forwards it). Open question: whether the loop can be pumped without submitting a synthetic user message, or whether the adapter must readsession_cronsand submit the cron's prompt text itself.@anthropic-ai/claude-agent-sdkrun the scheduler in streaming-input mode so due crons are yielded on the generator without an external pump. This is the cleaner fix if the SDK owns firing./loop/ScheduleWakeupas working.Happy to prototype (1) if maintainers can confirm whether the SDK will auto-fire a due cron once the loop is advanced, or whether the adapter is expected to resubmit the prompt from
session_crons.