[BUG] ScheduleWakeup has no fallback when tengu_kairos_loop_dynamic is off — self-pacing unreachable for all non-first-party deployments, and non-/loop callers fail opaquely
Rewritten twice on 2026-08-12. The original report claimed a no-interval /loop dies after one tick (wrong — the harness degrades gracefully), and then attributed a regression to v2.1.202 (also wrong — the behavior is identical in every retrievable build). See the correction comments below; the surviving defect is described here.
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
- [x] Verified against v2.1.197, v2.1.201, v2.1.202, v2.1.220, v2.1.222, and current public v2.1.229 (binary inspection of the published
@anthropic-ai/claude-code-darwin-arm64artifacts); runtime environment is v2.1.222
What's Wrong?
ScheduleWakeup is gated on the feature flag tengu_kairos_loop_dynamic, which ships defaulting to false. The gate is checked before the caller's inputs are read, so well-formed calls are refused with:
Wakeup not scheduled. Either the /loop dynamic runtime gate is off or the loop reached its maximum duration — the loop has ended; do not re-issue.
The fallback for this lives in the prompt layer, not the tool. The /loop skill body is selected by the same flag:
if (wKe()) { // wKe() = Qe("tengu_kairos_loop_dynamic", !1)
if (!r) return izT(); // "/loop — schedule a recurring or self-paced prompt"
return szT(r); // ...full ScheduleWakeup protocol
}
if (!r) return ozT(); // "/loop — schedule a recurring prompt"
return BVS(r); // ...CronCreate only, defaults to 10m
So an interactive /loop behaves sensibly when the gate is off: the model is handed cron instructions and never attempts to self-pace. That degradation works, and does so identically in every build I checked, from v2.1.197 through v2.1.229.
The problem is everything that isn't the built-in /loop prompt. A skill, plugin, or agent that calls ScheduleWakeup directly — a perfectly reasonable thing to build, since the tool is exposed and documented — receives a hard rejection with no fallback path and no way to detect the condition in advance. In my case an autonomous skill self-paced via ScheduleWakeup per its own instructions; every call was refused and the loop died after one tick, while /loop itself would have been fine. The graceful path exists but only protects one caller.
Who this affects. Flag resolution is skipped entirely — every flag pinned to its compiled default — under any one of three independent conditions:
function Cle(){ return !te.DISABLE_GROWTHBOOK && aue() }
function aue(){ return !zJ() }
function zJ(){ return o5g() || Py() !== null || Txe() }
function o5g(){ if (tr(process.env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST)) return !1; return !mu() }
function mu(){ return Ln() === "firstParty" }
function Txe(){ return aKl() !== "default" } // CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC, DISABLE_TELEMETRY, DO_NOT_TRACK
- provider is not
firstParty— AWS Bedrock, Google Vertex, Microsoft Foundry, or any customANTHROPIC_BASE_URLgateway - gateway auth present
- non-essential traffic disabled
So self-pacing is permanently unavailable to every enterprise deployment not talking directly to api.anthropic.com, and to everyone who opts out of telemetry — independently of one another. A telemetry preference should not decide whether a tool works.
No opt-in exists. The CLAUDE_INTERNAL_FC_OVERRIDES parser sits behind an unconditional return in public builds (verified in 2.1.222 and 2.1.229, where it survived a refactor from standalone function to class method — presumably deliberate stripping). The CLAUDE_CODE_GB_DISK_CACHE_WHEN_TELEMETRY_OFF disk-cache path requires firstParty, excluding the affected population. Meanwhile both sibling loop flags accept an environment override:
function Jhd(){ if (Q.CLAUDE_CODE_LOOP_KEEPALIVE) return !0; return rt("tengu_kairos_loop_keepalive", !1) }
function eTo(){ if (Q.CLAUDE_CODE_LOOP_PERSISTENT) return !0; return Qe("tengu_kairos_loop_persistent", !1) }
function tot(){ return rt("tengu_kairos_loop_dynamic", !1) } // no override
tengu_kairos_loop_dynamic is the only one of the three with no escape hatch, and the keepalive path opens with if (!tot()) return rWt("gate_off"), null — so that override can't reach past the gate it would need to bypass.
What Should Happen?
- Give
ScheduleWakeupa tool-level fallback or an explicit failure mode. When the gate is off, either fall back to a cron-equivalent schedule, or return an error that says this deployment cannot self-pace so a caller can branch on it. Today the graceful path is reachable only by the built-in prompt. - Add an environment override, matching both sibling flags — and move the gate check in the keepalive path so the override can take effect.
- Decouple flag resolution from provider identity and telemetry preference. Whether a tool works shouldn't depend on the endpoint or on a data-collection setting.
- Split the error message. "Either the gate is off or the loop reached maximum duration" names two unrelated conditions with different remedies. A bare call outside a loop produces identical text, so the message can't discriminate — diagnosing this required cross-version binary inspection.
Error Messages/Logs
# ScheduleWakeup(delaySeconds: 1200, prompt: "...", reason: "...") from a skill's tick body, v2.1.220
Wakeup not scheduled. Either the /loop dynamic runtime gate is off or the loop
reached its maximum duration — the loop has ended; do not re-issue.
# Same environment: CronCreate schedules and fires normally (it reads no flag).
Steps to Reproduce
- Configure any non-first-party provider —
ANTHROPIC_BASE_URLpointing at a gateway, or Bedrock/Vertex/Foundry — or setCLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1on a first-party connection. - Invoke any skill or agent whose instructions call
ScheduleWakeupdirectly to pace itself. - Observed: every call refused with the message above; the loop ends after one tick with no fallback.
- Control: an interactive
/loop <prompt>in the same environment receives the cron instruction body and schedules a 10-minute recurring job — confirming the prompt-layer fallback works and the tool-layer gap is what bites.
Claude Model
Opus
Is this a regression?
No, this never worked. The flag, its resolution chain, the instruction-body selection, ScheduleWakeup and the rejection text are present and identical in v2.1.197, v2.1.201, v2.1.202, v2.1.220, v2.1.222 and v2.1.229. For non-first-party deployments, self-pacing has never been reachable.
Last Working Version
n/a
Claude Code Version
2.1.222 (Claude Code) — behavior verified identical in 2.1.220 and 2.1.229
Platform
Other — Anthropic API via an enterprise gateway (ANTHROPIC_BASE_URL override)
Operating System
macOS
Terminal/Shell
Terminal.app (macOS)
Additional Information
Relevant environment (proxy hosts and model aliases omitted):
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
ANTHROPIC_BASE_URL=<enterprise gateway>
This environment trips the gate twice over — gateway provider and traffic policy — so addressing only the telemetry coupling would not unblock it.
Related, distinct: #85838 (fixed-interval /loop doesn't self-schedule — a model-inference gap in the cron body, adjacent to the prompt-layer split described here), #74685 (ScheduleWakeup silent no-op in headless), #82633, #85321.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗