Problem
src/discord.rs is the only production path in the crate that locks its shared Mutex with expect(...):
src/discord.rs: allow_request / rate_limit_delay / record_success / record_failure / bury_to_dlq
let mut state = self.state.lock().expect("discord state lock");
Every other long-lived path already tolerates poisoning (daemon.rs, dispatch.rs, gjc_lane.rs, source/subscription.rs, source/git.rs, source/tmux.rs, lifecycle.rs use if let Ok(..) / .ok()? / map(..)), so this is a local inconsistency rather than an intentional invariant.
Impact
DiscordState guards the rate limiter, the per-target circuit breakers, and the DLQ buffer. If any single panic unwinds while that guard is held, the mutex becomes poisoned and every later allow_request / rate_limit_delay / record_success / record_failure / DLQ bury panics for the remaining lifetime of the daemon. A one-off panic therefore escalates into permanent, unrecoverable loss of the entire Discord delivery lane (including DLQ capture, which is the very mechanism meant to preserve undelivered messages) until an operator restarts the daemon.
Expected
A poisoned Discord state should degrade to possibly-stale counters, not to an unrecoverable panic loop. The three guarded structures are all individually recoverable:
- rate limiter — worst case one mis-timed delay
- circuit breakers — worst case one stale failure count
- DLQ — an append-only bounded buffer
Proposed fix
Route all five sites through one poison-tolerant accessor (self.state() returning MutexGuard via PoisonError::into_inner), matching the convention used elsewhere in the crate, and add a regression test that poisons the state and asserts the limiter, circuit-breaker transition, and DLQ paths still work.
Found by a self-development sweep of dev@c4774562c6b073d4d6e1481aeb10ee8aa68afbad (open backlog was zero).
—
[repo owner's gaebal-gajae (clawdbot) 🦞]
Problem
src/discord.rsis the only production path in the crate that locks its sharedMutexwithexpect(...):Every other long-lived path already tolerates poisoning (
daemon.rs,dispatch.rs,gjc_lane.rs,source/subscription.rs,source/git.rs,source/tmux.rs,lifecycle.rsuseif let Ok(..)/.ok()?/map(..)), so this is a local inconsistency rather than an intentional invariant.Impact
DiscordStateguards the rate limiter, the per-target circuit breakers, and the DLQ buffer. If any single panic unwinds while that guard is held, the mutex becomes poisoned and every laterallow_request/rate_limit_delay/record_success/record_failure/ DLQ bury panics for the remaining lifetime of the daemon. A one-off panic therefore escalates into permanent, unrecoverable loss of the entire Discord delivery lane (including DLQ capture, which is the very mechanism meant to preserve undelivered messages) until an operator restarts the daemon.Expected
A poisoned Discord state should degrade to possibly-stale counters, not to an unrecoverable panic loop. The three guarded structures are all individually recoverable:
Proposed fix
Route all five sites through one poison-tolerant accessor (
self.state()returningMutexGuardviaPoisonError::into_inner), matching the convention used elsewhere in the crate, and add a regression test that poisons the state and asserts the limiter, circuit-breaker transition, and DLQ paths still work.Found by a self-development sweep of
dev@c4774562c6b073d4d6e1481aeb10ee8aa68afbad(open backlog was zero).—
[repo owner's gaebal-gajae (clawdbot) 🦞]