Verified on ground 2026-07-16 against CT 116 litellm_config.yaml + GPU hosts: - AMD host serves qwen3.6-35B-udq4 (LiteLLM alias strix-moe); ornith-1.0-35b does NOT exist - All 3 GPUs at 256K ctx, parallel 2 (RTX 3090 was listed 128K/parallel 1) - LiteLLM timeouts: qwen 300s, gemma 120s, strix 300s (were stale 90s/120s) - Added LiteLLM model surface + key scoping to litellm-self-heal - Patched health-check script path ref Files: litellm-self-heal, litellm-health, gpu-fleet, gpu-self-heal, zulip-adapter-lessons, abiba-zulip-restore, hermes-agent-baseline, delegation-prose-contract, mumuni-delegation-prose-contract
310 lines
10 KiB
Markdown
310 lines
10 KiB
Markdown
---
|
|
kind: function
|
|
name: abiba-zulip-restore
|
|
description: >
|
|
Restores Zulip connectivity for Abiba (pi agent). Verifies the v2 router-worker
|
|
extension code, starts a PM2 process as the Zulip gateway with correct env vars,
|
|
validates health endpoint, and confirms DM delivery. Run this whenever Abiba
|
|
stops responding on Zulip or after system restart.
|
|
agent: abiba
|
|
version: 1.0.0
|
|
status: active
|
|
runtime_contract: 2
|
|
---
|
|
|
|
# Abiba Zulip Restore — Resume pi Zulip Communication
|
|
|
|
Single-shot function that restores full Zulip connectivity for the Abiba pi agent.
|
|
Covers extension code validation, PM2 process management, health endpoint
|
|
verification, and DM loopback testing.
|
|
|
|
## Live-State Fields
|
|
|
|
| Field | Value | Trust |
|
|
|-------|-------|-------|
|
|
| Agent name | abiba | ✅ |
|
|
| Bot email | abiba-bot@chat.sysloggh.net | ✅ |
|
|
| Zulip server | https://chat.sysloggh.net | ✅ |
|
|
| Extension path | /root/.pi/agent/extensions/zulip/index.js | ✅ |
|
|
| Config path | /root/.pi/agent/extensions/zulip/config.yaml | ✅ |
|
|
| Health port | 9200 | ✅ |
|
|
| @all-bots user ID | 20 | ✅ (config, verified by API at runtime) |
|
|
| PM2 process name | abiba-zulip | ✅ |
|
|
| Provider | syslog-harness (http://192.168.68.116/v1) | ✅ |
|
|
| Default model | deepseek-v4-pro | ✅ (settings.json) |
|
|
|
|
## Architecture
|
|
|
|
The pi Zulip extension uses a **router-worker architecture** (v2):
|
|
|
|
- **Router** (PM2 `abiba-zulip`, `ZULIP_ROLE=router`): Polls Zulip for events,
|
|
maintains a per-sender pool of pi RPC worker processes. Each sender gets
|
|
their own `pi --mode rpc --session-dir` process with persistent sessions.
|
|
Handles streaming edits back to Zulip.
|
|
- **Worker** (child `pi --mode rpc`): Runs the agent with per-sender persistent
|
|
sessions. No Zulip logic in the worker — the router handles all Zulip I/O.
|
|
|
|
The extension loads in ALL pi sessions (via `settings.json` extensions array)
|
|
but is a **NO-OP** unless `ZULIP_ROLE=router` is set. Only the PM2 router process
|
|
carries the env var.
|
|
|
|
## Maintains
|
|
|
|
- extension_valid: bool — Extension code imports without errors
|
|
- pm2_running: bool — PM2 process `abiba-zulip` is online
|
|
- health_responding: bool — GET :9200/health returns "ok"
|
|
- zulip_connected: bool — Queue registered, bot identity resolved
|
|
- loopback_delivered: bool — Test DM sent and received
|
|
- model_valid: bool — Configured models match LiteLLM authorized models
|
|
|
|
### Postconditions
|
|
|
|
- PM2 process `abiba-zulip` online and stable (uptime > 30s)
|
|
- Health endpoint returns `{ status: "ok", connected: true }`
|
|
- Worker pool creates sessions on demand
|
|
- Echo prevention active (BOT_EMAILS includes all known bots)
|
|
- PM2 saved for auto-restart on boot
|
|
|
|
## Requires
|
|
|
|
- Node.js with `yaml` module available
|
|
- PM2 installed globally
|
|
- Zulip API key in `config.yaml`
|
|
- `pi` CLI available on PATH
|
|
- Zulip server accessible at https://chat.sysloggh.net
|
|
- LiteLLM provider accessible at http://192.168.68.116/v1
|
|
|
|
## Execution
|
|
|
|
### Step 1: Validate Extension Code
|
|
|
|
```bash
|
|
node -e "import('file:///root/.pi/agent/extensions/zulip/index.js').then(() => console.log('OK')).catch(e => {console.error('FAIL:', e.message); process.exit(1)})"
|
|
```
|
|
|
|
Expected: `OK`. If FAIL → check for missing dependencies, syntax errors.
|
|
|
|
### Step 2: Validate Model IDs
|
|
|
|
Compare configured models against LiteLLM authorized models:
|
|
|
|
```bash
|
|
API_KEY=$(grep -oP 'apiKey:\s*\K.*' /root/.pi/agent/models.json | head -1)
|
|
curl -s -H "Authorization: Bearer $API_KEY" http://192.168.68.116/v1/models | \
|
|
python3 -c "import json,sys; d=json.load(sys.stdin); [print(m['id']) for m in d.get('data',[])]" 2>/dev/null
|
|
```
|
|
|
|
Check: Every model ID in `models.json` must appear in the authorized list.
|
|
If not → fix `models.json` to only include authorized models (prefer `syslog-auto`).
|
|
|
|
### Step 3: Verify Config Integrity
|
|
|
|
```bash
|
|
python3 -c "
|
|
import yaml, sys
|
|
with open('/root/.pi/agent/extensions/zulip/config.yaml') as f:
|
|
cfg = yaml.safe_load(f)
|
|
required = ['zulip.site', 'zulip.email', 'zulip.api_key', 'agent.name']
|
|
for k in required:
|
|
keys = k.split('.')
|
|
v = cfg
|
|
for kk in keys:
|
|
v = v.get(kk)
|
|
if v is None:
|
|
print(f'MISSING: {k}')
|
|
sys.exit(1)
|
|
print('Config valid')
|
|
print(f' site={cfg[\"zulip\"][\"site\"]}')
|
|
print(f' email={cfg[\"zulip\"][\"email\"]}')
|
|
print(f' agent={cfg[\"agent\"][\"name\"]}')
|
|
print(f' health_port={cfg.get(\"health_port\", 9200)}')
|
|
"
|
|
```
|
|
|
|
Expected: Config valid with all fields non-empty.
|
|
|
|
### Step 4: Remove Stale Systemd Service
|
|
|
|
The old `abiba-zulip.service` points to `/opt/abiba-zulip/dist/index.js` (compiled
|
|
TypeScript, not the v2 extension). It's disabled and stale. Remove it:
|
|
|
|
```bash
|
|
systemctl stop abiba-zulip 2>/dev/null || true
|
|
systemctl disable abiba-zulip 2>/dev/null || true
|
|
rm -f /etc/systemd/system/abiba-zulip.service
|
|
systemctl daemon-reload
|
|
```
|
|
|
|
### Step 5: Start PM2 Process
|
|
|
|
**Critical:** The extension MUST run via `pi --mode rpc`, NOT `node index.js` directly.
|
|
The extension exports a function that requires pi's session lifecycle. Running
|
|
`node index.js` loads the module but never calls the export, so nothing happens.
|
|
`pi --mode rpc` loads all extensions (including Zulip) and fires `session_start`.
|
|
|
|
```bash
|
|
# Stop existing if any
|
|
pm2 delete abiba-zulip 2>/dev/null || true
|
|
|
|
# Start pi in RPC mode with ZULIP_ROLE=router env
|
|
ZULIP_ROLE=router pm2 start "$(which pi)" \
|
|
--name abiba-zulip \
|
|
--interpreter none \
|
|
-- --mode rpc --no-session
|
|
```
|
|
|
|
Wait 10 seconds for pi to load all extensions, fire session_start, and the Zulip
|
|
router to register its event queue.
|
|
|
|
### Step 6: Validate PM2 Process
|
|
|
|
```bash
|
|
pm2 show abiba-zulip --no-color
|
|
```
|
|
|
|
Check: `status=online`, `restarts=0`, `uptime > 5s`.
|
|
|
|
### Step 7: Check Logs for Connection
|
|
|
|
```bash
|
|
sleep 3
|
|
tail -20 /root/.pm2/logs/abiba-zulip-out.log
|
|
```
|
|
|
|
Look for:
|
|
- `[zulip-ext] Connecting to https://chat.sysloggh.net as abiba-bot@chat.sysloggh.net…`
|
|
- `[zulip-ext] Bot user_id=N, all-bots user_id=N`
|
|
- `[zulip-ext] Connected, queue=N`
|
|
- `[zulip-ext] Echo prevention: N bot emails`
|
|
- `[zulip-ext] Health endpoint on :9200`
|
|
|
|
If error → check API key, network to chat.sysloggh.net.
|
|
|
|
### Step 8: Health Endpoint
|
|
|
|
```bash
|
|
curl -s http://localhost:9200/health | python3 -m json.tool
|
|
```
|
|
|
|
Check:
|
|
- `status: "ok"` (not "down")
|
|
- `zulip.connected: true`
|
|
- `zulip.queue_id` is non-null string
|
|
- `zulip.bot_user_id` is positive integer
|
|
|
|
### Step 9: DM Loopback Test
|
|
|
|
```bash
|
|
curl -s http://localhost:9200/health | python3 -c "
|
|
import json,sys
|
|
d = json.load(sys.stdin)
|
|
if d.get('zulip',{}).get('connected'):
|
|
print(f'✅ Zulip connected. Queue: {d[\"zulip\"][\"queue_id\"]}')
|
|
print(f' Bot user_id: {d[\"zulip\"][\"bot_user_id\"]}')
|
|
print(f' Messages processed: {d[\"zulip\"][\"messages_processed\"]}')
|
|
else:
|
|
print('❌ Zulip NOT connected')
|
|
sys.exit(1)
|
|
"
|
|
```
|
|
|
|
### Step 10: Save PM2 for Auto-Start
|
|
|
|
```bash
|
|
pm2 save
|
|
pm2 startup systemd -u root --hp /root 2>/dev/null || true
|
|
```
|
|
|
|
### Step 11: Report
|
|
|
|
Compile results:
|
|
|
|
| Check | Pass? |
|
|
|-------|-------|
|
|
| Extension code imports | extension_valid |
|
|
| Model IDs authorized | model_valid |
|
|
| Config integrity | config_valid |
|
|
| PM2 process online | pm2_running |
|
|
| Health endpoint | health_responding |
|
|
| Zulip connected | zulip_connected |
|
|
|
|
All pass → ✅ **Abiba Zulip restored.** Relay success to user.
|
|
|
|
Partial failure → see recovery matrix below.
|
|
|
|
## Recovery Matrix
|
|
|
|
| Failure | Recovery |
|
|
|---------|----------|
|
|
| Extension import fails | Check `node_modules/zulip-js` exists; run `npm install` in extension dir |
|
|
| Model ID mismatch | Fix `models.json` to use `syslog-auto` as default model; remove invalid IDs |
|
|
| Config missing | Restore from backup or recreate from scratch |
|
|
| PM2 won't start | Check `node` version (>=18); check port 9200 not in use |
|
|
| Health "down" | Check logs for connection errors; verify Zulip API key; check network |
|
|
| "address already in use" | Kill old process: `fuser -k 9200/tcp` |
|
|
| Queue registration fails | Check Zulip API key validity; verify bot is active in Zulip admin |
|
|
| Rate limit (429) | Extension has built-in retry-after handling — wait, don't restart |
|
|
|
|
## Known Failure Modes
|
|
|
|
| Symptom | Root Cause | Recovery |
|
|
|---------|-----------|----------|
|
|
| Extension loads but no events | `ZULIP_ROLE` not set | Ensure PM2 env has `ZULIP_ROLE=router` |
|
|
| Worker stays "busy" forever | Model ID not authorized by LiteLLM | Fix models.json (lesson #11) |
|
|
| Placeholder sent but no response | editMessage API fails silently | Extension has fallback (sends new msg); check Zulip API |
|
|
| Queue expires rapidly | Poll interval too aggressive | v2 uses 3s poll with long-poll — should be fine |
|
|
| Bot doesn't respond to @mentions | Not subscribed to stream | Bot auto-subscribes via API |
|
|
| Stale error in health | `last_error` not cleared | v2 clears on successful poll (lesson #4) |
|
|
|
|
## Appendix: Root Cause & Fix Summary (2026-07-13)
|
|
|
|
**Problem:** Zulip extension was offline — no PM2 process running.
|
|
|
|
**Root cause:** The PM2 command ran `node index.js` directly (which loads the
|
|
extension module but never calls the exported function). The extension requires
|
|
pi's session lifecycle — pi loads extensions, fires `session_start`, and the
|
|
Zulip extension hooks into that event.
|
|
|
|
**Fix:** Run `pi --mode rpc` (not `node index.js`). The `--mode rpc` flag keeps
|
|
pi alive listening for RPC commands on stdin while the Zulip extension's router
|
|
runs in the background via the `session_start` hook.
|
|
|
|
```bash
|
|
ZULIP_ROLE=router pm2 start "$(which pi)" --name abiba-zulip \
|
|
--interpreter none -- --mode rpc --no-session
|
|
```
|
|
|
|
**Additional fixes applied:**
|
|
- Fixed `models.json`: replaced `qwen3.6-35B-A3B` (not authorized by LiteLLM)
|
|
with `syslog-auto` + `strix-moe` (prevents silent worker failure per
|
|
Lesson #11)
|
|
- Removed stale systemd unit `abiba-zulip.service` (pointed to old TS code)
|
|
- PM2 saved for auto-restart on boot
|
|
|
|
## Appendix: PM2 Ecosystem Config (Optional)
|
|
|
|
If preferred over manual `pm2 start`, create `/root/ecosystem.config.js` entry:
|
|
|
|
```js
|
|
module.exports = {
|
|
apps: [{
|
|
name: 'abiba-zulip',
|
|
script: '/bin/pi',
|
|
interpreter: 'none',
|
|
args: '--mode rpc --no-session',
|
|
cwd: '/root',
|
|
env: {
|
|
ZULIP_ROLE: 'router',
|
|
},
|
|
log_file: '/root/.pm2/logs/abiba-zulip-out.log',
|
|
error_file: '/root/.pm2/logs/abiba-zulip-error.log',
|
|
max_restarts: 20,
|
|
restart_delay: 5000,
|
|
}]
|
|
};
|
|
```
|
|
|
|
---
|
|
|
|
**Last verified good state**: 2026-07-13 — Extension v2 running via `pi --mode rpc`, health endpoint :9200 returning `{status:"ok",connected:true}`, queue a669f21e.
|