Skip to content

Channel builder sets no maxOutputTokens — a single build step can generate until the context limit #33

Description

@area51tazz

The channel builder sets no maxOutputTokens, so a single build step can generate until the model server's context limit. On a local reasoning model that turned two channel builds into ~70-minute runaways and blocked a whole lineup run.

packages/api/src/services/agent/channel-builder.ts:394:

const MAX_STEPS = 24;
...
const result = await generateText({
  model: getModel(connection),
  stopWhen: stepCountIs(MAX_STEPS),
  // no maxOutputTokens
});

stepCountIs(24) bounds the agent loop — how many tool-call rounds it takes. It doesn't bound generation within a step. And since #22 removed the Bun fetch watchdog (correctly — that fix is what makes slow local models viable at all), there's no longer a time bound either. So a step that doesn't converge has nothing to stop it short of the server's context window.

Measured

Local model, qwen3.6-35b on vLLM via LiteLLM, reasoning on for the worker role, planner on a cloud model, channelBuildConcurrency: 2:

channel agent steps wall clock outcome
Preschool Playhouse 2 71 min still generating when I killed it
Sci-Fi Frontier 1 65 min still generating when I killed it
Retro Action Night 24 failed cleanly at MAX_STEPS
20 others in the same run avg 3.5 25–43 s ok, 895–1,516 output tokens each

Measured throughput at the time was ~29.5 tok/s per request, so the older one had generated roughly 125,000 tokens on a single channel build — against successful builds in the same run using around a thousand.

Retro Action Night is the useful control: the loop guard fires exactly as designed. There simply isn't a token guard next to it.

The only backstop left is the model server. vLLM enforces prompt + max_tokens <= max_model_len (163,840 here), so it would eventually stop — after well over an hour, with a truncated result. That's a slow and expensive way to fail.

Consequence

With concurrency at 2, both slots were held by the two runaways, so the run was fully blocked at 23 of 42 channels for over an hour. I ended up cancelling the run and dropping the LiteLLM proxy to abort the in-flight generations, which cost me the 19 unbuilt channels.

Workaround, for anyone hitting this

The Extra request body (JSON) field added in 0.12.47 works nicely as a local fix — on the worker connection:

{"max_tokens": 16000}

That's ~10x headroom over what a real build uses, so it doesn't touch legitimate work, and it turns a 70-minute runaway into a fast failure. Might be worth a line on the local-models docs page.

Suggestion

A workerMaxOutputTokens setting alongside the plannerMaxOutputTokens you added in 0.12.48 would be the symmetric fix — or simply passing a generous default to generateText, since a per-step budget in the tens of thousands is far above anything a healthy build needs.

Related: #22 (where the watchdog removal came from) and #28 (a stop control would at least let an operator end one of these without killing the whole run).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions