Skip to content

Commit 91d488c

Browse files
committed
feat(hooks): add question.asked and session.error notifications
Add notifications for two additional events: - question.asked: Notifies when agent asks a question (Prometheus interview) - session.error: Notifies when session encounters an error (e.g., thinking block errors) Both handlers follow existing patterns: - Skip subagent sessions - Only notify for main session - Immediate notification (no delay) Includes comprehensive test coverage for both event types.
1 parent 3ed1c66 commit 91d488c

File tree

2 files changed

+203
-0
lines changed

2 files changed

+203
-0
lines changed

src/hooks/session-notification.test.ts

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,4 +358,157 @@ describe("session-notification", () => {
358358
// #then - only one notification should be sent
359359
expect(notificationCalls).toHaveLength(1)
360360
})
361+
362+
test("should trigger notification immediately on question.asked event for main session", async () => {
363+
// #given - main session is set
364+
const mainSessionID = "main-question"
365+
setMainSession(mainSessionID)
366+
367+
const hook = createSessionNotification(createMockPluginInput(), {})
368+
369+
// #when - question.asked event fires
370+
await hook({
371+
event: {
372+
type: "question.asked",
373+
properties: {
374+
sessionID: mainSessionID,
375+
questions: [{ header: "Test", question: "Test question?" }],
376+
},
377+
},
378+
})
379+
380+
// #then - notification should be sent immediately
381+
expect(notificationCalls.length).toBeGreaterThanOrEqual(1)
382+
expect(notificationCalls[0]).toContain("Question")
383+
})
384+
385+
test("should not trigger question.asked notification for subagent session", async () => {
386+
// #given - a subagent session exists
387+
const subagentSessionID = "subagent-question"
388+
subagentSessions.add(subagentSessionID)
389+
390+
const hook = createSessionNotification(createMockPluginInput(), {})
391+
392+
// #when - question.asked event fires for subagent
393+
await hook({
394+
event: {
395+
type: "question.asked",
396+
properties: { sessionID: subagentSessionID },
397+
},
398+
})
399+
400+
// #then - notification should NOT be sent
401+
expect(notificationCalls).toHaveLength(0)
402+
})
403+
404+
test("should not trigger question.asked notification for non-main session", async () => {
405+
// #given - main session is set, but different session asks question
406+
const mainSessionID = "main-q"
407+
const otherSessionID = "other-q"
408+
setMainSession(mainSessionID)
409+
410+
const hook = createSessionNotification(createMockPluginInput(), {})
411+
412+
// #when - question.asked event fires for non-main session
413+
await hook({
414+
event: {
415+
type: "question.asked",
416+
properties: { sessionID: otherSessionID },
417+
},
418+
})
419+
420+
// #then - notification should NOT be sent
421+
expect(notificationCalls).toHaveLength(0)
422+
})
423+
424+
test("should trigger notification on session.error event for main session", async () => {
425+
// #given - main session is set
426+
const mainSessionID = "main-error"
427+
setMainSession(mainSessionID)
428+
429+
const hook = createSessionNotification(createMockPluginInput(), {})
430+
431+
// #when - session.error event fires
432+
await hook({
433+
event: {
434+
type: "session.error",
435+
properties: {
436+
sessionID: mainSessionID,
437+
error: { message: "The final block in an assistant message cannot be thinking" },
438+
},
439+
},
440+
})
441+
442+
// #then - notification should be sent immediately with error message
443+
expect(notificationCalls.length).toBeGreaterThanOrEqual(1)
444+
expect(notificationCalls[0]).toContain("Error")
445+
})
446+
447+
test("should not trigger session.error notification for subagent session", async () => {
448+
// #given - a subagent session exists
449+
const subagentSessionID = "subagent-error"
450+
subagentSessions.add(subagentSessionID)
451+
452+
const hook = createSessionNotification(createMockPluginInput(), {})
453+
454+
// #when - session.error event fires for subagent
455+
await hook({
456+
event: {
457+
type: "session.error",
458+
properties: {
459+
sessionID: subagentSessionID,
460+
error: { message: "Some error" },
461+
},
462+
},
463+
})
464+
465+
// #then - notification should NOT be sent
466+
expect(notificationCalls).toHaveLength(0)
467+
})
468+
469+
test("should not trigger session.error notification for non-main session", async () => {
470+
// #given - main session is set, but different session has error
471+
const mainSessionID = "main-e"
472+
const otherSessionID = "other-e"
473+
setMainSession(mainSessionID)
474+
475+
const hook = createSessionNotification(createMockPluginInput(), {})
476+
477+
// #when - session.error event fires for non-main session
478+
await hook({
479+
event: {
480+
type: "session.error",
481+
properties: {
482+
sessionID: otherSessionID,
483+
error: { message: "Some error" },
484+
},
485+
},
486+
})
487+
488+
// #then - notification should NOT be sent
489+
expect(notificationCalls).toHaveLength(0)
490+
})
491+
492+
test("should handle session.error with no error message", async () => {
493+
// #given - main session is set
494+
const mainSessionID = "main-error-no-msg"
495+
setMainSession(mainSessionID)
496+
497+
const hook = createSessionNotification(createMockPluginInput(), {})
498+
499+
// #when - session.error event fires without error message
500+
await hook({
501+
event: {
502+
type: "session.error",
503+
properties: {
504+
sessionID: mainSessionID,
505+
error: {},
506+
},
507+
},
508+
})
509+
510+
// #then - notification should be sent with default message
511+
expect(notificationCalls.length).toBeGreaterThanOrEqual(1)
512+
expect(notificationCalls[0]).toContain("Error")
513+
})
361514
})

src/hooks/session-notification.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,56 @@ export function createSessionNotification(
325325
notificationVersions.delete(sessionInfo.id)
326326
executingNotifications.delete(sessionInfo.id)
327327
}
328+
return
329+
}
330+
331+
if (event.type === "question.asked") {
332+
const sessionID = props?.sessionID as string | undefined
333+
if (!sessionID) return
334+
335+
if (subagentSessions.has(sessionID)) return
336+
337+
const mainSessionID = getMainSessionID()
338+
if (mainSessionID && sessionID !== mainSessionID) return
339+
340+
await sendNotification(
341+
ctx,
342+
currentPlatform,
343+
"OpenCode - Question",
344+
"Agent is waiting for your answer"
345+
)
346+
347+
if (mergedConfig.playSound && mergedConfig.soundPath) {
348+
await playSound(ctx, currentPlatform, mergedConfig.soundPath)
349+
}
350+
return
351+
}
352+
353+
if (event.type === "session.error") {
354+
const sessionID = props?.sessionID as string | undefined
355+
if (!sessionID) return
356+
357+
if (subagentSessions.has(sessionID)) return
358+
359+
const mainSessionID = getMainSessionID()
360+
if (mainSessionID && sessionID !== mainSessionID) return
361+
362+
const error = props?.error as { message?: string } | undefined
363+
const errorMessage = error?.message
364+
? `Error: ${error.message.slice(0, 100)}`
365+
: "Session encountered an error"
366+
367+
await sendNotification(
368+
ctx,
369+
currentPlatform,
370+
"OpenCode - Error",
371+
errorMessage
372+
)
373+
374+
if (mergedConfig.playSound && mergedConfig.soundPath) {
375+
await playSound(ctx, currentPlatform, mergedConfig.soundPath)
376+
}
377+
return
328378
}
329379
}
330380
}

0 commit comments

Comments
 (0)