v0.12.3 — audit-backlog health signal & degrade-safe hardening
CTO review follow-ups on the v0.12.2 tree — truthful audit-backlog health signal and degrade-safe failure-path hardening.
Fixed
-
swarm:healthnow ages unclaimed audit-outbox rows, closing a false "relay appears active" signal (review F1).runAuditOutboxStalenessCheck()only warned on pending rows whose reservation had expired (reserved_atset and stale). Rows the relay had never claimed (reserved_at IS NULL) were never aged, so a relay that is unscheduled, misrouted, or starved before claim let audit evidence back up while the check still reported"<n> pending row(s), relay appears active"— a false clean signal in a regulated deployment where audit-backlog observability is a compliance concern, not an ops nicety. The check now also counts unclaimed pending rows older than the warning threshold (bycreated_at) and reports the two backlog modes separately in the warning detail (<n> pending row(s) with stale reservationsvs<n> unclaimed pending row(s) aging past <T>s). The threshold reuses the existingswarm.durable.relay.stale_warning_threshold_secondskey (the audit outbox already shares the durable relay timeout), so no new configuration surface is introduced; the config anddocs/configuration.mdentries for that key now document that it governs both the durable (swarm:health --durable) and audit (bareswarm:health) staleness checks. Recent unclaimed rows still readok(normal between relay runs). Mirrors the existing, provenrunOutboxStalenessCheck()durable-outbox pattern. The health check's staleness/warning thresholds are now computed in UTC (now('UTC')) to match how the outbox storescreated_at/reserved_at(Carbon::now('UTC')) — a barenow()compared app-timezone wall-clock against the UTC columns and skewed the boundary by the UTC offset on any non-UTCapp.timezone, hiding (or prematurely flagging) genuinely-aged rows; this corrects the audit and durable staleness checks. Covered by a non-UTC-timezone regression test. -
The audit/durable "degrade safely" paths can no longer become a second failure surface if the logging or error-reporting stack itself throws (review F2). The fallback
report()andlogger->{level}()calls on the failure-handling paths assumed the logging/reporting stack was safe; a hostile or misconfigured logger could turn a contained failure into an uncaught one. A new internalBuiltByBerry\LaravelSwarm\Support\SafeReportingtrait centralizes containment behind never-throwsafeReport()/safeLog()helpers (with anerror_log()last resort that is itself guarded), and is now used byConfiguredSinkFailureHandler(all five policy log calls),DatabaseAuditOutbox(the threereport()calls plus the dead-letter transition log), andDatabaseDurableOutbox(both drain-loopreport()calls). Thefailure_policy=haltdecision still returnsHalt(and the dispatcher still throws) when the logger throws — only the log/report call is contained, never the halt itself.safeLog()invokes the level-specific logger method, so logging behavior is byte-identical to before; only the throw is contained. Theerror_log()last resort emits only the exception class (nevergetMessage()), so the rare double-failure breadcrumb cannot leak a caller-supplied audit payload into PHP's ungoverned system error log — the full detail is still persisted sealed in the outbox row'slast_error. Covered by a new hostile-logger/hostile-reporter test suite exercising the trait in isolation, theerror_logleak boundary (asserting the class is present and a sensitive sentinel is absent), the failure handler (including halt-survives-hostile-logger), and the realDatabaseAuditOutbox::drain()dead-letter path with both the sink and the logger throwing.Operator-facing notes for the
swarm:healthoutput change and the timezone correction are inUPGRADING.md(exit code and--jsonokare unchanged).