docs: note Atlas stalled continuation fix

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-05-18 12:43:26 +09:00
parent d3b4c022d2
commit 1a66b96bb8
4 changed files with 151 additions and 2 deletions
+2 -1
View File
@@ -12,11 +12,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Relanded BLOCKER-4 delegated child-session empty-history fallback. Runtime fallback now consumes the captured bootstrap prompt when a delegated child session fails before history is persisted, while preserving delegated system prompts and tool permissions for the retry.
- Team Mode fresh-install diagnostics now log the resolved `team_mode` config and tool-registry team tool count, making #3893-style missing `team_*` registrations visible instead of silent.
- Added a regression test proving a fresh minimal user config with `{ "team_mode": { "enabled": true } }` registers all 12 `team_*` tools.
- Atlas boulder continuation now hard-stalls after three consecutive continuation turns with no successful bash/edit/write tool progress, preventing the #3446 runaway loop where text-only blocker reports kept the session alive for hours.
- Strengthened the boulder continuation prompt so externally blocked tasks must be marked in the plan as `- [~]` via an actual file edit before Atlas moves on.
### Documentation
- Marked the v4.2.0 BLOCKER-4 known issue as resolved in v4.2.1.
## [4.2.0] - 2026-05-15
### Added
+124
View File
@@ -0,0 +1,124 @@
import { describe, expect, test } from "bun:test"
import {
recordToolProgress,
resetStallStateForPlanChange,
shouldAbortForNoToolProgress,
updateNoToolProgressIterations,
markContinuationInjectedAwaitingToolProgress,
markContinuationStalled,
MAX_BOULDER_CONTINUATION_NO_TOOL_PROGRESS,
} from "./tool-progress"
import type { SessionState } from "./types"
function emptyState(): SessionState {
return { promptFailureCount: 0 }
}
describe("#given a fresh session state", () => {
describe("#when resetStallStateForPlanChange is called the first time", () => {
test("#then it records the active plan path without touching counters", () => {
// given
const state = emptyState()
// when
resetStallStateForPlanChange(state, "/plans/a.md")
// then
expect(state.activeContinuationPlanPath).toBe("/plans/a.md")
expect(state.iterationsSinceLastToolProgress).toBeUndefined()
expect(state.awaitingToolProgressAfterContinuation).toBeUndefined()
expect(state.stalledContinuationReason).toBeUndefined()
})
})
})
describe("#given a session that already accumulated no-tool-progress for plan A", () => {
describe("#when the active plan switches to plan B before the stall threshold is hit", () => {
test("#then iterations and awaiting state reset so plan B gets a fresh budget", () => {
// given - plan A starts and the agent racks up 2 no-progress iterations without stalling yet
const state = emptyState()
resetStallStateForPlanChange(state, "/plans/a.md")
markContinuationInjectedAwaitingToolProgress(state)
updateNoToolProgressIterations(state)
markContinuationInjectedAwaitingToolProgress(state)
updateNoToolProgressIterations(state)
expect(state.iterationsSinceLastToolProgress).toBe(2)
// when - active plan switches to plan B
resetStallStateForPlanChange(state, "/plans/b.md")
// then - plan B inherits a clean counter and is NOT one idle away from a false stall
expect(state.activeContinuationPlanPath).toBe("/plans/b.md")
expect(state.iterationsSinceLastToolProgress).toBe(0)
expect(state.awaitingToolProgressAfterContinuation).toBe(false)
expect(shouldAbortForNoToolProgress(state)).toBe(false)
})
})
describe("#when the active plan stays the same", () => {
test("#then counters are preserved across the reset call", () => {
// given
const state = emptyState()
resetStallStateForPlanChange(state, "/plans/a.md")
markContinuationInjectedAwaitingToolProgress(state)
updateNoToolProgressIterations(state)
// when
resetStallStateForPlanChange(state, "/plans/a.md")
// then
expect(state.iterationsSinceLastToolProgress).toBe(1)
expect(state.activeContinuationPlanPath).toBe("/plans/a.md")
})
})
})
describe("#given a session that already stalled on plan A", () => {
describe("#when the active plan switches to a different plan B", () => {
test("#then both the stall state and the in-progress counter clear for the new plan", () => {
// given - plan A reached the stall threshold and got marked stalled
const state = emptyState()
resetStallStateForPlanChange(state, "/plans/a.md")
for (let i = 0; i < MAX_BOULDER_CONTINUATION_NO_TOOL_PROGRESS; i += 1) {
markContinuationInjectedAwaitingToolProgress(state)
updateNoToolProgressIterations(state)
}
markContinuationStalled(state, "a", "/plans/a.md")
expect(shouldAbortForNoToolProgress(state)).toBe(true)
expect(state.stalledContinuationReason).toBeDefined()
// when
resetStallStateForPlanChange(state, "/plans/b.md")
// then
expect(state.activeContinuationPlanPath).toBe("/plans/b.md")
expect(state.stalledContinuationReason).toBeUndefined()
expect(state.stalledContinuationPlanPath).toBeUndefined()
expect(state.iterationsSinceLastToolProgress).toBe(0)
expect(state.awaitingToolProgressAfterContinuation).toBe(false)
expect(shouldAbortForNoToolProgress(state)).toBe(false)
})
})
})
describe("#given a session running on plan A with tool progress", () => {
describe("#when recordToolProgress fires", () => {
test("#then counters clear but the activeContinuationPlanPath is preserved", () => {
// given
const state = emptyState()
resetStallStateForPlanChange(state, "/plans/a.md")
markContinuationInjectedAwaitingToolProgress(state)
updateNoToolProgressIterations(state)
// when
recordToolProgress(state, 1000)
// then
expect(state.iterationsSinceLastToolProgress).toBe(0)
expect(state.awaitingToolProgressAfterContinuation).toBe(false)
expect(state.lastToolProgressAt).toBe(1000)
expect(state.activeContinuationPlanPath).toBe("/plans/a.md")
})
})
})
+22 -1
View File
@@ -31,6 +31,26 @@ export function recordToolProgress(state: SessionState, now = Date.now()): void
state.iterationsSinceLastToolProgress = 0
state.lastToolProgressAt = now
state.stalledContinuationReason = undefined
state.stalledContinuationPlanPath = undefined
}
export function resetStallStateForPlanChange(state: SessionState, planPath: string): void {
const previousPlanPath = state.activeContinuationPlanPath
if (previousPlanPath === undefined) {
state.activeContinuationPlanPath = planPath
return
}
if (previousPlanPath === planPath) {
return
}
state.activeContinuationPlanPath = planPath
state.iterationsSinceLastToolProgress = 0
state.awaitingToolProgressAfterContinuation = false
if (state.stalledContinuationReason && state.stalledContinuationPlanPath !== planPath) {
state.stalledContinuationReason = undefined
state.stalledContinuationPlanPath = undefined
}
}
export function markContinuationInjectedAwaitingToolProgress(state: SessionState): void {
@@ -51,6 +71,7 @@ export function shouldAbortForNoToolProgress(state: SessionState): boolean {
return (state.iterationsSinceLastToolProgress ?? 0) >= MAX_BOULDER_CONTINUATION_NO_TOOL_PROGRESS
}
export function markContinuationStalled(state: SessionState, planName: string): void {
export function markContinuationStalled(state: SessionState, planName: string, planPath: string): void {
state.stalledContinuationReason = `Boulder continuation stalled for plan "${planName}": ${MAX_BOULDER_CONTINUATION_NO_TOOL_PROGRESS} consecutive continuation iterations produced no successful bash/edit/write tool progress.`
state.stalledContinuationPlanPath = planPath
}
+3
View File
@@ -53,4 +53,7 @@ export interface SessionState {
iterationsSinceLastToolProgress?: number
lastToolProgressAt?: number
stalledContinuationReason?: string
stalledContinuationPlanPath?: string
/** The plan path the in-progress no-tool-progress counter is keyed to. Changes here reset the counter. */
activeContinuationPlanPath?: string
}