Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

- Fix goal auto-continue stalling after session compaction: a continuation
claim for a pre-compaction source turn could match the still-visible tail
assistant message after compaction and suppress the post-compaction
continuation until the user nudged the goal. The claim is now invalidated on
`session.compacted`, so the loop resumes on the next idle.

## 0.8.0 — 2026-08-06

Both new options in this release were contributed by
Expand Down
6 changes: 6 additions & 0 deletions src/goal-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -5331,6 +5331,12 @@ async function createGoalPlugin({ client, directory } = {}, pluginOptions = {})
if (!goal) return
goal.messageIDs = new Set()
goal.totalTokens = 0
// Compaction rewrites the context: a continuation claim for a
// pre-compaction source turn must not suppress the post-compaction
// continuation (the recent tail can still end on the same assistant
// message, which would otherwise stall the goal loop until the user
// nudges it).
goal.continuationClaim = null
await persist(sessionID)
return
}
Expand Down
43 changes: 43 additions & 0 deletions test/goal-plugin.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4363,6 +4363,49 @@ test("totalTokens resets to zero after session compaction", async () => {
assert.equal(currentGoal(session).totalTokens, 45_000, "post-compaction tokens accumulate from zero")
})

test("auto-continue fires after compaction despite a pre-compaction continuation claim", async () => {
const calls = []
const client = {
app: { log: async () => {} },
session: {
messages: async () => ({
data: [pluginContinuationMessage(), message("did a step")],
}),
promptAsync: async (input) => {
calls.push(input)
return {}
},
},
}
const hooks = await GoalPlugin(
{ client },
{ persistState: false, minDelayMs: 1, noToolCallTurnsBeforePause: 0 },
)
await hooks["command.execute.before"](
{ command: "goal", sessionID: "session-1", arguments: "ship it" },
{ parts: [] },
)
const goal = currentGoal("session-1")
goal.turnCount = 1
goal.lastContinueAt = Date.now() - 10
// A continuation was claimed for the latest assistant turn right before the
// compaction interrupted it; after compaction the recent tail still ends on
// that same assistant message, so the stale claim must not suppress the
// post-compaction continuation.
goal.continuationClaim = { runId: goal.runId, sourceAssistantMessageID: "msg-assistant" }

await hooks.event({
event: { type: "session.compacted", properties: { sessionID: "session-1" } },
})

await hooks.event({
event: { type: "session.status", properties: { sessionID: "session-1", status: { type: "idle" } } },
})

assert.equal(calls.length, 1)
assert.equal(currentGoal("session-1").stopped, false)
})

test("parses --max-duration-ms flag directly", () => {
const parsed = parseGoalArguments("fix tests --max-duration-ms 90000", normalizeOptions())
assert.equal(parsed.condition, "fix tests")
Expand Down