fix(opencode): order legacy message loop by time (#40990)

Co-authored-by: Dax <mail@thdxr.com>
This commit is contained in:
opencode-agent[bot]
2026-08-07 04:08:23 +00:00
committed by GitHub
parent a54a693af2
commit db581e47a3
4 changed files with 120 additions and 10 deletions
@@ -1611,6 +1611,44 @@ describe("session.message-v2.latest", () => {
] as SessionV1.Part[],
}
test("selects latest messages by creation time when IDs are nonmonotonic", () => {
const oldUser = { ...userInfo("msg_z_user"), time: { created: 100 } }
const newUser = { ...userInfo("msg_a_user"), time: { created: 200 } }
const oldAssistant = {
...assistantInfo("msg_z_assistant", oldUser.id),
time: { created: 300 },
finish: "stop",
} as SessionV1.Assistant
const newAssistant = {
...assistantInfo("msg_a_assistant", newUser.id),
time: { created: 400 },
finish: "stop",
} as SessionV1.Assistant
const state = MessageV2.latest([
{ info: newAssistant, parts: [] },
{ info: oldUser, parts: [] },
{ info: oldAssistant, parts: [] },
{ info: newUser, parts: [] },
])
expect(state.user?.id).toBe(newUser.id)
expect(state.assistant?.id).toBe(newAssistant.id)
expect(state.finished?.id).toBe(newAssistant.id)
})
test("uses ID as a deterministic tie-breaker for equal creation times", () => {
const lower = { ...userInfo("msg_a_user"), time: { created: 100 } }
const higher = { ...userInfo("msg_z_user"), time: { created: 100 } }
const state = MessageV2.latest([
{ info: higher, parts: [] },
{ info: lower, parts: [] },
])
expect(state.user?.id).toBe(higher.id)
})
// Regression for double auto-compaction. The reorder in filterCompacted
// (#27145) returns [compaction-user, summary, ...tail..., continue-user],
// so picking lastFinished by array position landed on the pre-compaction
@@ -1659,4 +1697,33 @@ describe("session.message-v2.latest", () => {
expect(state.tasks).toHaveLength(1)
expect(state.tasks[0]).toMatchObject({ type: "compaction", auto: true })
})
test("selects compaction and subtask work after the finished boundary by creation time", () => {
const finished = {
...assistantInfo("msg_z_finished", "msg_parent"),
time: { created: 200 },
finish: "stop",
} as SessionV1.Assistant
const oldTask: SessionV1.WithParts = {
info: { ...userInfo("msg_z_old"), time: { created: 100 } },
parts: [{ ...basePart("msg_z_old", "old"), type: "compaction", auto: true }] as SessionV1.Part[],
}
const newTask: SessionV1.WithParts = {
info: { ...userInfo("msg_a_new"), time: { created: 300 } },
parts: [
{
...basePart("msg_a_new", "new"),
type: "subtask",
prompt: "inspect",
description: "inspect ordering",
agent: "general",
},
] as SessionV1.Part[],
}
const state = MessageV2.latest([newTask, { info: finished, parts: [] }, oldTask])
expect(state.tasks).toHaveLength(1)
expect(state.tasks[0]).toMatchObject({ type: "subtask", prompt: "inspect" })
})
})
@@ -460,6 +460,46 @@ noLLMServer.instance(
{ config: cfg },
)
noLLMServer.instance(
"loop exits for a completed parent turn with nonmonotonic message IDs",
() =>
Effect.gen(function* () {
const prompt = yield* SessionPrompt.Service
const sessions = yield* Session.Service
const chat = yield* sessions.create({ title: "Pinned" })
const userID = MessageID.make("msg_z_user")
const assistantID = MessageID.make("msg_a_assistant")
yield* sessions.updateMessage({
id: userID,
role: "user",
sessionID: chat.id,
agent: "build",
model: ref,
time: { created: 100 },
})
yield* sessions.updateMessage({
id: assistantID,
role: "assistant",
parentID: userID,
sessionID: chat.id,
mode: "build",
agent: "build",
cost: 0,
path: { cwd: "/tmp", root: "/tmp" },
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
modelID: ref.modelID,
providerID: ref.providerID,
time: { created: 200, completed: 201 },
finish: "stop",
})
const result = yield* prompt.loop({ sessionID: chat.id })
expect(result.info.id).toBe(assistantID)
}),
{ config: cfg },
)
it.instance("loop exits without an LLM request for interrupted orphan tool calls", () =>
Effect.gen(function* () {
const { llm } = yield* useServerConfig(providerCfg)