Skip to content

Bug: steps from opencode.json agent config silently discarded during agent merge #53

@AssassinQuin

Description

@AssassinQuin

Summary

When users configure per-agent steps (iteration limits) in opencode.json, micode's config() hook silently replaces the entire agent config object, discarding the steps value. This means steps limits have no effect when micode is active.

Root Cause

In src/index.ts, the config() hook merges agents like this:

config.agent = {
  ...config.agent,        // ← OpenCode's config WITH steps from opencode.json
  build: { ...config.agent?.build, mode: "subagent" },    // ✅ MERGE - preserves steps
  plan:  { ...config.agent?.plan, mode: "subagent" },      // ✅ MERGE - preserves steps
  triage:{ ...config.agent?.triage, mode: "subagent" },    // ✅ MERGE - preserves steps
  docs:  { ...config.agent?.docs, mode: "subagent" },      // ✅ MERGE - preserves steps
  ...Object.fromEntries(
    Object.entries(mergedAgents).filter(([k]) => k !== PRIMARY_AGENT_NAME)
  ),   // ❌ REPLACES - loses steps
  [PRIMARY_AGENT_NAME]: mergedAgents[PRIMARY_AGENT_NAME]   // ❌ REPLACES - loses steps
};

OpenCode's own agents (build, plan, triage, docs) use { ...config.agent?.X, mode } which merges into the existing config. But micode's agents are spread from mergedAgents, which does whole-object replacement due to JavaScript spread's last-wins semantics.

Since steps is also not in SAFE_AGENT_PROPERTIES, users cannot work around this via micode.json either — the property is stripped during sanitization.

Steps to Reproduce

  1. Add steps limits in opencode.json:
{
  "agent": {
    "planner": { "steps": 15 },
    "executor": { "steps": 30 },
    "implementer": { "steps": 40 }
  }
}
  1. Start OpenCode with micode plugin active
  2. Observe that agents run with default/unlimited steps — the configured limits are ignored

Impact

  • Agents can run indefinitely (no iteration cap), contributing to zombie agent problems (OpenCode issue #11865)
  • Token consumption is unbounded for heavy agents like executor/implementer
  • Users who configure steps may not realize it has no effect

Suggested Fix

Option A (minimal): Merge steps from the original config before spreading:

const preserveSteps = (agents: Record<string, any>, original: Record<string, any>) => {
  const result = { ...agents };
  for (const [name, cfg] of Object.entries(original)) {
    if (cfg?.steps && result[name]) {
      result[name] = { ...result[name], steps: cfg.steps };
    }
  }
  return result;
};

Option B (comprehensive): Add steps to SAFE_AGENT_PROPERTIES so users can configure it via micode.json:

const SAFE_AGENT_PROPERTIES = ["model", "temperature", "maxTokens", "thinking", "steps"] as const;

And change the agent spread to merge instead of replace:

...Object.fromEntries(
  Object.entries(mergedAgents)
    .filter(([k]) => k !== PRIMARY_AGENT_NAME)
    .map(([name, cfg]) => [name, { ...config.agent?.[name], ...cfg }])
),
[PRIMARY_AGENT_NAME]: { ...config.agent?.[PRIMARY_AGENT_NAME], ...mergedAgents[PRIMARY_AGENT_NAME] }

Environment

  • micode: v0.10.0
  • OpenCode: v1.4.x
  • Model: zhipuai-coding-plan/glm-5.1

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