Complete rewrite of the pi Zulip gateway from crash-prone v2 to production-resilient v3. Research-backed patterns from Zulip event system docs + Node.js resilience best practices.
Root Cause
Pi workers used DeepSeek v4-pro as default model — a reasoning model that produces only reasoning_content with normal token limits. Workers waited forever for content that never arrived, hung, and were killed by the 5-minute stuck-worker timeout. Every "Processing..." placeholder was a dead worker.
## Summary
Complete rewrite of the pi Zulip gateway from crash-prone v2 to production-resilient v3. Research-backed patterns from Zulip event system docs + Node.js resilience best practices.
## Root Cause
Pi workers used DeepSeek v4-pro as default model — a reasoning model that produces only `reasoning_content` with normal token limits. Workers waited forever for content that never arrived, hung, and were killed by the 5-minute stuck-worker timeout. Every "Processing..." placeholder was a dead worker.
## Changes
### extension-src/zulip-extension.js (v2→v3)
- **Circuit breaker** (`ZulipCircuitBreaker`): CLOSED→OPEN→HALF_OPEN states, 50% failure threshold, volumeThreshold=2 for slow-polling, 30s reset timeout
- **Retry with jitter** (`withRetry`): Exponential backoff + 50-100% jitter, max 2 attempts for transient errors only
- **Queue lifecycle**: `idle_queue_timeout=600` (Zulip 12.0+), auto re-register on BAD_EVENT_QUEUE_ID
- **Busy worker timeout**: SIGKILL after 5 minutes + error DM to user
- **Crash handlers**: `uncaughtException` + `unhandledRejection` → log + reconnect instead of dying
- **Enhanced health endpoint**: Circuit breaker stats, idle_queue_timeout
- **Poll loop**: `while(connected)` instead of recursive `setTimeout` — cleaner lifecycle
### extension-src/watchdog.js (NEW)
- External supervisor process (Hermes pattern)
- Polls `/health` every 30s, restarts router after 3 consecutive failures
- Grace period after restart before re-checking
### config/ecosystem.abiba.config.cjs
- PM2 hardening: `max_restarts=100` (was 10), `max_memory_restart=500M`, `kill_timeout=15s`
- Separate configs for router and watchdog processes
### config/settings.json (NEW)
- Fixed default model: `deepseek-v4-pro` → `syslog-harness/syslog-auto`
- This was the actual root cause of all the worker hangs
## Verification
- Circuit breaker: CLOSED→HALF_OPEN→OPEN on simulated outage, recovered gracefully ✅
- End-to-end: DM processed in 4 seconds ✅
- Watchdog: 20+ min uptime, 0 unnecessary restarts ✅
- Health: `{status:"ok", connected:true, circuit_breaker:"CLOSED"}` ✅
Root cause: Workers hung because default model was DeepSeek v4-pro (reasoning
model that produces empty content with normal token limits). pi RPC workers
waited forever for content that never arrived.
Changes:
- Circuit breaker (CLOSED→OPEN→HALF_OPEN) around Zulip API calls
- Retry with exponential backoff + jitter for transient errors
- Queue lifecycle management (idle_queue_timeout, auto re-register on BAD_EVENT_QUEUE_ID)
- External supervisor watchdog (restarts router after 3 health check failures)
- Busy worker timeout (SIGKILL after 5 min + error DM)
- PM2 hardening (max_restarts=100, max_memory_restart=500M)
- Crash handlers (uncaughtException + unhandledRejection → reconnect, not die)
- Fixed default model: deepseek-v4-pro → syslog-harness/syslog-auto
- Enhanced health endpoint with circuit breaker stats
Architecture: Research-backed from Zulip event system docs + Node.js resilience
patterns. Inline circuit breaker (no dependency). Separate watchdog process (Hermes pattern).
Verified: Circuit breaker trips on outage, recovers gracefully. End-to-end DM
processed in 4 seconds. Watchdog monitoring every 30s.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Complete rewrite of the pi Zulip gateway from crash-prone v2 to production-resilient v3. Research-backed patterns from Zulip event system docs + Node.js resilience best practices.
Root Cause
Pi workers used DeepSeek v4-pro as default model — a reasoning model that produces only
reasoning_contentwith normal token limits. Workers waited forever for content that never arrived, hung, and were killed by the 5-minute stuck-worker timeout. Every "Processing..." placeholder was a dead worker.Changes
extension-src/zulip-extension.js (v2→v3)
ZulipCircuitBreaker): CLOSED→OPEN→HALF_OPEN states, 50% failure threshold, volumeThreshold=2 for slow-polling, 30s reset timeoutwithRetry): Exponential backoff + 50-100% jitter, max 2 attempts for transient errors onlyidle_queue_timeout=600(Zulip 12.0+), auto re-register on BAD_EVENT_QUEUE_IDuncaughtException+unhandledRejection→ log + reconnect instead of dyingwhile(connected)instead of recursivesetTimeout— cleaner lifecycleextension-src/watchdog.js (NEW)
/healthevery 30s, restarts router after 3 consecutive failuresconfig/ecosystem.abiba.config.cjs
max_restarts=100(was 10),max_memory_restart=500M,kill_timeout=15sconfig/settings.json (NEW)
deepseek-v4-pro→syslog-harness/syslog-autoVerification
{status:"ok", connected:true, circuit_breaker:"CLOSED"}✅