Files
oh-my-opencode/src/hooks/team-session-events/team-idle-wake-hint.ts
T
2026-05-15 22:49:10 +09:00

154 lines
5.3 KiB
TypeScript

import type { TeamModeConfig } from "../../config/schema/team-mode"
import { findResolvedMemberSession } from "../../features/team-mode/member-session-resolution"
import {
applyMemberSessionRouting,
buildMemberPromptBody,
} from "../../features/team-mode/member-session-routing"
import { ackMessages } from "../../features/team-mode/team-mailbox/ack"
import { listUnreadMessages } from "../../features/team-mode/team-mailbox/inbox"
import { loadRuntimeState, transitionRuntimeState } from "../../features/team-mode/team-state-store/store"
import { resolveSessionEventID } from "../../shared/event-session-id"
import { log } from "../../shared/logger"
import { promptAsyncAfterSessionIdle } from "../shared/prompt-async-gate"
type PromptAsyncInput = {
path: { id: string }
body: {
parts: Array<{ type: "text"; text: string }>
agent?: string
model?: { providerID: string; modelID: string }
variant?: string
}
query: { directory: string }
}
type TeamIdleWakeHintContext = {
directory: string
client: {
session: {
promptAsync?: (input: PromptAsyncInput) => Promise<unknown>
status?: () => Promise<unknown>
}
}
}
type HookInput = { event: { type: string; properties?: unknown } }
export type HookImpl = (input: HookInput) => Promise<void>
type TeamIdleWakeHintOptions = { idleSettleMs?: number }
function getIdleSessionID(properties: unknown): string | undefined {
return resolveSessionEventID(properties)
}
function buildWakeHint(unreadCount: number): string {
return `You have ${unreadCount} new team messages. They will be injected on your next turn.`
}
export function createTeamIdleWakeHint(ctx: TeamIdleWakeHintContext, config: TeamModeConfig, options?: TeamIdleWakeHintOptions): HookImpl {
return async ({ event }: HookInput): Promise<void> => {
if (event.type !== "session.idle") return
const sessionID = getIdleSessionID(event.properties)
if (!sessionID) return
try {
const runtimeMember = await findResolvedMemberSession(sessionID, config, "team idle wake hint")
if (runtimeMember === null) {
return
}
const runtimeState = await loadRuntimeState(runtimeMember.teamRunId, config)
const memberEntry = runtimeState.members.find((member) => member.name === runtimeMember.memberName)
if (!memberEntry) {
return
}
const pendingInjectedMessageIds = [...memberEntry.pendingInjectedMessageIds]
if (pendingInjectedMessageIds.length > 0) {
await ackMessages(runtimeState.teamRunId, memberEntry.name, pendingInjectedMessageIds, config)
await transitionRuntimeState(runtimeState.teamRunId, (currentRuntimeState) => ({
...currentRuntimeState,
members: currentRuntimeState.members.map((member) => (
member.name === memberEntry.name
? { ...member, pendingInjectedMessageIds: [] }
: member
)),
}), config)
}
const unreadMessages = await listUnreadMessages(runtimeState.teamRunId, memberEntry.name, config)
if (unreadMessages.length === 0) {
log("team idle handled without wake hint", {
event: "team-mode-idle-ack-only",
teamRunId: runtimeState.teamRunId,
memberName: memberEntry.name,
sessionID,
ackedCount: pendingInjectedMessageIds.length,
})
return
}
if (memberEntry.agentType === "leader") {
log("team lead idle handled without wake hint", {
event: "team-mode-lead-idle-ack-only",
teamRunId: runtimeState.teamRunId,
memberName: memberEntry.name,
sessionID,
ackedCount: pendingInjectedMessageIds.length,
})
return
}
if (typeof ctx.client.session.promptAsync !== "function") {
log("team idle wake hint skipped without promptAsync", {
event: "team-mode-idle-wake-hint-skipped",
teamRunId: runtimeState.teamRunId,
memberName: memberEntry.name,
sessionID,
unreadCount: unreadMessages.length,
})
return
}
applyMemberSessionRouting(sessionID, memberEntry)
const promptResult = await promptAsyncAfterSessionIdle({
client: ctx.client,
sessionID,
source: "team-idle-wake-hint",
settleMs: options?.idleSettleMs,
input: {
path: { id: sessionID },
body: buildMemberPromptBody(memberEntry, buildWakeHint(unreadMessages.length)),
query: { directory: ctx.directory },
},
})
if (promptResult.status !== "dispatched") {
log("team idle wake hint skipped by promptAsync gate", {
event: "team-mode-idle-wake-hint-gated",
teamRunId: runtimeState.teamRunId,
memberName: memberEntry.name,
sessionID,
unreadCount: unreadMessages.length,
status: promptResult.status,
})
return
}
log("team idle wake hint sent", {
event: "team-mode-idle-wake-hint",
teamRunId: runtimeState.teamRunId,
memberName: memberEntry.name,
sessionID,
unreadCount: unreadMessages.length,
ackedCount: pendingInjectedMessageIds.length,
})
} catch (error) {
log("team idle wake hint failed", {
event: "team-mode-idle-wake-hint-error",
sessionID,
error: error instanceof Error ? error.message : String(error),
})
}
}
}