Root cause of Tanko not responding to DMs after initial connection:
Zulip event queues expire after ~10min of dont_block polling. When the
queue expired, _api_call() returned None for all 400-level errors, which
_fetch_events() treated as 'no events' and returned []. The poll loop
never knew the queue died, so it never reconnected.
Fix:
1. _api_call() now returns (Optional[Dict], status_code) tuple
2. _fetch_events() explicitly checks for 400 status and raises
RuntimeError('BAD_EVENT_QUEUE_ID') for the poll loop to catch
3. All callers updated to unpack the tuple
Now when a queue expires, the poll loop catches the error and
calls _reconnect() to register a fresh queue.
Root cause of 'str' object has no attribute 'value' error during connect():
the adapter passed a raw string 'zulip' to the base class's platform
parameter, which expects a Platform enum instance. When self.name was
accessed (calling self.platform.value.title()), the string had no .value
attribute.
Fix: import Platform from gateway.config and wrap with Platform('zulip').
Gen 3 improvements from build-zulip-plugin contract run:
1. Malformed message resilience
- Per-event try/except in poll loop — one bad event never kills adapter
- malformed_events counter tracked in health stats
- Fulfills Success Criterion: handles_malformed_messages == true
2. Periodic @all-bots refresh
- _all_bots_refresh_forever() background task re-resolves every hour
- Cancelled gracefully on disconnect()
- all_bots_refreshes counter in health stats
3. Health stats callback mechanism
- set_health_callback(callback, interval=600) — register external consumer
- _report_health_if_callback() triggers on dedup cleanup cycle
- Designed for Hermes Gateway to wire to RA-H OS knowledge graph logging
- Cross-contract integration: get_all_bots_user_id() and get_bot_user_id()
exposed for zulip-mention-reliability contract
- Adds message_update handler that periodically edits the Zulip
placeholder with partial response text (every ~2s during streaming)
- Sends a 'Thinking...' placeholder to Zulip immediately on DM receipt
- Detects agent busy state and uses deliverAs: 'steer' so subsequent
Zulip DMs steer the conversation mid-stream (like terminal TUI)
- Adds editMessage() to the queue helper (Zulip PATCH /messages/{id})
- sendMessage() now returns the message ID for editing
- Turn_start / agent_end lifecycle tracks agent busy state
- Cleanup: streaming timer properly cleared on shutdown
- Removed spawnSync subprocess (pi -p --mode json) for Zulip DM processing
- Messages now inject directly into the current pi session via
pi.sendUserMessage(), reusing the already-hot LLM — zero cold-start
- agent_end event handler captures assistant response and relays it to Zulip
- Added pending reply queue for ordered delivery of multiple Zulip messages
- Fixed zulip-js import (CJS default export, not named createClient)
- Added types.d.ts for zulip-js with default export declaration
- Updated package.json with yaml dependency, @earendil-works/pi-coding-agent
- Config supports both nested YAML and flat env-var formats
- Removed all subprocess timeout/buffer error paths (no longer needed)
- docs/GITOPS.md: complete workflow from prototype to production
- .gitea/pull_request_template.md: pre-merge checklist for swarm PR review
- scripts/deploy.sh: tag-based deployment to all 6 CTs with health checks
- scripts/rollback.sh: rollback to previous tag
- .gitea/workflows/ci.yml: CI pipeline (activates when Actions runners available)
- .gitignore: prevent config.yaml and secrets from being committed
- .gitattributes: consistent LF line endings across platforms