[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

Status Open
Reported on v2.1.222
Maintainer reply None cached
Activity 2 comments · opened Aug 13, 2026
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-arm64 artifacts); 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 firstPartyAWS Bedrock, Google Vertex, Microsoft Foundry, or any custom ANTHROPIC_BASE_URL gateway
  • 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?

  1. Give ScheduleWakeup a 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.
  2. Add an environment override, matching both sibling flags — and move the gate check in the keepalive path so the override can take effect.
  3. 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.
  4. 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

  1. Configure any non-first-party provider — ANTHROPIC_BASE_URL pointing at a gateway, or Bedrock/Vertex/Foundry — or set CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 on a first-party connection.
  2. Invoke any skill or agent whose instructions call ScheduleWakeup directly to pace itself.
  3. Observed: every call refused with the message above; the loop ends after one tick with no fallback.
  4. 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.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗