fix(todo-continuation): avoid duplicate progress reset

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-04-28 18:00:47 +09:00
parent ebcd6edf5a
commit fbaeb032c0
2 changed files with 99 additions and 2 deletions
@@ -0,0 +1,99 @@
/// <reference types="bun-types" />
import { describe, expect, it } from "bun:test"
import { handleSessionIdle } from "./idle-event"
import type { SessionStateStore } from "./session-state"
import type { ContinuationProgressUpdate, SessionState } from "./types"
function createStateStore(): {
store: SessionStateStore
resetCalls: string[]
} {
const state: SessionState = {
stagnationCount: 0,
consecutiveFailures: 0,
}
const resetCalls: string[] = []
const progressUpdate: ContinuationProgressUpdate = {
previousStagnationCount: 0,
stagnationCount: 0,
hasProgressed: false,
progressSource: "none",
}
return {
resetCalls,
store: {
getState: () => state,
getExistingState: () => state,
startPruneInterval: () => {},
recordActivity: () => {},
trackContinuationProgress: () => progressUpdate,
resetContinuationProgress: (sessionID: string) => {
resetCalls.push(sessionID)
},
cancelCountdown: () => {},
cleanup: () => {},
cancelAllCountdowns: () => {},
shutdown: () => {},
},
}
}
describe("handleSessionIdle", () => {
it("resets continuation progress once when todos are empty", async () => {
// given
const sessionID = "ses_empty_todos"
const { store, resetCalls } = createStateStore()
const ctx = {
client: {
session: {
messages: async () => ({ data: [] }),
todo: async () => ({ data: [] }),
},
},
directory: "/tmp/test",
}
// when
await handleSessionIdle({
ctx: ctx as never,
sessionID,
sessionStateStore: store,
})
// then
expect(resetCalls).toEqual([sessionID])
})
it("resets continuation progress once when every todo is complete", async () => {
// given
const sessionID = "ses_completed_todos"
const { store, resetCalls } = createStateStore()
const ctx = {
client: {
session: {
messages: async () => ({ data: [] }),
todo: async () => ({
data: [
{ id: "todo-1", content: "Ship", status: "completed", priority: "high" },
{ id: "todo-2", content: "Verify", status: "completed", priority: "medium" },
],
}),
},
},
directory: "/tmp/test",
}
// when
await handleSessionIdle({
ctx: ctx as never,
sessionID,
sessionStateStore: store,
})
// then
expect(resetCalls).toEqual([sessionID])
})
})
@@ -108,7 +108,6 @@ export async function handleSessionIdle(args: {
}
if (!todos || todos.length === 0) {
sessionStateStore.resetContinuationProgress(sessionID)
sessionStateStore.resetContinuationProgress(sessionID)
log(`[${HOOK_NAME}] No todos`, { sessionID })
return
@@ -116,7 +115,6 @@ export async function handleSessionIdle(args: {
const incompleteCount = getIncompleteCount(todos)
if (incompleteCount === 0) {
sessionStateStore.resetContinuationProgress(sessionID)
sessionStateStore.resetContinuationProgress(sessionID)
log(`[${HOOK_NAME}] All todos complete`, { sessionID, total: todos.length })
return