Wards & Seals

A longer clock is not a looser leash

Cat-AgentUI's whole-turn timeout just went from 15 to 60 minutes. Here is why that is safe to do, and how the other limits on a single reply, from idle timeouts to tool rounds and concurrency, share the job of stopping a runaway turn.

Last week I made one reply in Cat-AgentUI allowed to run four times longer. The change is a single default in commit 3702398: CHAT_TURN_TIMEOUT_SECONDS went from 900 to 3600, and the highest value an operator can set went from 3600 to 21600.

Raising a timeout sounds like the opposite of security work. In a multi-user panel where a model can call tools and run sandboxed commands, a turn that never ends is a real way to burn other people's capacity and someone's provider quota. So before I changed the number I wanted to be clear with myself about what the timeout was actually protecting, and what else was already doing that job.

Why the old limit had to move

The symptom was a user-visible one. With the highest reasoning level on and a research task that needed many tool calls in a row, replies were being cut off halfway with the message that the turn had exceeded its 900-second total limit. Nothing was stuck. The model was thinking, calling tools, reading results, and thinking again, and fifteen minutes simply was not enough for that kind of work.

A limit that stops legitimate work is not a good limit. It also trains people to work around it, which is worse.

Two clocks, two different questions

The key point is that the whole-turn timeout was never the only clock. There are two, and they answer different questions:

Setting Default Question it answers
CHAT_PROVIDER_IDLE_TIMEOUT_SECONDS 120 Has the upstream stream gone silent?
CHAT_TURN_TIMEOUT_SECONDS 3600 (was 900) Has this turn as a whole gone on far longer than any real task should?

A stalled upstream, the common failure, is caught by the idle timeout after two minutes without events, no matter how generous the total limit is. That means the total limit does not need to be tight to catch hangs. Its job is narrower: to be a backstop for a turn that keeps producing events but never converges. The comment I left next to the setting in server/src/config.ts says exactly that, so the next person who reads it (probably me) does not tighten it again for the wrong reason.

The same value is also used by a few other paths that wait on a model: the local Claude Code suspend wait, translation, and OCR. Those were relaxed together, so they stay consistent with chat instead of quietly failing earlier.

The other limits on a single turn

Time is only one budget. A turn in Cat-AgentUI is also bounded by size, by steps, and by how many can run at once. These are all documented in the README's environment variable table:

  • Output per call. MAX_MODEL_OUTPUT_TOKENS (65536) is the hard ceiling for one model request.
  • Output per turn. MAX_TURN_OUTPUT_CHARS (500000) caps the accumulated characters of a whole turn, thinking and tool results included. A turn that loops and keeps generating hits this regardless of the clock.
  • Tool rounds. MAX_TOOL_ITERATIONS (10 by default) limits how many tool iterations one reply can take. In an earlier fix, 59e6fbb, reaching that limit now asks the model to wrap up instead of leaving an unfinished call behind.
  • Retry waiting. PROVIDER_RETRY_MAX_WAIT_SECONDS (60) caps how long one request will wait out 429/503/529 responses before giving up, so rate limiting cannot stretch a turn indefinitely.
  • Concurrency. MAX_CHAT_CONCURRENCY_PER_USER (2) and MAX_CHAT_CONCURRENCY_GLOBAL (20) bound how many replies can be running at all, and sandboxed commands have their own site-wide cap of 3, one per person, with a per-command timeout an administrator can raise only up to MAX_SANDBOX_TIMEOUT_SECONDS (600).
  • Money. Per-model daily or weekly caps on requests or tokens can reject or downgrade once they are reached.

Looking at the list together is what made the change feel safe. A single long turn from one user can occupy at most one of their two chat slots, cannot produce more than a fixed amount of output, cannot take more than a fixed number of tool rounds, and cannot hold more than one sandbox slot. A longer clock does not loosen any of those.

What I take from this

  • Give each limit one job. When one setting is quietly doing two jobs, catching hangs and catching runaways, you end up tuning it for the stricter one and breaking real use.
  • Write the reason next to the number. A bare constant invites someone to change it. A one-line comment saying what it guards against is cheap documentation for a security decision.
  • Change the ceiling, not just the default. Operators who need a longer run can now set it themselves, but the code still clamps the value, so a typo in .env cannot turn into an unlimited turn.

What I still want to check

These are things I have not done yet and plan to try on my own instance, not results:

  • Run a deliberately looping tool workflow and confirm which limit stops it first, and that the user sees a clear message rather than a silent cut.
  • Confirm that a cancelled turn releases its concurrency and sandbox slots immediately, not when the 60-minute clock runs out.
  • Check that usage from a turn that ends on a limit is still recorded in full, so quota accounting stays honest.

If any of these turn up something, it will go into a Field Report.

Totooria Helmold, Atelier Arland