# LemonRouter

`lemon_router` owns request normalization, conversation identity, queue semantics, and semantic output tracking.
It sits between channel transports and `lemon_gateway`.

## Current Flow

```text
Channel transport or gateway-native ingress
  -> LemonRouter.Router.handle_inbound/1
  -> LemonRouter.RunOrchestrator
  -> LemonRouter.SessionCoordinator
  -> LemonRouter.RunProcess
  -> configured LemonCore.EngineRuntime.submit_execution/1
  -> gateway runtime scheduler / thread worker / run
  -> LemonCore.Bus run events
  -> LemonRouter semantic coalescers / output tracking
  -> LemonCore.DeliveryIntent
  -> LemonChannels.Dispatcher
  -> channel-specific renderer / outbox
```

## Ownership

- Router owns:
  - `RunRequest` normalization
  - policy, model, and engine resolution
  - resume resolution and conversation-key selection
  - queue semantics: `collect`, `followup`, `steer`, `steer_backlog`, `interrupt`
  - pending-compaction prompt rewriting
  - semantic stream and tool-status coalescing
- Router does not own:
  - Telegram or Discord rendering details
  - `OutboundPayload` construction
  - Telegram message-id presentation state
  - gateway slot scheduling or engine lifecycle

## Key Modules

| Module | Responsibility |
| --- | --- |
| `LemonRouter.Router` | Main inbound entrypoint, session-key resolution, pending-compaction application, control-plane abort/keepalive hooks |
| LemonRouter.RunOrchestrator (internal) | Builds router-owned submissions from `LemonCore.RunRequest` and hands them to `SessionCoordinator` |
| `LemonRouter.SessionCoordinator` | Single owner of per-conversation queue semantics and active-run handoff |
| Router internal session read model | Internal read model over coordinator-owned active session state |
| `LemonRouter.ConversationKey` | Canonical conversation-key selection from structured resume or session key |
| `LemonRouter.ResumeResolver` | Structured resume resolution before runtime submission |
| `LemonRouter.RunProcess` | Active-run lifecycle wrapper around one execution |
| `LemonRouter.MediaJobRecorder` | Records generated final-answer files into redacted media job metadata before channel delivery |
| LemonRouter.ChannelsDelivery (internal) | Narrow bridge from router-adjacent automation delivery requests into `LemonChannels`; must not construct `OutboundPayload` or own channel rendering |
| `LemonRouter.StreamCoalescer` | Semantic answer coalescing that emits `DeliveryIntent` snapshots/finalization |
| `LemonRouter.ToolStatusCoalescer` | Semantic tool-status coalescing that emits `DeliveryIntent` snapshots/finalization |
| `LemonRouter.PendingCompactionStore` | Router-owned typed wrapper for pending-compaction markers |
| `LemonRouter.AgentEndpointStore` | Router-owned typed wrapper for persistent endpoint aliases |
| `LemonRouter.AgentInbox` | BEAM-local send API with selectors, fanout, and queue-mode selection |
| `LemonRouter.AgentDirectory` | Active/durable session discovery |
| `LemonRouter.AgentEndpoints` | Persistent route aliases |

`SurfaceManager.finalize_answer/3` dispatches `:stream_finalize` first for runs that already
streamed answer deltas, then finalizes `StreamCoalescer` state so late flushes cannot overwrite
the final. For non-streamed completions it still finalizes through `StreamCoalescer`, with a direct
`:final_text` fallback if the coalescer finalize call exits or times out. Completion-time artifact
metadata enrichment is best-effort and must not block the final answer path.
`RunProcess.ArtifactTracker` also supports Hermes-style final-answer media
directives: a line containing `MEDIA:<project-relative-path>` is converted into
an explicit `auto_send_files` entry after existing-file, cwd, and symlink escape
checks pass. The directive line is removed from the final text before channel
rendering, and Telegram/Discord delivery still goes through the normal
attachment policy.

## Important Contracts

- Inbound callers should provide structured resume data through `LemonCore.RunRequest.resume` when they already know it.
- Engine ID validation and normalization should use `LemonCore.EngineCatalog`; default cwd resolution should use `LemonCore.Cwd`.
- Router emits `LemonCore.DeliveryIntent`, not `LemonChannels.OutboundPayload`.
- Tool-status failure summaries preserve safe structured fields from
  `action.detail.result_meta`, including `error_type`, tool name, timeout,
  command exit code, exception class/name, status, reason, message, and
  validation errors.
- Cron/channel-origin summary delivery may cross router through the internal LemonRouter.ChannelsDelivery bridge, but the bridge must stay narrow. Router must not construct `LemonChannels.OutboundPayload`; `LemonChannels` remains responsible for enqueue semantics, retries, chunking, and adapter delivery.
- Runtime input is `LemonCore.ExecutionCommand`; gateway-private adapters may translate it to `LemonGateway.ExecutionRequest`.
- Telegram-specific state is owned by `lemon_channels` wrappers:
  - `LemonChannels.Telegram.StateStore`
  - `LemonChannels.Telegram.ResumeIndexStore`
- External apps must query busy/active session state through `LemonRouter.Router` or `LemonCore.RouterBridge`, not router-internal read-model or registry details.

## Session And Queue Semantics

`SessionCoordinator` serializes by conversation key:

- `{:resume, engine, token}` when a structured resume token is available
- `{:session, session_key}` otherwise

Queue-mode behavior lives here:

- `:collect` appends
- `:followup` debounces/merges recent followups, except async task/delegated followups which stay separate
- `:steer` attempts in-run steer and falls back to followup
- `:steer_backlog` attempts in-run steer and falls back to collect
- active async task/delegated auto-followups are promoted to `:steer` so completions try to reach the live parent run before falling back to a queued followup
- `:interrupt` cancels the active run and inserts the new request at the front

## Output Semantics

Router coalescers only track semantic state:

- accumulated text
- sequence numbers
- semantic tool/action state
- run/session metadata needed for `DeliveryIntent`

Channels decides:

- send vs edit
- truncation
- reply markup
- media batching
- Telegram resume indexing by platform message id

## Testing

Run the app suite from the umbrella root:

```bash
mix test apps/lemon_router
```

Useful focused suites during refactors:

```bash
mix test apps/lemon_router/test/lemon_router/router_test.exs
mix test apps/lemon_router/test/lemon_router/run_orchestrator_test.exs
mix test apps/lemon_router/test/lemon_router/session_coordinator_test.exs
mix test apps/lemon_router/test/lemon_router/run_process_test.exs
mix test apps/lemon_router/test/lemon_router/stream_coalescer_test.exs
mix test apps/lemon_router/test/lemon_router/tool_status_coalescer_test.exs
```

Run architecture checks after boundary changes:

```bash
mix lemon.quality
```
