Skip to content

Commit cc12a2b

Browse files
authored
fix: preserve model for exit notifications (#50)
Co-authored-by: tpob <tpob@tpob.xyz>
1 parent 65eee77 commit cc12a2b

2 files changed

Lines changed: 94 additions & 3 deletions

File tree

‎src/plugin/pty/notification-manager.ts‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,36 @@ export class NotificationManager {
1616

1717
try {
1818
const message = this.buildExitNotification(session, exitCode)
19+
let modelContext: {
20+
model?: { providerID: string; modelID: string }
21+
variant?: string
22+
} = {}
23+
try {
24+
const parent = await this.client.session.get({
25+
path: { id: session.parentSessionId },
26+
})
27+
const model = (
28+
parent.data as
29+
| (typeof parent.data & {
30+
model?: { id: string; providerID: string; variant?: string }
31+
})
32+
| undefined
33+
)?.model
34+
if (model) {
35+
modelContext = {
36+
model: { providerID: model.providerID, modelID: model.id },
37+
...(model.variant ? { variant: model.variant } : {}),
38+
}
39+
}
40+
} catch {
41+
// Older OpenCode versions may not expose the session model.
42+
}
1943
await this.client.session.promptAsync({
2044
path: { id: session.parentSessionId },
2145
body: {
2246
parts: [{ type: 'text', text: message }],
2347
...(session.parentAgent ? { agent: session.parentAgent } : {}),
48+
...modelContext,
2449
},
2550
})
2651
} catch {

‎test/notification-manager.test.ts‎

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ type PromptPayload = {
99
body: {
1010
parts: Array<{ type: string; text: string }>
1111
agent?: string
12+
model?: { providerID: string; modelID: string }
13+
variant?: string
1214
}
1315
}
1416

@@ -38,6 +40,67 @@ function createSession(overrides: Partial<PTYSession> = {}): PTYSession {
3840
}
3941

4042
describe('NotificationManager', () => {
43+
it('preserves the parent session model and variant', async () => {
44+
const get = mock(async () => ({
45+
data: {
46+
model: { providerID: 'openai', id: 'gpt-5.6-terra', variant: 'high' },
47+
},
48+
}))
49+
const promptAsync = mock(async (_payload: PromptPayload) => {})
50+
const manager = new NotificationManager()
51+
52+
manager.init({ session: { get, promptAsync } } as unknown as OpencodeClient)
53+
54+
await manager.sendExitNotification(createSession(), 0)
55+
56+
expect(get).toHaveBeenCalledWith({ path: { id: 'parent-session-id' } })
57+
const payload = promptAsync.mock.calls[0]?.[0]
58+
if (!payload) throw new Error('Expected a prompt payload')
59+
expect(payload.body.model).toEqual({
60+
providerID: 'openai',
61+
modelID: 'gpt-5.6-terra',
62+
})
63+
expect(payload.body.variant).toBe('high')
64+
})
65+
66+
it('preserves the parent model without inventing a variant', async () => {
67+
const get = mock(async () => ({
68+
data: { model: { providerID: 'openai', id: 'gpt-5.6-terra' } },
69+
}))
70+
const promptAsync = mock(async (_payload: PromptPayload) => {})
71+
const manager = new NotificationManager()
72+
73+
manager.init({ session: { get, promptAsync } } as unknown as OpencodeClient)
74+
75+
await manager.sendExitNotification(createSession(), 0)
76+
77+
const payload = promptAsync.mock.calls[0]?.[0]
78+
if (!payload) throw new Error('Expected a prompt payload')
79+
expect(payload.body.model).toEqual({
80+
providerID: 'openai',
81+
modelID: 'gpt-5.6-terra',
82+
})
83+
expect(Object.hasOwn(payload.body, 'variant')).toBe(false)
84+
})
85+
86+
it('sends the notification when reading the parent model fails', async () => {
87+
const get = mock(async () => {
88+
throw new Error('Session API unavailable')
89+
})
90+
const promptAsync = mock(async (_payload: PromptPayload) => {})
91+
const manager = new NotificationManager()
92+
93+
manager.init({ session: { get, promptAsync } } as unknown as OpencodeClient)
94+
95+
await manager.sendExitNotification(createSession(), 0)
96+
97+
expect(promptAsync).toHaveBeenCalledTimes(1)
98+
const payload = promptAsync.mock.calls[0]?.[0]
99+
if (!payload) throw new Error('Expected a prompt payload')
100+
expect(Object.hasOwn(payload.body, 'model')).toBe(false)
101+
expect(Object.hasOwn(payload.body, 'variant')).toBe(false)
102+
})
103+
41104
it('includes body.agent when originating agent is present', async () => {
42105
const promptAsync = mock(async (_payload: PromptPayload) => {})
43106
const manager = new NotificationManager()
@@ -47,7 +110,8 @@ describe('NotificationManager', () => {
47110
await manager.sendExitNotification(createSession({ parentAgent: 'agent-two' }), 0)
48111

49112
expect(promptAsync).toHaveBeenCalledTimes(1)
50-
const payload = promptAsync.mock.calls[0]![0]
113+
const payload = promptAsync.mock.calls[0]?.[0]
114+
if (!payload) throw new Error('Expected a prompt payload')
51115

52116
expect(payload.path).toEqual({ id: 'parent-session-id' })
53117
expect(payload.body.agent).toBe('agent-two')
@@ -65,7 +129,8 @@ describe('NotificationManager', () => {
65129
await manager.sendExitNotification(createSession({ parentAgent: undefined }), 1)
66130

67131
expect(promptAsync).toHaveBeenCalledTimes(1)
68-
const payload = promptAsync.mock.calls[0]![0]
132+
const payload = promptAsync.mock.calls[0]?.[0]
133+
if (!payload) throw new Error('Expected a prompt payload')
69134

70135
expect(payload.path).toEqual({ id: 'parent-session-id' })
71136
expect(Object.hasOwn(payload.body, 'agent')).toBe(false)
@@ -85,7 +150,8 @@ describe('NotificationManager', () => {
85150
await manager.sendExitNotification(createSession({ timeoutSeconds: 2, timedOut: true }), 0)
86151

87152
expect(promptAsync).toHaveBeenCalledTimes(1)
88-
const payload = promptAsync.mock.calls[0]![0]
153+
const payload = promptAsync.mock.calls[0]?.[0]
154+
if (!payload) throw new Error('Expected a prompt payload')
89155
const text = payload.body.parts[0]?.text ?? ''
90156

91157
expect(text).toContain('TimeoutSeconds: 2')

0 commit comments

Comments
 (0)