fix(todo-continuation-enforcer): fire continuation for all sessions with incomplete todos

Remove boulder session restriction (f84ef532) and stagnation cap (10a60854)
that prevented continuation from firing in regular sessions.

Changes:
- Remove boulder/subagent session gate in idle-event.ts — continuation now
  fires for ANY session with incomplete todos, as originally intended
- Remove stagnation cap (MAX_UNCHANGED_CYCLES) — agent must keep rolling
  the boulder until all todos are complete, no giving up after 3 attempts
- Remove lastTodoHash and unchangedCycles from SessionState type
- Keep 30s cooldown (CONTINUATION_COOLDOWN_MS) as safety net against
  re-injection loops
- Update tests: remove boulder gate tests, update stagnation test to verify
  continuous injection, update non-main-session test to verify injection

42 tests pass, typecheck and build clean.
This commit is contained in:
YeonGyu-Kim
2026-02-13 18:50:53 +09:00
parent 38ea1c372b
commit db76a1a531
4 changed files with 60 additions and 213 deletions
@@ -1,8 +1,6 @@
import type { PluginInput } from "@opencode-ai/plugin"
import type { BackgroundManager } from "../../features/background-agent"
import { readBoulderState } from "../../features/boulder-state"
import { subagentSessions } from "../../features/claude-code-session-state"
import type { ToolPermission } from "../../features/hook-message-injector"
import { log } from "../../shared/logger"
@@ -11,7 +9,6 @@ import {
CONTINUATION_COOLDOWN_MS,
DEFAULT_SKIP_AGENTS,
HOOK_NAME,
MAX_UNCHANGED_CYCLES,
} from "./constants"
import { isLastAssistantMessageAborted } from "./abort-detection"
import { getIncompleteCount } from "./todo"
@@ -38,16 +35,6 @@ export async function handleSessionIdle(args: {
log(`[${HOOK_NAME}] session.idle`, { sessionID })
const isBackgroundTaskSession = subagentSessions.has(sessionID)
const boulderState = readBoulderState(ctx.directory)
const isBoulderSession = boulderState?.session_ids.includes(sessionID) ?? false
// Continuation is restricted to boulder/background sessions to prevent accidental continuation in regular sessions, ensuring controlled task resumption.
if (!isBackgroundTaskSession && !isBoulderSession) {
log(`[${HOOK_NAME}] Skipped: not boulder or background task session`, { sessionID })
return
}
const state = sessionStateStore.getState(sessionID)
if (state.isRecovering) {
log(`[${HOOK_NAME}] Skipped: in recovery`, { sessionID })
@@ -117,19 +104,6 @@ export async function handleSessionIdle(args: {
return
}
const incompleteTodos = todos.filter((todo) => todo.status !== "completed" && todo.status !== "cancelled")
const todoHash = incompleteTodos.map((todo) => `${todo.id}:${todo.status}`).join("|")
if (state.lastTodoHash === todoHash) {
state.unchangedCycles = (state.unchangedCycles ?? 0) + 1
if (state.unchangedCycles >= MAX_UNCHANGED_CYCLES) {
log(`[${HOOK_NAME}] Skipped: stagnation cap reached`, { sessionID, cycles: state.unchangedCycles })
return
}
} else {
state.unchangedCycles = 0
}
state.lastTodoHash = todoHash
let resolvedInfo: ResolvedMessageInfo | undefined
let hasCompactionMessage = false
try {