Compare commits
22
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33453b8204 | ||
|
|
441255a07c | ||
|
|
5d9e36f657 | ||
|
|
bfc6e1877d | ||
|
|
e1b76376b1 | ||
|
|
aaaa11a912 | ||
|
|
b6fa3da540 | ||
|
|
40ce413fd8 | ||
|
|
15f94e3bcb | ||
|
|
3bb7698f88 | ||
|
|
0112c5bba7 | ||
|
|
1a6098f7fd | ||
|
|
15adb30bc7 | ||
|
|
faf9566332 | ||
|
|
b0aeb81caf | ||
|
|
04f78f3f01 | ||
|
|
3fbd31fbff | ||
|
|
a5dc880af1 | ||
|
|
9b9845fd14 | ||
|
|
106048999f | ||
|
|
6afc46734a | ||
|
|
9ee1919985 |
@@ -17,7 +17,7 @@ jobs:
|
|||||||
- 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:mipjoq-tybbox-2ciHru@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git .
|
- 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: |
|
||||||
@@ -46,7 +46,7 @@ print('✅ config.yaml.example valid')
|
|||||||
needs: [validate]
|
needs: [validate]
|
||||||
steps:
|
steps:
|
||||||
- run: echo "🚀 Deploy tag $(echo $GITHUB_REF_NAME)"
|
- run: echo "🚀 Deploy tag $(echo $GITHUB_REF_NAME)"
|
||||||
- run: git clone --depth 1 http://abiba-bot:mipjoq-tybbox-2ciHru@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git .
|
- 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"
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
run: git clone --depth 1 http://abiba-bot:mipjoq-tybbox-2ciHru@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git .
|
run: git clone --depth 1 http://abiba-bot:***REMOVED***@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git .
|
||||||
- name: Python syntax
|
- name: Python syntax
|
||||||
run: |
|
run: |
|
||||||
python3 -m py_compile plugins/platforms/zulip/adapter.py 2>/dev/null
|
python3 -m py_compile plugins/platforms/zulip/adapter.py 2>/dev/null
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
environment: canary
|
environment: canary
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
run: git clone --depth 1 http://abiba-bot:mipjoq-tybbox-2ciHru@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git .
|
run: git clone --depth 1 http://abiba-bot:***REMOVED***@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git .
|
||||||
- name: Deploy to Tanko
|
- name: Deploy to Tanko
|
||||||
env:
|
env:
|
||||||
TAG: ${{ github.ref_name }}
|
TAG: ${{ github.ref_name }}
|
||||||
@@ -66,7 +66,7 @@ jobs:
|
|||||||
environment: production
|
environment: production
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
run: git clone --depth 1 http://abiba-bot:mipjoq-tybbox-2ciHru@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git .
|
run: git clone --depth 1 http://abiba-bot:***REMOVED***@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git .
|
||||||
- name: Deploy to all Hermes agents
|
- name: Deploy to all Hermes agents
|
||||||
env:
|
env:
|
||||||
TAG: ${{ github.ref_name }}
|
TAG: ${{ github.ref_name }}
|
||||||
@@ -97,7 +97,7 @@ jobs:
|
|||||||
environment: agent-zero
|
environment: agent-zero
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
run: git clone --depth 1 http://abiba-bot:mipjoq-tybbox-2ciHru@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git .
|
run: git clone --depth 1 http://abiba-bot:***REMOVED***@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git .
|
||||||
- name: Deploy to kagentz
|
- name: Deploy to kagentz
|
||||||
env:
|
env:
|
||||||
TAG: ${{ github.ref_name }}
|
TAG: ${{ github.ref_name }}
|
||||||
|
|||||||
@@ -203,6 +203,22 @@ class ZulipAdapter(BasePlatformAdapter):
|
|||||||
logger.error("Zulip POST %s network error: %s", path, exc)
|
logger.error("Zulip POST %s network error: %s", path, exc)
|
||||||
return {"result": "error", "msg": str(exc)}
|
return {"result": "error", "msg": str(exc)}
|
||||||
|
|
||||||
|
async def _api_patch(self, path: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
import aiohttp
|
||||||
|
url = f"{self._site}/api/v1/{path.lstrip('/')}"
|
||||||
|
try:
|
||||||
|
async with self._session.patch(
|
||||||
|
url, data=payload, auth=self._auth(),
|
||||||
|
timeout=aiohttp.ClientTimeout(total=15),
|
||||||
|
) as resp:
|
||||||
|
data = await resp.json()
|
||||||
|
if resp.status >= 400:
|
||||||
|
logger.debug("Zulip PATCH %s -> %s: %s", path, resp.status, str(data)[:200])
|
||||||
|
return data
|
||||||
|
except (aiohttp.ClientError, asyncio.TimeoutError) as exc:
|
||||||
|
logger.debug("Zulip PATCH %s network error: %s", path, exc)
|
||||||
|
return {"result": "error", "msg": str(exc)}
|
||||||
|
|
||||||
async def _api_delete(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
async def _api_delete(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
import aiohttp
|
import aiohttp
|
||||||
url = f"{self._site}/api/v1/{path.lstrip('/')}"
|
url = f"{self._site}/api/v1/{path.lstrip('/')}"
|
||||||
@@ -326,6 +342,23 @@ class ZulipAdapter(BasePlatformAdapter):
|
|||||||
|
|
||||||
return SendResult(success=True, message_id=str(last_id) if last_id else None)
|
return SendResult(success=True, message_id=str(last_id) if last_id else None)
|
||||||
|
|
||||||
|
async def edit_message(
|
||||||
|
self,
|
||||||
|
chat_id: str,
|
||||||
|
message_id: str,
|
||||||
|
content: str,
|
||||||
|
**kwargs,
|
||||||
|
) -> SendResult:
|
||||||
|
"""Edit a previously-sent message. Used by the gateway stream consumer
|
||||||
|
for progressive message updates during agent streaming."""
|
||||||
|
formatted = self.format_message(content)
|
||||||
|
data = await self._api_patch(f"messages/{message_id}", {"content": formatted})
|
||||||
|
if data.get("result") != "success":
|
||||||
|
msg = str(data.get("msg", "edit failed"))
|
||||||
|
logger.debug("Zulip edit_message(%s) -> %s", message_id, msg)
|
||||||
|
return SendResult(success=False, error=msg)
|
||||||
|
return SendResult(success=True, message_id=message_id)
|
||||||
|
|
||||||
async def get_chat_info(self, chat_id: str) -> Dict[str, Any]:
|
async def get_chat_info(self, chat_id: str) -> Dict[str, Any]:
|
||||||
kind, to, topic = _parse_target(chat_id, self._default_topic)
|
kind, to, topic = _parse_target(chat_id, self._default_topic)
|
||||||
if kind == "direct":
|
if kind == "direct":
|
||||||
|
|||||||
@@ -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",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
@@ -99,6 +99,23 @@ def _build_auth_header(email: str, api_key: str) -> str:
|
|||||||
return f"Basic {token}"
|
return f"Basic {token}"
|
||||||
|
|
||||||
|
|
||||||
|
def _strip_html(text: str) -> str:
|
||||||
|
"""Strip HTML tags and decode HTML entities from Zulip message content.
|
||||||
|
|
||||||
|
Zulip delivers message content as rendered HTML (e.g. <p>/approve</p>).
|
||||||
|
The gateway slash command parser expects plain text, so HTML tags
|
||||||
|
prevent command matching. This helper strips tags and decodes entities.
|
||||||
|
"""
|
||||||
|
if not text:
|
||||||
|
return text
|
||||||
|
# Strip HTML tags
|
||||||
|
text = re.sub(r"<[^>]+>", "", text)
|
||||||
|
# Decode common HTML entities
|
||||||
|
text = text.replace("&", "&").replace("<", "<").replace(">", ">")
|
||||||
|
text = text.replace(""", '"').replace("'", "'").replace(" ", " ")
|
||||||
|
return text.strip()
|
||||||
|
|
||||||
|
|
||||||
def _truncate(text: str, limit: int = MAX_ZULIP_MESSAGE) -> str:
|
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:
|
||||||
@@ -784,6 +801,8 @@ class ZulipAdapter(BasePlatformAdapter):
|
|||||||
sender_name = msg.get("sender_full_name", "Unknown")
|
sender_name = msg.get("sender_full_name", "Unknown")
|
||||||
sender_id = msg.get("sender_id")
|
sender_id = msg.get("sender_id")
|
||||||
content = msg.get("content", "")
|
content = msg.get("content", "")
|
||||||
|
# Strip Zulip HTML for slash command matching (zulip-approval-fix contract)
|
||||||
|
content = _strip_html(content)
|
||||||
|
|
||||||
# Echo-loop prevention: skip own messages
|
# Echo-loop prevention: skip own messages
|
||||||
if sender_email == self._bot_email:
|
if sender_email == self._bot_email:
|
||||||
|
|||||||
Reference in New Issue
Block a user