Skip to content

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:

  1. Extend ascend-agent-worker — add a Slack-listener scheduled() path and Slack-specific Durable Objects to the existing agent Worker.
  2. Sibling Worker — new top-level directory hermes-slack-listener/ with its own wrangler.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

  1. 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.

  2. Failure isolation. A regression in ascend-agent-worker that 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.

  3. Deploy cadence mismatch. agent-worker is 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.

  4. Different architectural shape. agent-worker is 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.

  5. Sources-of-truth invariant (#15). The Slack listener owns only one new store: HERMES_CURSORS KV namespace, with cursor:{channel_id} entries. That belongs in its own namespace, not co-mingled with agent-worker’s bindings.

  6. 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 n8n instance. 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.toml and one more deploy target to maintain.
  • One more entry in the docs/projects/LEDGER.md to 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 work0 * * * *, 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 tail is 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.