AGENTS.md@apps/lemon_channels · git:20261016.274152d · 2026-10-16 · sha256 110e3de1a88134ef

AGENTS.md@apps/lemon_channels git:20261016.274152dA

Immutable. This exact content never changes and is served at /api/v1/blob/110e3de1a88134ef.

# LemonChannels contributor guide

Channels owns external messaging adapters, authorization/normalization, platform
rendering and the delivery outbox. Router owns conversation semantics and prompt
mutation; gateway owns execution slots. See [README](README.md) for adapter setup
and script notifications.

## App seams

- Normalize inbound traffic into core contracts and submit through
  `LemonCore.RouterBridge`. Preserve success, rejection and unknown-outcome
  distinctions. Never acknowledge rejected work or automatically retry an
  ambiguous mutation; busy-query failure is not idle.
- Router emits `LemonCore.DeliveryIntent`. Channels owns `OutboundPayload`,
  send/edit, chunking, buttons, media batching and platform message indices.
- Resolve project/agent/cwd/queue bindings through `LemonCore.BindingResolver`.
  Bindings never select an executor. Native `lemon` tokens alone resume top-level
  work; old vendor tokens remain readable history but never select a runtime.
- Use `TargetDirectory` as the cross-app target-discovery seam. Other apps should
  not read Telegram/Discord known-target tables directly.
- Adapters run under `AdapterSupervisor` from the configured adapter list. Do not
  add hardcoded platform startup gates to the application supervisor.
- Each platform config implements `LemonCore.Config.Gateway.Channel`, registers
  under `:gateway_channels`, and owns its env declarations. Production transport
  settings come from canonical TOML via `GatewayConfig`, not app-env overlays.
  The full-replacement test config layer is for isolation only.

## Authorization

- Telegram authorization applies to messages and callback queries. A non-empty
  `allowed_chat_ids` is authoritative; an empty allowlist is unpaired and drops
  updates unless `allow_all_chats: true` is explicit. `/whoami` may answer before
  auth; unpaired notices are throttled. Transcription runs only after auth.
- Transports read the persisted gateway allowlist on every (re)start; supervisor
  child specs must not freeze it. Pairing approval/revocation go through
  `LemonChannels.Pairing.allow/2` / `revoke/2` (a `:update_allowlist` call to the
  transport) so running transports update live; never require a restart or widen
  beyond the named chat. A configured empty Discord `allowed_channel_ids` is
  deny-all; only an absent key is unrestricted.
- Discord slash commands must pass the same guild/channel allowlists and
  `deny_unbound_channels` checks as ordinary messages before any read, mutation,
  cancellation, reflection or runtime call.
- Group/supergroup/channel trigger mode defaults to mentions. Keep topic/thread
  scope in auth, bindings, delivery and state keys.
- Diagnostics and notification JSON expose bounded metadata, not credentials,
  message bodies, attachment paths or raw platform responses.

## Delivery and state

- Outbox FIFO grouping includes channel, account, peer kind/id and thread.
  Preserve order within a group while allowing independent groups to progress.
- Outbox delivery is at-least-once for the channels whose adapters report an unknown outcome. An unknown outcome
  (adapter `{:outcome_unknown, _}`, adapter exception, worker exit) is retried
  with the same payload and idempotency key within the bounded `max_attempts`
  budget, then reported terminal. This is the one deliberate exception to the
  root never-retry `:outcome_unknown` rule: a rare duplicate message is cheaper
  than a lost one, and sending is not a state mutation to reconcile. Dedupe is
  marked only on confirmed delivery and still suppresses re-sends of confirmed
  keys, including pending retries. Adapters keep returning ambiguous writes as
  `{:outcome_unknown, _}` and never retry them internally; the outbox owns that
  retry. Unrecognized errors and definite rejections stay terminal, and the
  exception covers no other mutation (inbound submission, enqueue timeouts).
  SMTP network failures after DATA and WhatsApp bridge send exceptions are
  still terminal (tracked in F-0059).
  Honor rate-limit hints.
- Telegram markdown entity offsets are UTF-16, not bytes/graphemes. Truncation
  must preserve native resume lines and table column boundaries.
- Transport GenServers remain lifecycle shells; add behavior to existing local
  normalization/pipeline/command/action helpers. Async Telegram work uses its
  task supervisor. Picker/system traffic should use the delivery/outbox path.
- `/model` scope is deliberate: session choices stay session-local, while
  future-session defaults are chat-wide even when selected from a topic. Accept
  exact labels and plain provider/model IDs; selections are not ordinary prompts.
- `/new` increments `{account, chat, thread}` generation before new mappings.
  Session/resume keys include generation and message ID, invalidating old replies
  without full scans. Abort/cleanup/reflection is background work; reflection
  ambiguity is logged safely and not retried automatically.
- Known-target writes are throttled unless metadata changes. Use the app-owned
  wrapper rather than bypassing its policy.
- Generated media is opt-in, count/size bounded and cwd/symlink checked upstream.
  Rendering must not expose host-only `MEDIA:` directive lines.
- XMTP and WhatsApp retain separate process identities/scripts/event tags while
  sharing `PortBridge` lifecycle mechanics. A stopped port must not leave its
  child or stale reconnect state alive.

## Operator paths and verification

`lemon send` / `./bin/lemon send` support Telegram and Discord. Keep
[script notification usage](README.md#script-notifications) authoritative for
account/thread defaults, stdin, attachments, dry-run, JSON and exit codes.
`mix lemon.channels` inspects redacted configuration; `lemon doctor` reports
configuration health, not a stored live-proof gate.

From the umbrella root: `mix test apps/lemon_channels/test`.
Use local/fake platform APIs; exercise unauthorized callbacks/slash commands,
thread isolation, outbox order, retry boundaries, Unicode entity offsets and
restart cleanup. Tests changing global runtime apps restore their original
running set and do not stop unrelated consumers.