Skip to content

feat(agent): add unlimited mode for autonomous 24/7 operation#243

Open
lvndry wants to merge 17 commits into
mainfrom
feat/unlimited-mode
Open

feat(agent): add unlimited mode for autonomous 24/7 operation#243
lvndry wants to merge 17 commits into
mainfrom
feat/unlimited-mode

Conversation

@lvndry
Copy link
Copy Markdown
Owner

@lvndry lvndry commented May 26, 2026

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 maxIterations metadata. 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

  • Every agent run was capped at DEFAULT_MAX_ITERATIONS = 80 reasoning steps; longer runs were forced to type continue to resume.
  • Budget pressure messages were injected at 70% and 90% of the cap, biasing the model toward "wrap up now."
  • Meltdown detection aborted any run that made identical tool calls 4+ times in a 10-call window.
  • Each tool execution had a hard 3-minute timeout; a longer external call returned a timeout error.
  • Each LLM call had a hard 15-minute timeout; long reasoning chains failed.
  • LLM retries were capped at 10 attempts on transient errors; an outage past that window aborted the run.
  • Workflows could pin their own 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

unlimited mode is a global boolean activated by any of three paths (CLI flag > config > false at startup; /unlimited on|off toggles within a session):

  • CLI flag: jazz agent chat <agent> --unlimited (or --no-unlimited to disable for one run, even if config has it on). Same flag is available on jazz workflow run and jazz workflow catchup.
  • Config field: jazz config set unlimited true (persistent) or via the new "Unlimited Mode" entry in jazz config wizard.
  • Slash command: /unlimited (status), /unlimited on, /unlimited off inside a chat session.

When the mode is on:

  • The agent loop runs unbounded; budget-pressure messages are suppressed; meltdown detection does not abort.
  • Tool calls run without the 3-minute timeout.
  • Each LLM call runs without the 15-minute timeout.
  • LLM retries are unbounded (the existing exponential backoff capped at MAX_RETRY_DELAY_SECONDS still applies, so retries are not a hot loop).
  • Workflow-level maxIterations metadata is ignored.
  • Spawned subagents inherit the flag via parentUnlimited.
  • A subtle [unlimited] chip in the status footer makes the mode visible at a glance.

Changes made

Schema & types

  • src/core/types/config.ts — adds AppConfig.unlimited?: boolean with JSDoc enumerating every lifted guardrail.
  • src/core/agent/types.ts — adds unlimited?: boolean to AgentRunnerOptions and AgentRunContext.
  • src/core/types/tools.ts — adds parentUnlimited?: boolean to ToolExecutionContext for subagent inheritance.
  • src/core/interfaces/chat-service.ts — adds unlimited?: boolean to ChatService.startChatSession options.
  • src/services/chat/commands/types.ts — adds "unlimited" to CommandType, newUnlimited?: boolean to CommandResult, unlimited?: boolean to CommandContext.

Guardrail lifts

  • src/core/agent/execution/agent-loop.tsfor loop becomes for (let i = 0; unlimited || i < maxIterations; i++); buildBudgetPressureMessage accepts an unlimited parameter and short-circuits to null; detectMeltdown is gated by !unlimited. parentUnlimited is populated on the tool execution context.
  • src/core/agent/execution/tool-executor.tsexecuteTool, executeToolCall, executeToolCalls accept an unlimited parameter; when true, timeoutMs is forced to undefined so the no-timeout branch runs.
  • src/core/utils/llm-error.tsmakeLLMRetrySchedule(maxRetries, unlimited) drops the Schedule.recurs(maxRetries) cap when unlimited; the whileInput(isRetryableLLMError) filter and exponential backoff cap stay in both modes.
  • src/core/agent/execution/llm-retry-present.tsmakeUserVisibleLlmRetrySchedule accepts unlimited and omits "of up to N" from the status message.
  • src/core/agent/execution/streaming-executor.ts, batch-executor.ts — at each LLM call site, the Effect.timeout(LLM_TIMEOUT_SECONDS) wrapper is applied only when runContext.unlimited is false. Retry schedule and user-visible schedule receive runContext.unlimited.
  • src/core/agent/tools/subagent-tools.tsAgentRunner.runRecursive is given unlimited: context.parentUnlimited ?? false.
  • src/core/workflows/catch-up.ts, src/cli/commands/workflow.ts — workflow maxIterations metadata is dropped when appConfig.unlimited (or the --unlimited flag override) is true; unlimited is passed to AgentRunner.run. RunCatchUpOptions accepts an unlimitedOverride?: boolean.

Activation surfaces

  • src/cli/cli-app.ts — adds --unlimited / --no-unlimited to agent chat, workflow run, workflow catchup. Parses Commander.js's collapsed unlimited boolean correctly (the initial implementation used noUnlimited which Commander never sets; the fix in 5c22d74 reads options.unlimited === false for the negation).
  • src/cli/commands/chat-agent.ts — resolves resolvedUnlimited = options.unlimitedOverride ?? appConfig.unlimited ?? false and threads it into the chat session.
  • src/cli/commands/workflow.tsrunWorkflowCommand and catchupWorkflowCommand accept unlimitedOverride and resolve precedence.
  • src/cli/commands/config.ts — adds a coerceConfigValue helper that maps the string "true"/"false" to real booleans so jazz config set unlimited false actually stores false, 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 — adds unlimited to mergeConfig so a user's unlimited: true survives the config-file load path.
  • src/services/chat-service.ts — adds a session-local let unlimited, seeds it from CLI/config, threads it into CommandContext and AgentRunnerOptions, and updates it on commandResult.newUnlimited.
  • src/services/chat/commands/constants.ts, parser.ts, handler.ts — adds /unlimited to the slash-command registry, parser, and handler with on/off/status branches.

UI

  • src/cli/ui/store.ts — adds currentUnlimitedActive, registerUnlimitedSetter, setUnlimitedActive, getUnlimitedActive.
  • src/cli/ui/StatusFooter.tsx — renders a [unlimited] chip in THEME.warning color when the mode is active.
  • src/cli/ui/App.tsx — wires StatusFooterIslandComponent to the new store API.

Tests

  • New: src/core/utils/llm-error.test.ts (retry schedule bounded vs unlimited), src/cli/commands/config.test.ts (coerceConfigValue), plus bun:test blocks added to agent-loop.test.ts, tool-executor.test.ts, config.test.ts, parser.test.ts, handler.test.ts. The integration test for the iteration cap uses a try/finally block to guard mock teardown.

Impact

  • Default behavior is unchanged. Every new field is optional with a false default; every guardrail check still fires unless unlimited is explicitly true.
  • Performance: no measurable cost for users who don't enable the mode. When enabled, runs can use more tokens (no automatic abort) and run longer (no time cap). This is by design.
  • Risk surface for users who enable unlimited mode: a stuck tool or LLM call will hang until Ctrl+C; a model that loops on identical tool calls will burn tokens without an automatic abort; a permanently-down LLM provider will retry indefinitely (backoff still capped, so not a hot loop). Scheduled workflows in particular could miss the next run if a previous one hangs. These tradeoffs are accepted by the design.
  • No breaking API changes. All new fields are additive and optional. Existing callers that don't pass unlimited get the default behavior.
  • Out of scope (intentional): no daily-quota / cost-warning heuristics, no environment-variable activation, no per-tool unlimited overrides, no auto-persistence of the slash command toggle.

How to test

Run the full check suite first:

bun test          # 1242 pass / 0 fail
bun run typecheck # clean
bun run lint      # clean
bun run build     # bundles

1. CLI flag (happy path):

./dist/main.js agent chat --help

Expected: both --unlimited and --no-unlimited appear in the options list. Same for workflow run --help and workflow catchup --help.

2. Config round-trip (the boolean-coercion fix):

./dist/main.js config set unlimited true
./dist/main.js config get unlimited   # → true
./dist/main.js config set unlimited false
./dist/main.js config get unlimited   # → false

Inspect ~/.config/jazz/config.json (or your configured path) — the value should be the JSON boolean false, not the string "false". (Pre-fix, the string would be stored and if (appConfig.unlimited) would evaluate truthy because non-empty strings are truthy.)

3. Flag override edge case:
With unlimited: true already in config, run:

./dist/main.js agent chat <agent> --no-unlimited

Expected: the session does NOT have the [unlimited] chip in the footer, and standard guardrails apply for the run. (Pre-fix, --no-unlimited was silently ignored because the code read opts.noUnlimited which Commander never sets.)

4. Slash command toggle:
Inside a chat session:

  • Type /unlimited → reports current state and usage.
  • Type /unlimited on → confirmation message; [unlimited] chip appears in the footer.
  • Type /unlimited off → chip disappears.
  • Type /unlimited maybe → error + usage message.

5. Edge case — workflow maxIterations is dropped:
Create a workflow with maxIterations: 30 in its metadata. Enable unlimited mode via config. Run jazz 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: true and an agent that delegates to subagents via the spawn_subagent tool, confirm the subagent runs receive unlimited: true (visible in metrics: iterationsUsed for the subagent can exceed the default cap). Covered by the propagation test in agent-loop.test.ts.

Notes for reviewers

  • The spec and plan are local-only at docs/superpowers/specs/2026-05-25-unlimited-mode-design.md and docs/superpowers/plans/2026-05-25-unlimited-mode.md (gitignored per CLAUDE.md). Happy to share their content separately if useful.
  • During implementation, two correctness bugs surfaced in code review and were fixed before merge: (a) mergeConfig did not forward unlimited on load (commit 47e0f95); (b) setConfigCommand stored boolean flags as strings (commit 27e6f3c). Both fixes also indirectly help future boolean config fields.
  • The Schedule.intersect(Schedule.recurs(maxRetries)) removal in makeLLMRetrySchedule uses Schedule.map(([delay]) => delay) on the bounded branch to unify the schedule's output type with the unbounded branch. The recurs(N) AND-semantics still terminate the bounded schedule at exactly N attempts.
  • The status-footer chip placement leans on existing THEME.warning (yellow). If you'd prefer a different color or symbol, that's a one-line change in StatusFooter.tsx.

lvndry added 17 commits May 25, 2026 20:15
…is on

When appConfig.unlimited is true, workflow runners skip the workflow's
maxIterations and pass unlimited: true to AgentRunner.run instead.
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +20 to +24
export function coerceConfigValue(raw: string): string | boolean | number {
if (raw === "true") return true;
if (raw === "false") return false;
return raw;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since coerceConfigValue should coerce numeric strings to actual numbers for type safety, this test case should be updated to expect the number 123 instead of the string "123".

Suggested change
expect(coerceConfigValue("123")).toBe("123");
expect(coerceConfigValue("123")).toBe(123);

@github-actions
Copy link
Copy Markdown
Contributor

Jazz Code Review

No issues found.

Model: minimax/minimax-m2.5:free

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant