hermes-slack-listener is a sibling Worker, not an extension of agent-worker
ADR-050: hermes-slack-listener is a sibling Worker, not an extension of agent-worker
- Status: Accepted
- Date: 2026-05-13
- Deciders: Mishaal Murawala (engineer-decides delegation)
- Supersedes: —
- Superseded by: —
- Related: ADR-031 (Hermes agent layer), invariants #1, #9, #11, #12, #14
Context
Mishaal asked for a Slack history monitor over 5 channels in the CMO Coffee
Talk workspace (TCUP02C49). The Worker reads with a xoxp- user OAuth token
and writes draft replies only to Mishaal’s private bot IM with xoxb-. The
goal is zero footprint in monitored channels: no @mentions, no channel
posts, no presence.
Two homes were considered:
- Extend
ascend-agent-worker— add a Slack-listenerscheduled()path and Slack-specific Durable Objects to the existing agent Worker. - Sibling Worker — new top-level directory
hermes-slack-listener/with its ownwrangler.toml, secrets, KV namespace, and deploy cadence.
Decision
Sibling Worker. New directory hermes-slack-listener/ at repo root,
alongside agent-worker/, context-worker/, etc.
Rationale
-
Blast radius for the user OAuth token.
xoxp-is the most privileged credential involved — it reads message history across 5 community channels in a private Slack workspace that Mishaal does not own. Co-locating it with the multi-tenant agent infrastructure (which already holds tenant bearer hashes, OAuth state secrets, and the AgentSession / AgentJob / AgentWorker / OrchestratorJob SQLite DOs) widens the failure surface for that one credential well beyond what its use justifies. -
Failure isolation. A regression in
ascend-agent-workerthat crashes the Worker (or hits its 128 MB memory ceiling, or causes a runaway DO alarm) would silently break Slack polling too. With a separate Worker, each side fails independently. The hourly cron is unmonitored by design (Phase 1 has no alerting); coupling it to a more critical Worker would force us to either add monitoring we don’t yet need, or accept silent regressions in the Slack path. -
Deploy cadence mismatch.
agent-workeris iterating rapidly on tenant orchestration. The Slack listener should be deployed only when its code changes — coupling them forces unrelated redeploys (and the associated risk window) every time agent-worker ships. -
Different architectural shape.
agent-workeris a heavy DO-based session/job/worker/orchestrator runtime. The Slack listener is a flat hourly cron + a stateless fetch handler for slash/event webhooks. Mixing these in one Worker means either polluting agent-worker with cron triggers that don’t belong to it, or splitting the Slack code into a sub-handler inside agent-worker that is conceptually a sibling anyway. -
Sources-of-truth invariant (#15). The Slack listener owns only one new store:
HERMES_CURSORSKV namespace, withcursor:{channel_id}entries. That belongs in its own namespace, not co-mingled with agent-worker’s bindings. -
Cron CPU budget (CF docs). CF Cron Triggers have a 15-minute CPU ceiling for hourly+ intervals. The Slack listener’s tick is well under that, but a future regression in agent-worker that increases its request-path CPU could push a co-resident cron handler past the budget. Independent Workers have independent CPU budgets.
Alternatives considered and rejected
-
A Workers Workflow inside agent-worker. Workflows are great for multi-step ingestion (see ADR-026), but this is a single-step cron poll with KV cursor state — Workflows are overkill and would add Durable Object dependencies the listener doesn’t need.
-
Run as a recipe inside the existing
n8ninstance. Rejected on principle — global rules treat n8n as consumer-only and forbid using it as the orchestrator (~/.claude/rules/n8n-workflows.md). Also Slack history polling at hourly cadence with 30 s timeouts is a poor fit for n8n’s UI-driven workflow model. -
Run as a script on the user’s Mac via launchd. Hermes already runs there for the CLI agent layer. Rejected because: (a) requires the laptop to be online, (b) puts a long-lived
xoxp-token on a non-managed endpoint, (c) leaves no audit surface —wrangler tail+ Logpush is cheaper than rebuilding observability on macOS.
Consequences
Positive:
- Smaller, more auditable code surface for the highest-privileged credential in this workstream.
- Independent deploys, independent rollback.
- Clean per-Worker secret/KV/observability boundary.
- No widening of
ascend-agent-worker’s scope.
Negative:
- One more
wrangler.tomland one more deploy target to maintain. - One more entry in the
docs/projects/LEDGER.mdto keep current. - Cross-Worker invocation, if ever needed (e.g. agent-worker wants to ask the Slack listener for draft history), would require a Service Binding per invariant #1 — that’s an ADR-update if and when it happens, not now.
Invariants honored
- #1 Two-plane architecture — this Worker is in the execution plane only; it owns no context. If it ever needs context-worker, the call is a Service Binding from the execution plane to the context plane.
- #9 CF Cron for scheduled work —
0 * * * *, no external scheduler. - #11 30 s AbortController on every outbound fetch — Slack and DeepSeek both wrapped.
- #12 Every LLM call through AI Gateway — DeepSeek requests routed via
gateway.ai.cloudflare.com. - #14 Secrets in Wrangler — never in code, never in KV values.
Open follow-ups
- Decide alerting strategy for Phase 2 (Slack-of-Slack? Email?
wrangler tailis enough for Phase 1). - If Phase 2 adds a “send for me” approval flow, that needs its own ADR — it crosses from observer to participant in the monitored workspace.