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
- Add
steps limits in opencode.json:
{
"agent": {
"planner": { "steps": 15 },
"executor": { "steps": 30 },
"implementer": { "steps": 40 }
}
}
- Start OpenCode with micode plugin active
- 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
Summary
When users configure per-agent
steps(iteration limits) inopencode.json, micode'sconfig()hook silently replaces the entire agent config object, discarding thestepsvalue. This meansstepslimits have no effect when micode is active.Root Cause
In
src/index.ts, theconfig()hook merges agents like this: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 frommergedAgents, which does whole-object replacement due to JavaScript spread's last-wins semantics.Since
stepsis also not inSAFE_AGENT_PROPERTIES, users cannot work around this viamicode.jsoneither — the property is stripped during sanitization.Steps to Reproduce
stepslimits inopencode.json:{ "agent": { "planner": { "steps": 15 }, "executor": { "steps": 30 }, "implementer": { "steps": 40 } } }Impact
stepsmay not realize it has no effectSuggested Fix
Option A (minimal): Merge
stepsfrom the original config before spreading:Option B (comprehensive): Add
stepstoSAFE_AGENT_PROPERTIESso users can configure it viamicode.json:And change the agent spread to merge instead of replace:
Environment