fix(hooks): extract model from assistant messages with flat modelID/providerID

OpenCode API returns different structures for user vs assistant messages:
- User: info.model = { providerID, modelID } (nested)
- Assistant: info.modelID, info.providerID (flat top-level)

Previous code only checked nested format, causing model info loss when
continuation hooks fired after assistant messages.

Files modified:
- todo-continuation-enforcer.ts
- ralph-loop/index.ts
- sisyphus-task/tools.ts
- background-agent/manager.ts

Added test for assistant message model extraction.
This commit is contained in:
justsisyphus
2026-01-16 13:54:22 +09:00
parent 1d47d54a51
commit 227cc52f20
5 changed files with 59 additions and 11 deletions
@@ -829,4 +829,50 @@ describe("todo-continuation-enforcer", () => {
expect(promptCalls[0].text).toContain("TODO CONTINUATION")
expect("model" in promptCalls[0]).toBe(true)
})
test("should extract model from assistant message with flat modelID/providerID", async () => {
// #given - session with assistant message that has flat modelID/providerID (OpenCode API format)
const sessionID = "main-assistant-model"
setMainSession(sessionID)
// OpenCode returns assistant messages with flat modelID/providerID, not nested model object
const mockMessagesWithAssistant = [
{ info: { id: "msg-1", role: "user", agent: "Sisyphus", model: { providerID: "openai", modelID: "gpt-5.2" } } },
{ info: { id: "msg-2", role: "assistant", agent: "Sisyphus", modelID: "gpt-5.2", providerID: "openai" } },
]
const mockInput = {
client: {
session: {
todo: async () => ({
data: [{ id: "1", content: "Task 1", status: "pending", priority: "high" }],
}),
messages: async () => ({ data: mockMessagesWithAssistant }),
prompt: async (opts: any) => {
promptCalls.push({
sessionID: opts.path.id,
agent: opts.body.agent,
model: opts.body.model,
text: opts.body.parts[0].text,
})
return {}
},
},
tui: { showToast: async () => ({}) },
},
directory: "/tmp/test",
} as any
const hook = createTodoContinuationEnforcer(mockInput, {
backgroundManager: createMockBackgroundManager(false),
})
// #when - session goes idle
await hook.handler({ event: { type: "session.idle", properties: { sessionID } } })
await new Promise(r => setTimeout(r, 2500))
// #then - model should be extracted from assistant message's flat modelID/providerID
expect(promptCalls.length).toBe(1)
expect(promptCalls[0].model).toEqual({ providerID: "openai", modelID: "gpt-5.2" })
})
})