feat(agent): add unlimited mode for autonomous 24/7 operation#243
feat(agent): add unlimited mode for autonomous 24/7 operation#243lvndry wants to merge 17 commits into
Conversation
…ion in unlimited mode
…ation count assertion
…is on When appConfig.unlimited is true, workflow runners skip the workflow's maxIterations and pass unlimited: true to AgentRunner.run instead.
There was a problem hiding this comment.
Code Review
This pull request introduces an "unlimited" mode across the CLI, agent runner, workflows, and chat services, allowing users to bypass standard guardrails such as iteration caps, retries, and timeouts. It also adds configuration options, UI indicators, and a new /unlimited chat command to manage this state. The review feedback correctly identifies that the newly added coerceConfigValue utility fails to coerce numeric strings to numbers, which would cause configuration values like maxRetries to be stored as strings and violate TypeScript definitions. The reviewer provided actionable suggestions to fix this coercion logic and update the corresponding unit tests.
| export function coerceConfigValue(raw: string): string | boolean | number { | ||
| if (raw === "true") return true; | ||
| if (raw === "false") return false; | ||
| return raw; | ||
| } |
There was a problem hiding this comment.
The coerceConfigValue function claims to return string | boolean | number, but it currently only coerces boolean strings ("true" / "false") and leaves numeric strings as strings. This causes configuration values like maxRetries or telemetry limits (which are typed as number in AppConfig) to be stored as strings, violating the TypeScript type definitions.
We should coerce valid numeric strings to numbers to maintain type safety.
export function coerceConfigValue(raw: string): string | boolean | number {
if (raw === "true") return true;
if (raw === "false") return false;
const num = Number(raw);
if (raw.trim() !== "" && !Number.isNaN(num)) return num;
return raw;
}| it("leaves other strings unchanged", () => { | ||
| expect(coerceConfigValue("hello")).toBe("hello"); | ||
| expect(coerceConfigValue("")).toBe(""); | ||
| expect(coerceConfigValue("123")).toBe("123"); |
There was a problem hiding this comment.
Jazz Code ReviewNo issues found. Model: minimax/minimax-m2.5:free |
What and why
Adds an opt-in "unlimited mode" that lifts every per-run guardrail in Jazz — iteration cap, budget-pressure nudges, meltdown detection, per-tool timeouts, per-call LLM timeout, LLM retry cap, and workflow-level
maxIterationsmetadata. The use case is running Jazz autonomously over long horizons (24/7 background workflows, "run my company" style operation) where the default safety rails are too aggressive. The mode is fully opt-in and propagates to spawned subagents, so a single switch governs the whole agent graph.Before this PR
DEFAULT_MAX_ITERATIONS = 80reasoning steps; longer runs were forced to typecontinueto resume.maxIterations, which always took precedence.There was no single switch to turn off these guardrails for users who explicitly wanted unbounded operation.
After this PR
unlimitedmode is a global boolean activated by any of three paths (CLI flag > config > false at startup;/unlimited on|offtoggles within a session):jazz agent chat <agent> --unlimited(or--no-unlimitedto disable for one run, even if config has it on). Same flag is available onjazz workflow runandjazz workflow catchup.jazz config set unlimited true(persistent) or via the new "Unlimited Mode" entry injazz config wizard./unlimited(status),/unlimited on,/unlimited offinside a chat session.When the mode is on:
MAX_RETRY_DELAY_SECONDSstill applies, so retries are not a hot loop).maxIterationsmetadata is ignored.parentUnlimited.[unlimited]chip in the status footer makes the mode visible at a glance.Changes made
Schema & types
src/core/types/config.ts— addsAppConfig.unlimited?: booleanwith JSDoc enumerating every lifted guardrail.src/core/agent/types.ts— addsunlimited?: booleantoAgentRunnerOptionsandAgentRunContext.src/core/types/tools.ts— addsparentUnlimited?: booleantoToolExecutionContextfor subagent inheritance.src/core/interfaces/chat-service.ts— addsunlimited?: booleantoChatService.startChatSessionoptions.src/services/chat/commands/types.ts— adds"unlimited"toCommandType,newUnlimited?: booleantoCommandResult,unlimited?: booleantoCommandContext.Guardrail lifts
src/core/agent/execution/agent-loop.ts—forloop becomesfor (let i = 0; unlimited || i < maxIterations; i++);buildBudgetPressureMessageaccepts anunlimitedparameter and short-circuits to null;detectMeltdownis gated by!unlimited.parentUnlimitedis populated on the tool execution context.src/core/agent/execution/tool-executor.ts—executeTool,executeToolCall,executeToolCallsaccept anunlimitedparameter; when true,timeoutMsis forced toundefinedso the no-timeout branch runs.src/core/utils/llm-error.ts—makeLLMRetrySchedule(maxRetries, unlimited)drops theSchedule.recurs(maxRetries)cap when unlimited; thewhileInput(isRetryableLLMError)filter and exponential backoff cap stay in both modes.src/core/agent/execution/llm-retry-present.ts—makeUserVisibleLlmRetryScheduleacceptsunlimitedand omits "of up to N" from the status message.src/core/agent/execution/streaming-executor.ts,batch-executor.ts— at each LLM call site, theEffect.timeout(LLM_TIMEOUT_SECONDS)wrapper is applied only whenrunContext.unlimitedis false. Retry schedule and user-visible schedule receiverunContext.unlimited.src/core/agent/tools/subagent-tools.ts—AgentRunner.runRecursiveis givenunlimited: context.parentUnlimited ?? false.src/core/workflows/catch-up.ts,src/cli/commands/workflow.ts— workflowmaxIterationsmetadata is dropped whenappConfig.unlimited(or the--unlimitedflag override) is true;unlimitedis passed toAgentRunner.run.RunCatchUpOptionsaccepts anunlimitedOverride?: boolean.Activation surfaces
src/cli/cli-app.ts— adds--unlimited/--no-unlimitedtoagent chat,workflow run,workflow catchup. Parses Commander.js's collapsedunlimitedboolean correctly (the initial implementation usednoUnlimitedwhich Commander never sets; the fix in 5c22d74 readsoptions.unlimited === falsefor the negation).src/cli/commands/chat-agent.ts— resolvesresolvedUnlimited = options.unlimitedOverride ?? appConfig.unlimited ?? falseand threads it into the chat session.src/cli/commands/workflow.ts—runWorkflowCommandandcatchupWorkflowCommandacceptunlimitedOverrideand resolve precedence.src/cli/commands/config.ts— adds acoerceConfigValuehelper that maps the string"true"/"false"to real booleans sojazz config set unlimited falseactually storesfalse, not the truthy string"false".src/cli/commands/config-wizard.ts— adds a top-level "Unlimited Mode" menu entry with a single Y/N confirm prompt.src/services/config.ts— addsunlimitedtomergeConfigso a user'sunlimited: truesurvives the config-file load path.src/services/chat-service.ts— adds a session-locallet unlimited, seeds it from CLI/config, threads it intoCommandContextandAgentRunnerOptions, and updates it oncommandResult.newUnlimited.src/services/chat/commands/constants.ts,parser.ts,handler.ts— adds/unlimitedto the slash-command registry, parser, and handler with on/off/status branches.UI
src/cli/ui/store.ts— addscurrentUnlimitedActive,registerUnlimitedSetter,setUnlimitedActive,getUnlimitedActive.src/cli/ui/StatusFooter.tsx— renders a[unlimited]chip inTHEME.warningcolor when the mode is active.src/cli/ui/App.tsx— wiresStatusFooterIslandComponentto the new store API.Tests
src/core/utils/llm-error.test.ts(retry schedule bounded vs unlimited),src/cli/commands/config.test.ts(coerceConfigValue), plusbun:testblocks added toagent-loop.test.ts,tool-executor.test.ts,config.test.ts,parser.test.ts,handler.test.ts. The integration test for the iteration cap uses atry/finallyblock to guard mock teardown.Impact
falsedefault; every guardrail check still fires unlessunlimitedis explicitly true.unlimitedget the default behavior.How to test
Run the full check suite first:
1. CLI flag (happy path):
Expected: both
--unlimitedand--no-unlimitedappear in the options list. Same forworkflow run --helpandworkflow catchup --help.2. Config round-trip (the boolean-coercion fix):
Inspect
~/.config/jazz/config.json(or your configured path) — the value should be the JSON booleanfalse, not the string"false". (Pre-fix, the string would be stored andif (appConfig.unlimited)would evaluate truthy because non-empty strings are truthy.)3. Flag override edge case:
With
unlimited: truealready in config, run:Expected: the session does NOT have the
[unlimited]chip in the footer, and standard guardrails apply for the run. (Pre-fix,--no-unlimitedwas silently ignored because the code readopts.noUnlimitedwhich Commander never sets.)4. Slash command toggle:
Inside a chat session:
/unlimited→ reports current state and usage./unlimited on→ confirmation message;[unlimited]chip appears in the footer./unlimited off→ chip disappears./unlimited maybe→ error + usage message.5. Edge case — workflow
maxIterationsis dropped:Create a workflow with
maxIterations: 30in its metadata. Enable unlimited mode via config. Runjazz workflow run <name>. The agent should not stop at iteration 30; it should run until the strategy returns no further tool calls.6. Edge case — subagent inheritance:
With
unlimited: trueand an agent that delegates to subagents via thespawn_subagenttool, confirm the subagent runs receiveunlimited: true(visible in metrics:iterationsUsedfor the subagent can exceed the default cap). Covered by the propagation test inagent-loop.test.ts.Notes for reviewers
docs/superpowers/specs/2026-05-25-unlimited-mode-design.mdanddocs/superpowers/plans/2026-05-25-unlimited-mode.md(gitignored per CLAUDE.md). Happy to share their content separately if useful.mergeConfigdid not forwardunlimitedon load (commit47e0f95); (b)setConfigCommandstored boolean flags as strings (commit27e6f3c). Both fixes also indirectly help future boolean config fields.Schedule.intersect(Schedule.recurs(maxRetries))removal inmakeLLMRetryScheduleusesSchedule.map(([delay]) => delay)on the bounded branch to unify the schedule's output type with the unbounded branch. Therecurs(N)AND-semantics still terminate the bounded schedule at exactly N attempts.THEME.warning(yellow). If you'd prefer a different color or symbol, that's a one-line change inStatusFooter.tsx.