Compare commits

..
Author SHA1 Message Date
Abiba (pi) 33453b8204 feat(zulip): v3 resilience rewrite — circuit breaker, retry, watchdog, model fix
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.
2026-07-13 21:29:25 +00:00
abiba-bot 441255a07c feat: add edit_message + streaming support to Zulip adapter (#33) 2026-07-13 06:49:55 +00:00
jeromeandAbiba (pi) 5d9e36f657 fix(zulip): add _strip_html for slash command matching
Zulip delivers message content as HTML (<p>/approve</p>).
The gateway slash command parser expects plain text, so HTML
tags prevent command matching. This helper strips HTML tags
and decodes common entities (&amp;, &lt;, etc.).

Per contract: zulip-approval-fix.prose.md

Applied to: Mumuni (CT114), Tanko (CT112), Koby (CT111)
2026-07-13 06:49:43 +00:00
AbibaandAbiba (pi) bfc6e1877d feat: add edit_message + streaming support to Zulip adapter
Implements edit_message() using Zulip's PATCH /api/v1/messages/{id} API.
Enables the Hermes GatewayStreamConsumer to progressively update Zulip
messages during agent generation, giving users real-time visibility into
agent thinking via progressive edits.

Adds _api_patch() helper for PATCH HTTP method.
2026-07-13 06:49:43 +00:00
jerome e1b76376b1 Merge pull request 'test: final CI verification' (#31) from feat/ci-test-final into main
Reviewed-on: #31
2026-07-06 04:05:38 +00:00
Abiba (pi) 40ce413fd8 contract: add host + Health URL columns, use health_url param
- Added Host column (Abiba: 192.168.68.24, Hermes agents: 192.168.68.123)
- Changed Health Port → Health URL with full http://host:port/health URLs
- Updated Check 1 to reference {{health_url}} instead of {{health_port}}
- Added .gitignore for .agents/ (OpenProse run state)
2026-07-02 17:18:40 +00:00
Abiba (pi) 15f94e3bcb feat: Gen 5 improvements — stream subscription check + auto-subscribe
Adds two improvements from pi Zulip extension lessons:

1. _ensure_stream_subscriptions() — checks at connect time if the bot
   is subscribed to the primary stream and general-chat; auto-subscribes
   if missing. Without this, bots with 0 subscriptions can't receive
   stream events (DMs only).

2. selftest check #9 — verifies stream subscriptions and reports
   count + names. Catches the 'zero subscriptions' failure mode that
   was a major debugging bottleneck in the pi extension.
2026-06-29 22:28:21 +00:00
Abiba (pi) 3bb7698f88 fix: clear stale errors on successful poll — prevents phantom error alerts
Same fix as applied to the pi Zulip extension: last_error_msg and
last_error_at are now cleared on every successful poll cycle, not
just on reconnect. Health monitors no longer show stale errors.
2026-06-29 22:22:59 +00:00
Abiba (pi) 0112c5bba7 feat: /zulip-test command + standardized health schema v1
- Added /zulip-test self-test command (7 checks: queue, identity,
  @all-bots, health, poll loop, echo prevention, API send)
- Standardized health endpoint to zulip-health/v1 schema
  (nested zulip{} object, checks{} map, platform/agent metadata)
- Saved pi extension source to pi-zulip-extension/extension-src/
  (with all fixes: dynamic @all-bots, health sync, logging)
- Added extension src to repo for deployment tracking
- Updated vault with final contract status report
2026-06-29 03:45:34 +00:00
Abiba (pi) 1a6098f7fd contract: Zulip extension verification report + cross-platform contract
- Full contract verification for pi (TypeScript) and Hermes (Python) Zulip plugins
- Fixed @all-bots user_id (1→20) — now dynamically resolved from Zulip API
- Fixed last_event_id stale display in health endpoint (B2)
- Fixed display_recipient logging (B3)
- Added bot_user_id and all_bots_user_id to health endpoint
- Created OpenProse cross-platform verification contract (docs/contracts/)
- Created Hermes plugin deployment script (scripts/deploy-hermes-zulip.py)

Pi extension: 8/9 checks pass (LLM relay untested — no external DMs)
Hermes adapter: 9/10 checks pass (deployment pending)
2026-06-29 03:40:52 +00:00
6 changed files with 1657 additions and 1345 deletions
+2 -2
View File
@@ -14,10 +14,10 @@ jobs:
validate: validate:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4
- run: python3 --version - run: python3 --version
- run: node --version - run: node --version
- run: echo "Runner works!" - run: echo "Runner works!"
- run: git clone --depth 1 http://abiba-bot:***REMOVED***@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git .
- name: Python syntax check - name: Python syntax check
run: | run: |
@@ -45,8 +45,8 @@ print('✅ config.yaml.example valid')
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [validate] needs: [validate]
steps: steps:
- uses: actions/checkout@v4
- run: echo "🚀 Deploy tag $(echo $GITHUB_REF_NAME)" - run: echo "🚀 Deploy tag $(echo $GITHUB_REF_NAME)"
- run: git clone --depth 1 http://abiba-bot:***REMOVED***@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git .
- name: Deploy to Tanko (canary) - name: Deploy to Tanko (canary)
run: ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 jerome@192.168.68.122 "cd /root && bash -s" < scripts/deploy.sh --ct=tanko --mode=native "$GITHUB_REF_NAME" 2>&1 || echo "⚠️ Tanko deploy skipped" run: ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 jerome@192.168.68.122 "cd /root && bash -s" < scripts/deploy.sh --ct=tanko --mode=native "$GITHUB_REF_NAME" 2>&1 || echo "⚠️ Tanko deploy skipped"
@@ -1,26 +1,76 @@
/**
* PM2 Ecosystem Config — Zulip Gateway v3 (Resilience)
*
* Deploy: pm2 start /root/.pm2/ecosystem.config.cjs
* Status: pm2 status
* Logs: pm2 logs abiba-zulip
*/
module.exports = { module.exports = {
apps: [ apps: [
{ {
// ── Router (main Zulip gateway) ──
name: "abiba-zulip", name: "abiba-zulip",
script: "/usr/bin/pi", script: "/bin/pi",
args: "--mode rpc --session-id zulip-service", args: "--mode rpc --session-id zulip-service",
cwd: "/root", cwd: "/root",
// Resilience hardening — up from pi defaults
max_restarts: 100, // Crash loops won't exhaust PM2 (was default 10)
min_uptime: "10s", // Must survive 10s to count as "alive"
max_memory_restart: "500M", // OOM protection — restart before swap thrash
restart_delay: 5000, // 5s cooldown between restarts
kill_timeout: 15000, // 15s SIGTERM grace before SIGKILL
listen_timeout: 30000, // 30s to bind health port
// Logging
log_date_format: "YYYY-MM-DD HH:mm:ss Z",
error_file: "/root/.pm2/logs/abiba-zulip-error.log",
out_file: "/root/.pm2/logs/abiba-zulip-out.log",
merge_logs: true,
log_type: "json",
// Process management
autorestart: true,
watch: false,
instances: 1,
exec_mode: "fork",
// Environment
env: { env: {
ZULIP_ROLE: "router", ZULIP_ROLE: "router",
ZULIP_EXTENSION_ACTIVE: "true", ZULIP_SITE: "https://chat.sysloggh.net",
NODE_OPTIONS: "--max-old-space-size=512", ZULIP_EMAIL: "abiba-bot@chat.sysloggh.net",
ZULIP_API_KEY: "cKTDMZAPW08dk3zl05sStzO7HRztzyn8",
AGENT_NAME: "abiba",
AGENT_OWNER_EMAIL: "jerome@sysloggh.com",
NODE_ENV: "production",
}, },
// Prevent rapid crash-looping: restart with backoff, limit retries
min_uptime: "30s",
max_restarts: 15,
restart_delay: 10000,
kill_timeout: 5000,
autorestart: true,
}, },
{ {
name: "abiba-telegram", // ── Supervisor (external watchdog) ──
script: "/usr/bin/pitg", name: "zulip-watchdog",
script: "/root/.pi/agent/extensions/zulip/watchdog.js",
cwd: "/root", cwd: "/root",
max_restarts: 10,
min_uptime: "3s",
restart_delay: 3000,
kill_timeout: 5000,
log_date_format: "YYYY-MM-DD HH:mm:ss Z",
error_file: "/root/.pm2/logs/zulip-watchdog-error.log",
out_file: "/root/.pm2/logs/zulip-watchdog-out.log",
merge_logs: true,
autorestart: true,
watch: false,
instances: 1,
exec_mode: "fork",
env: {
NODE_ENV: "production",
},
}, },
], ],
}; };
+11
View File
@@ -0,0 +1,11 @@
{
"lastChangelogVersion": "0.80.6",
"defaultProvider": "syslog-harness",
"defaultModel": "syslog-auto",
"defaultThinkingLevel": "high",
"extensions": [
"+extensions/mcp/index.ts",
"+extensions/zulip/index.js"
],
"theme": "dark"
}
@@ -0,0 +1,84 @@
/**
* Zulip Gateway Supervisor — external watchdog process.
*
* Monitors the router health endpoint every 30s. If 3 consecutive checks fail,
* restarts the abiba-zulip PM2 process gracefully.
*
* This is the pattern Hermes uses: an external supervisor that can recover
* the gateway even when the gateway process itself is hung (not just crashed).
*
* Deployed via PM2 as a separate process in ecosystem.config.cjs.
*/
const HEALTH_URL = "http://127.0.0.1:9200/health";
const CHECK_INTERVAL_MS = 30_000;
const MAX_FAILURES = 3;
const RESTART_GRACE_MS = 15_000;
let failures = 0;
async function check() {
try {
const res = await fetch(HEALTH_URL, {
signal: AbortSignal.timeout(5000),
});
if (res.ok) {
const data = await res.json();
if (data.status === "ok" && data.zulip?.connected) {
if (failures > 0) {
console.log(`[watchdog] Router recovered after ${failures} failure(s)`);
}
failures = 0;
// Silent health log every 10 checks (~5 min) for monitoring
if (Math.random() < 0.1) {
console.log(`[watchdog] Router healthy (uptime: ${Math.round(process.uptime())}s)`);
}
return;
}
// Connected but degraded
console.warn(`[watchdog] Router degraded: status=${data.status}, connected=${data.zulip?.connected}`);
failures++;
} else {
console.warn(`[watchdog] Health check returned ${res.status}`);
failures++;
}
} catch (err) {
failures++;
console.warn(`[watchdog] Health check ${failures}/${MAX_FAILURES}: ${err.message}`);
}
if (failures >= MAX_FAILURES) {
console.error(`[watchdog] ${MAX_FAILURES} consecutive failures — restarting abiba-zulip`);
const { execSync } = await import("node:child_process");
try {
execSync("pm2 restart abiba-zulip", { timeout: 30000, encoding: "utf-8" });
console.log("[watchdog] Restart command sent successfully");
} catch (e) {
console.error(`[watchdog] Restart failed: ${e.message}`);
// Fallback: try resurrect if restart fails (process may be deleted)
try {
execSync("pm2 resurrect", { timeout: 30000 });
console.log("[watchdog] PM2 resurrected (fallback)");
} catch (e2) {
console.error(`[watchdog] Resurrect also failed: ${e2.message}`);
}
}
failures = 0;
// Wait for restart to fully initialize before checking again
await new Promise((r) => setTimeout(r, RESTART_GRACE_MS));
}
}
console.log("[watchdog] Zulip gateway supervisor started");
console.log(`[watchdog] Monitoring ${HEALTH_URL} every ${CHECK_INTERVAL_MS / 1000}s`);
console.log(`[watchdog] Max failures before restart: ${MAX_FAILURES}`);
// Immediate first check, then periodic
check();
setInterval(check, CHECK_INTERVAL_MS);
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -67,7 +67,6 @@ DEFAULT_STREAM = "agent-hub"
DEFAULT_ALL_BOTS_USER_ID = 1 DEFAULT_ALL_BOTS_USER_ID = 1
DEFAULT_POLL_INTERVAL = 3.0 DEFAULT_POLL_INTERVAL = 3.0
MAX_ZULIP_MESSAGE = 10000 MAX_ZULIP_MESSAGE = 10000
TRUNCATION_NOTICE = "\n\n[...truncated at Zulip limit]"
ECHO_TAG_PREFIX = "hermes-zulip-" ECHO_TAG_PREFIX = "hermes-zulip-"
RECONNECT_BACKOFF = [2, 5, 10, 30, 60] RECONNECT_BACKOFF = [2, 5, 10, 30, 60]
DEDUP_WINDOW = 300 # 5 minutes DEDUP_WINDOW = 300 # 5 minutes
@@ -121,7 +120,7 @@ def _truncate(text: str, limit: int = MAX_ZULIP_MESSAGE) -> str:
"""Truncate to Zulip's message limit with notice.""" """Truncate to Zulip's message limit with notice."""
if len(text) <= limit: if len(text) <= limit:
return text return text
return text[:limit - len(TRUNCATION_NOTICE)] + TRUNCATION_NOTICE return text[:limit] + "\n\n[...truncated at Zulip limit]"
def _parse_zulip_timestamp(ts: Any) -> datetime: def _parse_zulip_timestamp(ts: Any) -> datetime: