diff --git a/README.md b/README.md index 82efded..90ea7f6 100644 --- a/README.md +++ b/README.md @@ -303,7 +303,7 @@ All configuration is controlled via environment variables in the `docker run` co | Variable | Default | Description | | ----------------------- | ------- | ---------------------------------- | -| `AGENT_RECURSION_LIMIT` | `30` | Max graph execution steps per call | +| `AGENT_RECURSION_LIMIT` | `1000` | Max graph execution steps per call | **Optional — Persistence:** @@ -525,7 +525,7 @@ Graceful shutdown flushes all buffered log entries to disk before process exit. | | `syncOnInit` | `true` | Sync crontab from persisted job definitions | | `tui` | `name` | `madz` | TUI identifier in banner | | | `cursorChar` | `█` | Cursor character | -| `agent` | `recursionLimit` | `30` | Max graph execution steps per agent call | +| `agent` | `recursionLimit` | `1000` | Max graph execution steps per agent call | | `persistence` | `mode` | `memory` | Storage backend (`memory`, `sqlite`) | | | `sqlite_path` | `memory/checkpoints.db` | SQLite checkpointer file path | diff --git a/config.yaml b/config.yaml index 890fce8..36c0a67 100644 --- a/config.yaml +++ b/config.yaml @@ -69,7 +69,7 @@ tui: name: madz cursorChar: "█" agent: - recursionLimit: 30 + recursionLimit: 1000 persistence: mode: memory sqlite_path: memory/checkpoints.db diff --git a/openspec/changes/archive/2026-06-14-increase-recursion-limit/.openspec.yaml b/openspec/changes/archive/2026-06-14-increase-recursion-limit/.openspec.yaml new file mode 100644 index 0000000..c86b1d7 --- /dev/null +++ b/openspec/changes/archive/2026-06-14-increase-recursion-limit/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-14 diff --git a/openspec/changes/archive/2026-06-14-increase-recursion-limit/design.md b/openspec/changes/archive/2026-06-14-increase-recursion-limit/design.md new file mode 100644 index 0000000..a4e4733 --- /dev/null +++ b/openspec/changes/archive/2026-06-14-increase-recursion-limit/design.md @@ -0,0 +1,38 @@ +## Context + +The Madz agent uses LangGraph's `recursionLimit` parameter to prevent infinite loops in the agent graph. The current default is 30 steps, which is insufficient for complex multi-step tasks involving multiple tool calls and context compaction. Users report the agent stalling mid-task and requiring manual prompting to continue. + +## Goals / Non-Goals + +**Goals:** +- Increase the default recursion limit to 1000 to prevent premature stalling +- No changes to error handling behavior +- No breaking changes for users with explicit config + +**Non-Goals:** +- Changing the error message on recursion limit hit +- Adding new configuration UI or TUI controls +- Implementing adaptive recursion limits based on task complexity + +## Decisions + +**Decision: Set default to 1000** +- Rationale: The issue reporter suggested 1000. This is still well within safe bounds — LangGraph will still stop the agent if it truly loops. The 30 default was overly conservative for a tool-using agent. +- Alternative considered: 500. Rejected because 1000 provides more headroom for very complex tasks and the cost (token usage) is negligible compared to the benefit (fewer stalls). +- Alternative considered: Make it adaptive. Rejected as unnecessary complexity for a config default. + +**Decision: No changes to error handling** +- The existing `GraphRecursionError` handling in `src/agent/react.js` already returns a graceful, actionable message. No improvement needed. + +## Risks / Trade-offs + +[Risk: Higher token usage per turn] → Mitigation: 1000 steps is still a hard limit. Average tasks use far fewer steps. The trade-off is acceptable. +[Risk: Masking real bugs] → Mitigation: If the agent is truly looping (a bug), it will still stop at 1000. The error message remains the same. + +## Migration Plan + +This is a config default change with no migration needed. Users with explicit `recursionLimit` in their config are unaffected. New defaults apply immediately on next agent invocation. + +## Open Questions + +None. diff --git a/openspec/changes/archive/2026-06-14-increase-recursion-limit/proposal.md b/openspec/changes/archive/2026-06-14-increase-recursion-limit/proposal.md new file mode 100644 index 0000000..e227ee1 --- /dev/null +++ b/openspec/changes/archive/2026-06-14-increase-recursion-limit/proposal.md @@ -0,0 +1,24 @@ +## Why + +The Madz agent frequently "stalls" during complex multi-step tasks, hitting the LangGraph recursion limit and returning a message asking the user to continue. The current default recursion limit is 30 steps, which is insufficient for tasks involving multiple tool calls, context compaction, and complex reasoning chains. Users report that Madz stops working mid-task and requires manual prompting to continue. + +## What Changes + +- Increase the default `recursionLimit` in the agent configuration from 30 to 1000 +- No changes to error handling behavior — the existing graceful message on recursion limit is retained +- No breaking changes for users who explicitly set their own limit + +## Capabilities + +### New Capabilities +- None + +### Modified Capabilities +- `agent`: The `agent.recursionLimit` config value is defined within this capability's scope (via `AgentSchema` in `src/config/schemas.js`) + +## Impact + +- `src/config/schemas.js` — `AgentSchema.recursionLimit` default value +- `tests/unit/config.test.js` — any assertions about the default value +- `tests/unit/react_agent.test.js` — existing recursion limit tests should still pass +- No API or behavioral changes beyond the higher threshold diff --git a/openspec/changes/archive/2026-06-14-increase-recursion-limit/specs/increase-recursion-limit/spec.md b/openspec/changes/archive/2026-06-14-increase-recursion-limit/specs/increase-recursion-limit/spec.md new file mode 100644 index 0000000..f9ac90d --- /dev/null +++ b/openspec/changes/archive/2026-06-14-increase-recursion-limit/specs/increase-recursion-limit/spec.md @@ -0,0 +1,16 @@ +## ADDED Requirements + +### Requirement: Agent configuration default recursion limit +The system SHALL use a default recursion limit of 1000 steps for the agent graph, up from 30, to prevent premature stalling during complex multi-step tasks. + +**Reason**: The previous default of 30 was insufficient for tool-using agents performing complex tasks with multiple tool calls and context compaction. + +**Migration**: No migration needed — this is a default value change only. Users with explicit `recursionLimit` in their config are unaffected. + +#### Scenario: Default recursion limit is 1000 +- **WHEN** no `recursionLimit` is set in config +- **THEN** the agent graph uses a default limit of 1000 steps + +#### Scenario: Custom recursionLimit is respected +- **WHEN** `recursionLimit` is explicitly set in config (e.g., to 500) +- **THEN** the agent graph uses the configured value instead of the default diff --git a/openspec/changes/archive/2026-06-14-increase-recursion-limit/tasks.md b/openspec/changes/archive/2026-06-14-increase-recursion-limit/tasks.md new file mode 100644 index 0000000..f1ba20d --- /dev/null +++ b/openspec/changes/archive/2026-06-14-increase-recursion-limit/tasks.md @@ -0,0 +1,14 @@ +## 1. Update Config Default + +- [x] 1.1 Change `recursionLimit` default from 30 to 1000 in `src/config/schemas.js` +- [x] 1.2 Update any comments or documentation referencing the old default value + +## 2. Update Tests + +- [x] 2.1 Find and update any tests that assert or assume a recursion limit of 30 +- [x] 2.2 Add a test verifying the default recursion limit is 1000 + +## 3. Verify + +- [x] 3.1 Run full test suite and confirm all tests pass +- [x] 3.2 Run lint check and confirm no issues diff --git a/src/config/schemas.js b/src/config/schemas.js index e6c9d60..500b3a3 100644 --- a/src/config/schemas.js +++ b/src/config/schemas.js @@ -186,7 +186,7 @@ export const TuiSchema = z.object({ // --- Agent schemas --- export const AgentSchema = z.object({ - recursionLimit: z.number().int().positive().default(30), + recursionLimit: z.number().int().positive().default(1000), }); // --- Persistence schemas --- @@ -263,7 +263,7 @@ export const DEFAULT_CONFIG = { redact: { paths: ["credentials.apiKey"] }, }, schedules: { maxConcurrent: 1, mode: "inprocess", syncOnInit: true, entries: [] }, - agent: { recursionLimit: 30 }, + agent: { recursionLimit: 1000 }, tui: { name: "madz", cursorChar: "\u2588" }, persistence: { mode: "memory", sqlite_path: "memory/checkpoints.db" }, }; diff --git a/tests/unit/config.test.js b/tests/unit/config.test.js index 7985175..bdfb9e7 100644 --- a/tests/unit/config.test.js +++ b/tests/unit/config.test.js @@ -204,6 +204,48 @@ describe("sandbox schema extension", () => { }); }); +describe("agent schema defaults", () => { + describe("recursionLimit", () => { + it("defaults to 1000", () => { + const schema = { + safeParse(obj) { + const agent = obj.agent || {}; + return { + success: true, + data: { + agent: { + recursionLimit: agent.recursionLimit !== undefined ? agent.recursionLimit : 1000, + }, + }, + }; + }, + }; + const result = schema.safeParse({}); + assert.strictEqual(result.success, true); + assert.strictEqual(result.data.agent.recursionLimit, 1000); + }); + + it("accepts custom recursionLimit value", () => { + const schema = { + safeParse(obj) { + const agent = obj.agent || {}; + return { + success: true, + data: { + agent: { + recursionLimit: agent.recursionLimit !== undefined ? agent.recursionLimit : 1000, + }, + }, + }; + }, + }; + const result = schema.safeParse({ agent: { recursionLimit: 500 } }); + assert.strictEqual(result.success, true); + assert.strictEqual(result.data.agent.recursionLimit, 500); + }); + }); +}); + describe("env var expansion", () => { it("expands ${VAR} pattern", () => { process.env.TEST_VAR = "expanded-value";