Skip to content

Commit bf485dc

Browse files
fix(workspace): pick the newest user request by id, not by position
After a compaction that keeps a tail, the loop's history is not in id order (`filterCompacted` places the summary after the retained tail), so a positional scan could pick the retained request and skip a message queued during the compaction. Same rule as `MessageV2.latest`. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01HRUhVhg6XwxSKJs1iTufiA
1 parent 7ff8985 commit bf485dc

3 files changed

Lines changed: 27 additions & 5 deletions

File tree

‎packages/opencode/src/altimate/session/user-request.ts‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,14 @@ export function requestID(message: MessageV2.WithParts): string | undefined {
2525
return String(original?.metadata?.learnOriginalMessage ?? message.info.id)
2626
}
2727

28-
/** The request of the newest message that carries one. */
28+
/** The request of the newest message that carries one. Newest by id, not by position, like `MessageV2.latest`:
29+
* ids are chronological, and the loop's history is not in id order after a compaction that keeps a tail
30+
* (`filterCompacted` places the summary after the retained tail). */
2931
export function latestRequestID(messages: MessageV2.WithParts[]): string | undefined {
30-
for (let i = messages.length - 1; i >= 0; i--) {
31-
const id = requestID(messages[i])
32-
if (id !== undefined) return id
32+
let newest: { id: string; request: string } | undefined
33+
for (const message of messages) {
34+
const request = requestID(message)
35+
if (request !== undefined && (!newest || message.info.id > newest.id)) newest = { id: message.info.id, request }
3336
}
34-
return undefined
37+
return newest?.request
3538
}

‎packages/opencode/test/altimate/session/user-request.test.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ describe("latestRequestID", () => {
7676
expect(latestRequestID(history)).toBe(queued.info.id)
7777
})
7878

79+
test("is the newest by id, not by position: a compaction that keeps a tail reorders history", () => {
80+
const retained = message("user", [text("first")])
81+
const queued = message("user", [text("sent during compaction")])
82+
// `filterCompacted` places the summary after the retained tail, so the older request comes last.
83+
expect(latestRequestID([queued, message("assistant", [text("summary")]), retained])).toBe(queued.info.id)
84+
})
85+
7986
test("is undefined when no message carries one", () => {
8087
expect(latestRequestID([message("user", [{ type: "compaction", auto: true }])])).toBeUndefined()
8188
expect(latestRequestID([])).toBeUndefined()

‎packages/opencode/test/altimate/workspace/turn-boundary.test.ts‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,18 @@ describe("workspace turn boundary", () => {
7171
expect(catalogs([a], h2)).toEqual([true, true])
7272
})
7373

74+
test("runs for a message queued during a compaction that keeps a tail, which reorders history", () => {
75+
// The trace from review: request A is catalogued; compaction starts and keeps A as its tail; B
76+
// is queued after the marker, before the summary is saved; compaction writes its continuation.
77+
// `filterCompacted` returns [marker, B, summary, A, continuation]: A last by position, B newest by id.
78+
const a = asked("first")
79+
const m = marker()
80+
const b = asked("sent during compaction")
81+
const summary = reply()
82+
const continuation = loopWrote("Continue if you have next steps")
83+
expect(catalogs([a], [m, b, summary, a, continuation])).toEqual([true, true])
84+
})
85+
7486
test("does not run again for a stamped replay of an earlier request", () => {
7587
const a = asked("first")
7688
const b = asked("second")

0 commit comments

Comments
 (0)