Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8accb7135 | ||
|
|
cb83baf299 | ||
|
|
e7b658e3f7 |
@@ -34,3 +34,4 @@ deploy.log
|
||||
|
||||
# PM2 ecosystem (local configs)
|
||||
ecosystem.*.config.cjs
|
||||
.agents/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
---
|
||||
kind: contract
|
||||
kind: responsibility
|
||||
name: zulip-platform-verification
|
||||
version: 1.0.0
|
||||
description: >
|
||||
@@ -14,13 +14,13 @@ author: Abiba (pi agent)
|
||||
|
||||
## Platforms Under Contract
|
||||
|
||||
| Platform | Agent | Bot Email | Plugin Path | Health Port |
|
||||
|----------|-------|-----------|-------------|-------------|
|
||||
| pi | Abiba | abiba-bot@chat.sysloggh.net | `~/.pi/agent/extensions/zulip/` | :9200 |
|
||||
| Hermes | Tanko | tanko-bot@chat.sysloggh.net | `~/.hermes/plugins/platforms/zulip/` | :9201 |
|
||||
| Hermes | Mumuni | mumuni-bot@chat.sysloggh.net | `~/.hermes/plugins/platforms/zulip/` | :9202 |
|
||||
| Hermes | Koonimo | koonimo-bot@chat.sysloggh.net | `~/.hermes/plugins/platforms/zulip/` | :9203 |
|
||||
| Hermes | Koby | koby-bot@chat.sysloggh.net | `~/.hermes/plugins/platforms/zulip/` | :9204 |
|
||||
| Platform | Agent | Host | Bot Email | Health URL |
|
||||
|----------|-------|------|-----------|------------|
|
||||
| pi | Abiba | `192.168.68.24` | abiba-bot@chat.sysloggh.net | `http://192.168.68.24:9200/health` |
|
||||
| Hermes | Tanko | `192.168.68.123` | tanko-bot@chat.sysloggh.net | `http://192.168.68.123:9201/health` |
|
||||
| Hermes | Mumuni | `192.168.68.123` | mumuni-bot@chat.sysloggh.net | `http://192.168.68.123:9202/health` |
|
||||
| Hermes | Koonimo | `192.168.68.123` | koonimo-bot@chat.sysloggh.net | `http://192.168.68.123:9203/health` |
|
||||
| Hermes | Koby | `192.168.68.123` | koby-bot@chat.sysloggh.net | `http://192.168.68.123:9204/health` |
|
||||
|
||||
## Parameters
|
||||
|
||||
@@ -31,7 +31,7 @@ author: Abiba (pi agent)
|
||||
## Checks (per agent)
|
||||
|
||||
### Check 1: Health Endpoint
|
||||
- GET {{health_port}}/health → expect 200, `{ status: "ok", connected: true }`
|
||||
- GET {{health_url}} → expect 200, `{ status: "ok", connected: true }`
|
||||
|
||||
### Check 2: Queue Registered
|
||||
- Health response must include `queue_id` (non-null string)
|
||||
@@ -89,7 +89,7 @@ author: Abiba (pi agent)
|
||||
}
|
||||
```
|
||||
|
||||
## Ensures
|
||||
## Maintains
|
||||
|
||||
- Each agent is independently verified; one failure doesn't block others
|
||||
- @all-bots user_id is cross-validated against Zulip API (catches config drift)
|
||||
|
||||
@@ -302,6 +302,9 @@ class ZulipAdapter(BasePlatformAdapter):
|
||||
# Gen 2: Try to resolve @all-bots user ID dynamically
|
||||
await self._resolve_all_bots_user_id()
|
||||
|
||||
# Gen 5: Ensure bot is subscribed to primary stream
|
||||
await self._ensure_stream_subscriptions()
|
||||
|
||||
self._mark_connected()
|
||||
logger.info(
|
||||
"[%s] Connected to %s as %s (queue=%s, bot_id=%s, all_bots_id=%s)",
|
||||
@@ -449,6 +452,10 @@ class ZulipAdapter(BasePlatformAdapter):
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Clear stale errors on successful poll cycle
|
||||
self._health_stats["last_error_msg"] = None
|
||||
self._health_stats["last_error_at"] = None
|
||||
|
||||
# Gen 4: Heartbeat logging — prove poll loop is alive
|
||||
now = time.time()
|
||||
if now - self._last_heartbeat_time > HEARTBEAT_INTERVAL:
|
||||
@@ -677,6 +684,64 @@ class ZulipAdapter(BasePlatformAdapter):
|
||||
# Gen 2: Dynamic @all-bots resolution
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
async def _ensure_stream_subscriptions(self) -> None:
|
||||
"""Ensure the bot is subscribed to the primary streams.
|
||||
|
||||
Checks current subscriptions and subscribes to the configured primary
|
||||
stream (and general-chat) if missing. Without subscription, the bot
|
||||
receives DMs but no stream events — @mentions in streams are invisible.
|
||||
|
||||
This is a Gen 5 improvement based on lessons from the pi Zulip extension,
|
||||
where the bot had zero stream subscriptions and couldn't receive
|
||||
@mentions in stream topics.
|
||||
"""
|
||||
try:
|
||||
# Check current subscriptions
|
||||
resp, _ = await self._api_call(
|
||||
"GET", "/api/v1/users/me/subscriptions",
|
||||
)
|
||||
if not resp:
|
||||
return
|
||||
|
||||
subscribed = resp.get("subscriptions", [])
|
||||
subscribed_names = [s.get("name", "") for s in subscribed if isinstance(s, dict)]
|
||||
|
||||
# Streams the bot should be subscribed to
|
||||
required_streams = [self._stream, "general chat"]
|
||||
missing = [s for s in required_streams if s not in subscribed_names]
|
||||
|
||||
if not missing:
|
||||
logger.info(
|
||||
"[%s] Stream subscriptions OK: %s",
|
||||
self.name, ", ".join(subscribed_names),
|
||||
)
|
||||
return
|
||||
|
||||
# Subscribe to missing streams
|
||||
import json as _json
|
||||
payload = {"subscriptions": _json.dumps(
|
||||
[{"name": s} for s in missing]
|
||||
)}
|
||||
sub_resp, _ = await self._api_call(
|
||||
"POST", "/api/v1/users/me/subscriptions",
|
||||
data=payload,
|
||||
)
|
||||
if sub_resp and sub_resp.get("result") == "success":
|
||||
subscribed_str = ", ".join(missing)
|
||||
logger.info(
|
||||
"[%s] Subscribed to: %s", self.name, subscribed_str,
|
||||
)
|
||||
self._health_stats["subscriptions_fixed"] = (
|
||||
self._health_stats.get("subscriptions_fixed", 0) + 1
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"[%s] Failed to subscribe to %s",
|
||||
self.name, ", ".join(missing),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("[%s] Subscription check failed: %s", self.name, e)
|
||||
|
||||
async def _resolve_all_bots_user_id(self) -> None:
|
||||
"""Try to resolve the @all-bots user ID from the Zulip server.
|
||||
|
||||
@@ -1096,6 +1161,35 @@ class ZulipAdapter(BasePlatformAdapter):
|
||||
"detail": f"@all-bots user_id: {self._all_bots_user_id}",
|
||||
}
|
||||
|
||||
# 9. Stream subscriptions (verifies bot is subscribed to at least one stream)
|
||||
try:
|
||||
from urllib.parse import urlencode
|
||||
import json as _json
|
||||
key_str = f"{self._email}:{self._api_key}"
|
||||
auth_b64 = base64.b64encode(key_str.encode()).decode()
|
||||
resp, _ = await self._api_call(
|
||||
"GET", "/api/v1/users/me/subscriptions",
|
||||
)
|
||||
if resp and "subscriptions" in resp:
|
||||
stream_count = len(resp["subscriptions"])
|
||||
stream_names = [s.get("name", "?") for s in resp["subscriptions"]][:5]
|
||||
checks["stream_subscriptions"] = {
|
||||
"status": stream_count > 0,
|
||||
"detail": f"{stream_count} stream(s): {', '.join(stream_names)}"
|
||||
if stream_count > 0
|
||||
else "No stream subscriptions — bot will not receive stream events",
|
||||
}
|
||||
else:
|
||||
checks["stream_subscriptions"] = {
|
||||
"status": False,
|
||||
"detail": "Failed to check subscriptions",
|
||||
}
|
||||
except Exception as sub_err:
|
||||
checks["stream_subscriptions"] = {
|
||||
"status": False,
|
||||
"detail": f"Subscription check failed: {sub_err}",
|
||||
}
|
||||
|
||||
# Overall verdict
|
||||
critical = ["connected", "queue_registered", "http_client", "poll_loop"]
|
||||
passed = sum(1 for c in checks.values() if c["status"])
|
||||
|
||||
Reference in New Issue
Block a user