Monitor with persistent: true is capped at 30 minutes since 2.1.26x ("expires in 30m unless the source ends first")

Status Open
Reported on v2.1.268
Maintainer reply None cached
Activity 8 comments · opened Sep 15, 2026

Summary

Since around 2.1.268 (still on 2.1.272), a Monitor armed with persistent: true is capped at 30 minutes. The tool result reads:

Monitor started (task …, expires in 30m unless the source ends first; you get one notice at expiry — re-arm if you still need the watch).

and 30 minutes later the watch is killed with [Monitor expired after 30m …]. The documented behaviour, and what the same session saw earlier in September, is that a persistent Monitor runs until the session ends or TaskStop is called, with timeout_ms ignored. Nothing in the changelog for 2.1.257 to 2.1.272 mentions a 30-minute limit; 2.1.257 actually removed a one-hour limit on subagents' background commands.

Reproduce

  1. Interactive session, Linux (Arch), Claude Code 2.1.272 (also 2.1.268).
  2. Have the model arm any long-running, stdout-emitting command, e.g. a mailbox poller:

Monitor({command: "python3 watch.py --interval 180", description: "new mail", persistent: true, timeout_ms: 3600000})

  1. The tool result says "expires in 30m"; after 30 minutes the expiry notice arrives and the watch is gone.

Expected

A persistent watch runs for the session; no 30-minute expiry. timeout_ms: 3600000 should at least be honoured.

Why this matters

A watch for an external event (mail, a webhook, a queue) has no natural end. With the cap, the model must re-arm every 30 minutes: each re-arm is a model turn and a tool call, and events that land between expiry and re-arm are missed. Over a working day that is dozens of pointless turns and real gaps in coverage. It also makes the persistent flag misleading, since the tool accepts it and then ignores it.

Checks done

  • ~/.claude/settings.json, settings.local.json and policy-limits.json contain nothing monitor-related.
  • Reproduced across a machine restart and a Claude Code update (2.1.268 → 2.1.272).
  • Not the low-memory guard: that stops background commands with a different message ("running low on memory"), and Monitors were killed at exactly 30 minutes with the box idle.

Ask

Restore the documented persistent behaviour, or, if a cap is intended, make it configurable (a setting or a documented environment variable) and state it in the tool description and the changelog.

View original on GitHub ↗

3 Comments

ruihe774 · 1 month ago

+1, this is confirmed intentional, not a bug — the 2.1.271 changelog says:

Changed Monitor watches to always have a deadline (at most 30 minutes; 10 in single-prompt -p runs) and notify Claude to re-arm, replacing the no-timeout persistent option

So persistent is gone as documented behavior, not silently broken. Still, this disrupts long-running workloads (e.g. polling an external queue/mailbox for hours) — the model now has to babysit a re-arm every 30 minutes, and anything that arrives in the gap between expiry and re-arm is lost. Would like to see this configurable (env var or setting) rather than a hardcoded ceiling, per the original ask.

freiit · 29 days ago

might have been intentional, but not a good idea anyways. please revert!

kepler-somnium · 27 days ago

Adding measurements from a multi-seat deployment (six Claude Code seats on Linux and macOS, 2.1.267 through 2.1.276), plus the concrete ask, since this is the open issue closest to it.

Measured, 2026-09-18:

  • On 2.1.273 and 2.1.276 the Monitor tool schema has no persistent property at all (additionalProperties: false; command, description, timeout_ms required, ws). Passing persistent: true anyway is accepted without error and ignored: a never-exiting command (while true; do sleep 10; done) armed with timeout_ms: 60000, persistent: true expired at exactly 1 minute. On 2.1.267 the schema still lists persistent (bool, default false) and the arm line reads persistent — runs until TaskStop or session end; a monitor armed that way has run 6+ hours on its original task id. So the flag was removed between 2.1.267 and 2.1.273.
  • The timeout_ms schema on 2.1.27x disagrees with itself: "maximum": 3600000 while the same field's description says "Deadlines above 1800000ms are capped to 1800000ms". Arming at the declared maximum validates and returns "expires in 30m". A caller computing coverage from the declared maximum is wrong by 2x, silently.
  • Consequence for an agent that keeps three event watches armed (mail/queue/webhook shape): every expiry is a forced model turn purely to re-arm. On one seat that was 33 arms in 5 hours; across four seats a self-scheduled 55-minute heartbeat never fired once all day because a monitor expiry always woke the session first and the re-arm reset the timer. Effective wake cadence became the monitor cap, whatever the agent scheduled.

The ask, stated as a requirement rather than a preference: a session-length watch, or at minimum a timeout_ms ceiling of 3600000 honoured as declared. The reason 1 hour is the floor and not an arbitrary number: the prompt cache TTL on this plan is 1 hour, so a wake every 30 minutes to re-arm a watch is a full-context turn spent keeping a poller alive, twice per cache window, on every seat that watches anything. A 1-hour ceiling makes the forced re-arm coincide with the cache window instead of halving it; a true persistent watch removes it.

Also worth a line in the schema either way: if a parameter is dropped, reject it rather than accept-and-ignore — the silent path is what let this run unnoticed across several versions.

Showing cached comments. Read the full discussion on GitHub ↗