Skip to content

fix(automations): raise iteration cap to 50, surface update errors#369

Merged
mgoldsborough merged 2 commits into
mainfrom
fix/automation-iteration-cap
Jun 3, 2026
Merged

fix(automations): raise iteration cap to 50, surface update errors#369
mgoldsborough merged 2 commits into
mainfrom
fix/automation-iteration-cap

Conversation

@mgoldsborough
Copy link
Copy Markdown
Contributor

Problem

A scheduled automation ("daily users" on a tenant) reported No output captured for this run with Stop: max_iterations. These are one phenomenon: the agent called a tool on every iteration up to the cap — including the final wrap-up step — so the loop exited via the iteration ceiling instead of the model saying "done." The run's output only accumulates assistant text blocks, so a run that ends mid-tool-call has empty output → the reader's "No output captured" fallback.

Two root contributors:

  1. The automation iteration budget is too small. The create-form default is 5, and the hard cap is 15. Under progressive disclosure the agent spends ~2–4 iterations on tool discovery (nb__search → promote → call) before real work starts, so 5 leaves almost nothing for the actual task + synthesis. The run we debugged spent 2 of 5 steps on manage_workspaces/manage_tools discovery alone.
  2. The UI swallowed the save error. Trying to raise maxIterations to a value above the cap was rejected server-side (maxIterations must be between 1 and 15), but handleUpdate caught it in a silent catch {} and refetched — so the field snapped back and it looked like "save isn't saving."

Changes

  • Cap 15 → 50 (the engine's absolute MAX_ITERATIONS) in validateAutomationFields, the platform schema description, and the type doc comment.
  • Create-form default 5 → 25 (DEFAULT_MAX_ITERATIONS) and input max 15 → 50.
  • Surface update errors: handleUpdate lets the rejection propagate (still refreshes in finally); saveField renders it in the detail view's existing error banner.
  • Test: server.test.ts now pins the 1..50 bound (rejects 51, accepts 50 and 25).

Rationale

The iteration cap is a runaway-loop backstop, not a cost budget — per-run cost is bounded directly by maxInputTokens (200k) and maxRunDurationMs (120s). A run that hits the cap has already spent the tokens and yields nothing, so a low cap converts "expensive but useful" into "expensive and useless." 50 matches the engine ceiling and the chat surface.

Verify

bun run verify green (static + unit + web + integration + smoke); build:bundles rebuilds the automations UI clean.

The create-form default of 5 iterations amputated analytical automations
mid-run: they hit the cap, produced no final text, and reported "No output
captured for this run." The iteration cap is a runaway backstop, not a cost
budget — per-run cost is bounded directly by maxInputTokens and
maxRunDurationMs — so a low cap is nearly all downside.

- Raise the automation maxIterations cap 15 -> 50 (the engine's absolute
  MAX_ITERATIONS ceiling) in validateAutomationFields, the schema description,
  and the type doc comment.
- Raise the create-form default 5 -> 25 (DEFAULT_MAX_ITERATIONS) and the
  input max 15 -> 50.
- Surface update failures in the UI: handleUpdate no longer swallows the
  rejection in a silent catch, and the detail view's saveField renders the
  error in its existing error banner. Previously a rejected edit (e.g. an
  out-of-range maxIterations) looked like a silent no-op — the field just
  snapped back to its old value.
- Update server.test.ts to pin the new 1..50 bound.
…constant

Import MAX_ITERATIONS from src/limits.ts for the bundle's upper-bound check
instead of a literal 50, so the validation bound provably tracks the engine's
enforcement (engine.ts: Math.min(config.maxIterations, MAX_ITERATIONS)). A
drifted literal would otherwise let the bundle accept values the engine
silently clamps, or reject values it would honor.

The schema description stays a literal: src/tools/platform/schemas/ is
codegen'd under a strict rootDir (scripts/tsconfig.codegen-web.json) that
forbids importing from outside that dir, so the constant can't reach it. A
comment points at the source of truth; the description is documentation only.
@mgoldsborough mgoldsborough added the qa-reviewed QA review completed with no critical issues label Jun 3, 2026
@mgoldsborough mgoldsborough merged commit 17fb92f into main Jun 3, 2026
5 checks passed
@mgoldsborough mgoldsborough deleted the fix/automation-iteration-cap branch June 3, 2026 03:11
mgoldsborough added a commit that referenced this pull request Jun 3, 2026
…ilure (#371)

The error-surfacing fix from #369 was defeated by its own refresh call.
saveField set the error in its catch, then unconditionally called
loadDetail(), whose first synchronous statement is setError(null) (before
its await). Both updates landed in the same microtask continuation, React
batched them, null won, and the banner never rendered — so a rejected save
(e.g. an out-of-range maxIterations) still looked like a silent no-op, the
exact symptom #369 set out to fix.

Refresh only on success. On failure the error persists and the closed inline
editor re-renders the field at its unchanged saved value.

No regression test: the bundle UIs have no component-test harness
(@testing-library), so this React state-timing path can't be covered without
standing up RTL. Verified manually against the batching trace.

Co-authored-by: Mathew Goldsborough <1759329+mgoldsborough@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

qa-reviewed QA review completed with no critical issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant