repositories / will
will
owned by admin
src/shared/wake.ts
Rawexport 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
}