fix(todo-continuation-enforcer): add plan agent to DEFAULT_SKIP_AGENTS to prevent infinite loop

The todo-continuation-enforcer injects continuation prompts when
sessions go idle with pending todos. When Plan Mode agents (which are
read-only) create todo items, the continuation prompt contradicts
Plan Mode's STRICTLY FORBIDDEN directive, causing an infinite loop
where the agent acknowledges the conflict then goes idle, triggering
another injection.

Adding 'plan' to DEFAULT_SKIP_AGENTS prevents continuation injection
into Plan Mode sessions, matching the same exclusion pattern already
used for prometheus and compaction agents.

Fixes #2526
This commit is contained in:
MoerAI
2026-03-16 10:24:57 +09:00
committed by sspark-kisane
parent 65ccc9b854
commit 2b6b08345a
3 changed files with 36 additions and 2 deletions

View File

@@ -38,7 +38,7 @@ session.idle
## CONSTANTS
```typescript
DEFAULT_SKIP_AGENTS = ["prometheus", "compaction"]
DEFAULT_SKIP_AGENTS = ["prometheus", "compaction", "plan"]
CONTINUATION_COOLDOWN_MS = 30_000 // 30s between injections
MAX_CONSECUTIVE_FAILURES = 5 // Then 5min pause (exponential backoff)
FAILURE_RESET_WINDOW_MS = 5 * 60_000 // 5min window for failure reset

View File

@@ -2,7 +2,7 @@ import { createSystemDirective, SystemDirectiveTypes } from "../../shared/system
export const HOOK_NAME = "todo-continuation-enforcer"
export const DEFAULT_SKIP_AGENTS = ["prometheus", "compaction"]
export const DEFAULT_SKIP_AGENTS = ["prometheus", "compaction", "plan"]
export const CONTINUATION_PROMPT = `${createSystemDirective(SystemDirectiveTypes.TODO_CONTINUATION)}

View File

@@ -47,4 +47,38 @@ describe("injectContinuation", () => {
expect(capturedTools).toEqual({ question: false, bash: true })
expect(capturedText).toContain(OMO_INTERNAL_INITIATOR_MARKER)
})
test("skips injection when agent is plan (prevents Plan Mode infinite loop)", async () => {
// given
let injected = false
const ctx = {
directory: "/tmp/test",
client: {
session: {
todo: async () => ({ data: [{ id: "1", content: "todo", status: "pending", priority: "high" }] }),
promptAsync: async () => {
injected = true
return {}
},
},
},
}
const sessionStateStore = {
getExistingState: () => ({ inFlight: false, lastInjectedAt: 0, consecutiveFailures: 0 }),
}
// when
await injectContinuation({
ctx: ctx as never,
sessionID: "ses_plan_skip",
resolvedInfo: {
agent: "plan",
model: { providerID: "anthropic", modelID: "claude-sonnet-4-20250514" },
},
sessionStateStore: sessionStateStore as never,
})
// then
expect(injected).toBe(false)
})
})