export type WakeKind = 'deploy_failure' | 'ci_failure' | 'recovery' | 'chat' | 'heartbeat' export interface Wake { id: string seq: number kind: WakeKind createdAt: string payload: unknown /** attempts > 0 means a previous turn for this wake failed on the model path. */ attempts: number /** ISO time; wake is not claimable before this (deferred retry backoff). */ deferredUntil?: string | undefined } /** Priority classes; lower number = handled first (spec: failure, chat FIFO, heartbeat). */ export function wakePriority(kind: WakeKind): number { switch (kind) { case 'deploy_failure': case 'ci_failure': case 'recovery': return 0 case 'chat': return 1 case 'heartbeat': return 2 } } /** Stable ordering: priority class, then enqueue seq (FIFO inside a class). */ export function orderWakes< T extends { kind: WakeKind; seq: number; deferredUntil?: string | undefined }, >(wakes: T[], nowMs: number): T[] { return wakes .filter((w) => !w.deferredUntil || Date.parse(w.deferredUntil) <= nowMs) .sort((a, b) => wakePriority(a.kind) - wakePriority(b.kind) || a.seq - b.seq) } /** * At most one pending heartbeat wake may exist (coalescing). * Call before enqueueing a heartbeat. */ export function hasPendingHeartbeat(wakes: readonly { kind: WakeKind }[]): boolean { return wakes.some((w) => w.kind === 'heartbeat') } export interface HeartbeatState { lastClaimedHour?: string } /** * Claim the current UTC hour; restart may claim the current unhandled hour, * older hours are never replayed (spec AC16). */ export function claimHeartbeatHour( state: HeartbeatState, hourKey: string, ): { claimed: boolean; state: HeartbeatState } { if (state.lastClaimedHour === hourKey) return { claimed: false, state } if (state.lastClaimedHour !== undefined && hourKey < state.lastClaimedHour) { return { claimed: false, state } } return { claimed: true, state: { lastClaimedHour: hourKey } } } export interface WakeContext { wake: Wake facts: string prompt: string }