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:
+2
-1
@@ -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
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user