import type { NotifyPreference } from 'officerdb/types'; import { getChatNotifyPreference } from 'officerdb'; // Whether a chat's ping is worth telling someone about — the DECISION, deliberately apart from any // delivery. // // It lived inside `notify-delivery.ts`, which is the browser socket. That was fine while the browser was // the only listener, and stops being fine the moment phone push subscribes to the same signal: two // copies of this rule drift, and the symptom is a bell and a phone disagreeing about the same // conversation, which is close to impossible to debug from either end. // // So both channels ask here. The rule lives once. export type NotifyKind = 'turn-ended' | 'input-needed'; /** * The rule, pure so it can be tested without a database. * * **`every_turn` is a SUPERSET of `input_needed`, not a sibling.** The two kinds are disjoint at the * source — `turn-ended` fires on `result`/`error`/`stopped`, while `input-needed` fires at a plan gate or * a question gate, which PARK the turn and never emit a result. So matching them one-to-one meant * choosing "every end of turn" delivered nothing when the agent stopped to ask you something: the most * urgent case, silently the quietest, for whoever picked the most inclusive setting. */ export const notifyMatches = (preference: NotifyPreference | null, kind: NotifyKind): boolean => { if (!preference || preference === 'none') return false; if (preference === 'every_turn') return true; return kind === 'input-needed'; }; /** The same rule against stored state. A preference that cannot be read is a no — never a notification. */ export async function shouldNotify(userId: number, sessionKey: string, kind: NotifyKind): Promise { const preference = await getChatNotifyPreference(userId, sessionKey).catch(() => null); return notifyMatches(preference, kind); }