Skip to content

Commit e2b8aee

Browse files
authored
fix(todo-continuation-enforcer): use current selected model when continuing todos (#138) (#151)
- Prioritize messages with model info when finding nearest message for todo continuation - Pass resolved model to session.prompt call to use previously selected model - Add comprehensive test cases for findNearestMessageWithFields logic 🤖 Generated with assistance of oh-my-opencode
1 parent d7bc817 commit e2b8aee

File tree

3 files changed

+177
-2
lines changed

3 files changed

+177
-2
lines changed
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { describe, test, expect, beforeEach, afterEach } from "bun:test"
2+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
3+
import { join } from "node:path"
4+
import { findNearestMessageWithFields } from "./injector"
5+
6+
const TEST_DIR = "/tmp/test-hook-message-injector"
7+
8+
describe("findNearestMessageWithFields", () => {
9+
beforeEach(() => {
10+
if (existsSync(TEST_DIR)) {
11+
rmSync(TEST_DIR, { recursive: true })
12+
}
13+
mkdirSync(TEST_DIR, { recursive: true })
14+
})
15+
16+
afterEach(() => {
17+
if (existsSync(TEST_DIR)) {
18+
rmSync(TEST_DIR, { recursive: true })
19+
}
20+
})
21+
22+
test("returns message with model info when available", () => {
23+
// #given
24+
const messageWithModel = {
25+
id: "msg_001",
26+
agent: "Sisyphus",
27+
model: { providerID: "openai", modelID: "gpt-5.2" },
28+
tools: { write: true },
29+
}
30+
writeFileSync(join(TEST_DIR, "msg_001.json"), JSON.stringify(messageWithModel))
31+
32+
// #when
33+
const result = findNearestMessageWithFields(TEST_DIR)
34+
35+
// #then
36+
expect(result).not.toBeNull()
37+
expect(result?.agent).toBe("Sisyphus")
38+
expect(result?.model?.providerID).toBe("openai")
39+
expect(result?.model?.modelID).toBe("gpt-5.2")
40+
})
41+
42+
test("returns most recent message with model info", () => {
43+
// #given
44+
const olderMessage = {
45+
id: "msg_001",
46+
agent: "Sisyphus",
47+
model: { providerID: "anthropic", modelID: "claude-opus-4-5" },
48+
}
49+
const newerMessage = {
50+
id: "msg_002",
51+
agent: "oracle",
52+
model: { providerID: "openai", modelID: "gpt-5.2" },
53+
}
54+
writeFileSync(join(TEST_DIR, "msg_001.json"), JSON.stringify(olderMessage))
55+
writeFileSync(join(TEST_DIR, "msg_002.json"), JSON.stringify(newerMessage))
56+
57+
// #when
58+
const result = findNearestMessageWithFields(TEST_DIR)
59+
60+
// #then
61+
expect(result?.agent).toBe("oracle")
62+
expect(result?.model?.providerID).toBe("openai")
63+
expect(result?.model?.modelID).toBe("gpt-5.2")
64+
})
65+
66+
test("skips messages without complete model info", () => {
67+
// #given
68+
const incompleteMessage = {
69+
id: "msg_002",
70+
agent: "explore",
71+
model: { providerID: "openai" },
72+
}
73+
const completeMessage = {
74+
id: "msg_001",
75+
agent: "Sisyphus",
76+
model: { providerID: "anthropic", modelID: "claude-opus-4-5" },
77+
}
78+
writeFileSync(join(TEST_DIR, "msg_001.json"), JSON.stringify(completeMessage))
79+
writeFileSync(join(TEST_DIR, "msg_002.json"), JSON.stringify(incompleteMessage))
80+
81+
// #when
82+
const result = findNearestMessageWithFields(TEST_DIR)
83+
84+
// #then
85+
expect(result?.agent).toBe("Sisyphus")
86+
expect(result?.model?.providerID).toBe("anthropic")
87+
expect(result?.model?.modelID).toBe("claude-opus-4-5")
88+
})
89+
90+
test("falls back to message with agent only when no model info exists", () => {
91+
// #given
92+
const agentOnlyMessage = {
93+
id: "msg_001",
94+
agent: "librarian",
95+
}
96+
writeFileSync(join(TEST_DIR, "msg_001.json"), JSON.stringify(agentOnlyMessage))
97+
98+
// #when
99+
const result = findNearestMessageWithFields(TEST_DIR)
100+
101+
// #then
102+
expect(result?.agent).toBe("librarian")
103+
expect(result?.model).toBeUndefined()
104+
})
105+
106+
test("returns null for empty directory", () => {
107+
// #given - empty directory (already created in beforeEach)
108+
109+
// #when
110+
const result = findNearestMessageWithFields(TEST_DIR)
111+
112+
// #then
113+
expect(result).toBeNull()
114+
})
115+
116+
test("returns null for non-existent directory", () => {
117+
// #given
118+
const nonExistentDir = "/tmp/non-existent-test-dir-12345"
119+
120+
// #when
121+
const result = findNearestMessageWithFields(nonExistentDir)
122+
123+
// #then
124+
expect(result).toBeNull()
125+
})
126+
127+
test("preserves tools field from stored message", () => {
128+
// #given
129+
const messageWithTools = {
130+
id: "msg_001",
131+
agent: "frontend-ui-ux-engineer",
132+
model: { providerID: "google", modelID: "gemini-3-pro-preview" },
133+
tools: { write: true, edit: true, bash: false },
134+
}
135+
writeFileSync(join(TEST_DIR, "msg_001.json"), JSON.stringify(messageWithTools))
136+
137+
// #when
138+
const result = findNearestMessageWithFields(TEST_DIR)
139+
140+
// #then
141+
expect(result?.tools).toEqual({ write: true, edit: true, bash: false })
142+
})
143+
144+
test("handles malformed JSON files gracefully", () => {
145+
// #given
146+
const validMessage = {
147+
id: "msg_001",
148+
agent: "Sisyphus",
149+
model: { providerID: "openai", modelID: "gpt-5.2" },
150+
}
151+
writeFileSync(join(TEST_DIR, "msg_001.json"), JSON.stringify(validMessage))
152+
writeFileSync(join(TEST_DIR, "msg_002.json"), "{ invalid json }")
153+
154+
// #when
155+
const result = findNearestMessageWithFields(TEST_DIR)
156+
157+
// #then
158+
expect(result?.agent).toBe("Sisyphus")
159+
})
160+
})

src/features/hook-message-injector/injector.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,19 @@ export function findNearestMessageWithFields(messageDir: string): StoredMessage
2020
try {
2121
const content = readFileSync(join(messageDir, file), "utf-8")
2222
const msg = JSON.parse(content) as StoredMessage
23-
if (msg.agent && msg.model?.providerID && msg.model?.modelID) {
23+
if (msg.model?.providerID && msg.model?.modelID) {
24+
return msg
25+
}
26+
} catch {
27+
continue
28+
}
29+
}
30+
31+
for (const file of files) {
32+
try {
33+
const content = readFileSync(join(messageDir, file), "utf-8")
34+
const msg = JSON.parse(content) as StoredMessage
35+
if (msg.agent) {
2436
return msg
2537
}
2638
} catch {

src/hooks/todo-continuation-enforcer.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,11 +218,14 @@ export function createTodoContinuationEnforcer(ctx: PluginInput): TodoContinuati
218218
return
219219
}
220220

221-
log(`[${HOOK_NAME}] Injecting continuation prompt`, { sessionID, agent: prevMessage?.agent })
221+
log(`[${HOOK_NAME}] Injecting continuation prompt`, { sessionID, agent: prevMessage?.agent, model: prevMessage?.model })
222222
await ctx.client.session.prompt({
223223
path: { id: sessionID },
224224
body: {
225225
agent: prevMessage?.agent,
226+
model: prevMessage?.model?.providerID && prevMessage?.model?.modelID
227+
? { providerID: prevMessage.model.providerID, modelID: prevMessage.model.modelID }
228+
: undefined,
226229
parts: [
227230
{
228231
type: "text",

0 commit comments

Comments
 (0)