LemonRouter
lemon_router owns request normalization, conversation identity, queue semantics, and semantic output tracking.
It sits between channel transports and lemon_gateway.
Current Flow
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:
RunRequestnormalization- 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
OutboundPayloadconstruction- 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.resumewhen they already know it. - Engine ID validation and normalization should use
LemonCore.EngineCatalog; default cwd resolution should useLemonCore.Cwd. - Router emits
LemonCore.DeliveryIntent, notLemonChannels.OutboundPayload. - Tool-status failure summaries preserve safe structured fields from
action.detail.result_meta, includingerror_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;LemonChannelsremains responsible for enqueue semantics, retries, chunking, and adapter delivery. - Runtime input is
LemonCore.ExecutionCommand; gateway-private adapters may translate it toLemonGateway.ExecutionRequest. - Telegram-specific state is owned by
lemon_channelswrappers:LemonChannels.Telegram.StateStoreLemonChannels.Telegram.ResumeIndexStore
- External apps must query busy/active session state through
LemonRouter.RouterorLemonCore.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:
:collectappends:followupdebounces/merges recent followups, except async task/delegated followups which stay separate:steerattempts in-run steer and falls back to followup:steer_backlogattempts in-run steer and falls back to collect- active async task/delegated auto-followups are promoted to
:steerso completions try to reach the live parent run before falling back to a queued followup :interruptcancels 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:
mix test apps/lemon_router
Useful focused suites during refactors:
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:
mix lemon.quality