diff --git a/packages/hal/src/validators/ebds/ebds-fsm.ts b/packages/hal/src/validators/ebds/ebds-fsm.ts index 1373427..ea4b4a8 100644 --- a/packages/hal/src/validators/ebds/ebds-fsm.ts +++ b/packages/hal/src/validators/ebds/ebds-fsm.ts @@ -13,8 +13,14 @@ import { EventEmitter } from 'node:events' import type { ParseResult } from './ebds-rs232.js' +// MEI's internal escrow grace expires around 5s. We want any escrow that +// sits open for >500ms (5× our 100ms poll cadence) to surface in field +// logs so we can spot drift toward the autonomous-return failure mode. +const ESCROW_WATCHDOG_WARN_MS = 500 + export class EbdsFsm extends EventEmitter { private currentStatus: string | null = null + private escrowOpenedAt: number | null = null constructor() { super() @@ -38,6 +44,26 @@ export class EbdsFsm extends EventEmitter { this.currentStatus = status console.log('[EBDS] %d status: %s → %s', Date.now(), prev ?? '(init)', status) + // Escrow watchdog: time the bill's stay in `billsRead`. Anything past + // ESCROW_WATCHDOG_WARN_MS means the host took too long to decide + // stack/reject relative to the validator's grace window — exactly + // the timing that produces torn bills on MEI hardware. + if (prev === 'billsRead' && this.escrowOpenedAt !== null) { + const elapsed = Date.now() - this.escrowOpenedAt + this.escrowOpenedAt = null + if (elapsed > ESCROW_WATCHDOG_WARN_MS) { + console.warn( + '[EBDS] escrow held %dms before transition to %s (threshold %dms) — ' + + 'host decision latency creeping toward MEI grace window', + elapsed, + status, + ESCROW_WATCHDOG_WARN_MS + ) + } else { + console.log('[EBDS] escrow → %s in %dms', status, elapsed) + } + } + switch (status) { case 'accepting': // Transient: bill is being read by the validator head. Diagnostic @@ -53,6 +79,7 @@ export class EbdsFsm extends EventEmitter { this.emit('reject') return } + this.escrowOpenedAt = Date.now() this.emit('billsAccepted') // Emit billsRead on next tick (matches lamassu-machine behavior) process.nextTick(() => this.emit('billsRead', bill)) @@ -107,5 +134,6 @@ export class EbdsFsm extends EventEmitter { /** Reset status tracking (e.g., after reload) */ reset(): void { this.currentStatus = null + this.escrowOpenedAt = null } }