AGENTS.md@apps/lemon_router · git:20260927.6027bc7 · 2026-09-27 · sha256 7e3bd4378be3e4e2
AGENTS.md@apps/lemon_router git:20260927.6027bc7A
Immutable. This exact content never changes and is served at /api/v1/blob/7e3bd4378be3e4e2.
# LemonRouter contributor guide
Router owns conversation semantics: request normalization, model/policy/resume
resolution, queues, active runs, compaction and semantic delivery. It does not
parse platform protocols, allocate gateway slots or own message IDs.
See [README](README.md) and [architecture](../../docs/architecture/overview.md).
## Submission and queue ownership
- `RunOrchestrator` builds router `Submission` values wrapping core
`ExecutionCommand`; submit through configured `LemonCore.EngineRuntime`, not
`LemonGateway.Runtime` or gateway-private request structs.
- `SessionCoordinator` owns one active pointer/backlog per conversation key,
follow-up debounce/merge, steering fallback and interrupt semantics. A key is
`{:resume, engine, token}` when known, otherwise `{:session, session_key}`.
- Task/delegated auto-followups prefer steer with follow-up fallback. Do not merge
separate provenance-bearing async completions during debounce. Record the final
delivery disposition/receipt after router mutation, not before.
- `Router.handle_inbound/1` propagates submission errors. Only a non-empty binary
run ID acknowledges success; malformed acknowledgements are outcome-unknown,
not permission for an automatic retry.
- Public busy/active queries go through `Router`/`RouterBridge`. Registry and
read-model failure is unavailable, never an invented idle or empty state.
- Abort/keepalive `:ok` acknowledges dispatch, not synchronous target application.
- Unexpected exceptions in router callbacks/state transitions reach OTP
supervision, not fabricated `:ok`, nil or unchanged state.
## Cancellation and completion
- Serialize fixed-ID aborts with admission. Durable abort fences prevent later
execution even after in-memory tombstones expire; accepted work follows normal
cancellation. Failed fence registration must propagate as an unknown mutation.
- Admission callback crashes/malformed acknowledgements keep the durable submitting
fence. Only a definite pre-enqueue rejection permits rollback.
- Permanent admission rows retain only state/semantic identity; bounded cleanup
compacts legacy rows and sanitizes reasons without discarding replay protection.
- A run that never binds to a live executor must still terminalize exactly once
so its coordinator releases the conversation. Runtime submission retries are
bounded by a pre-start deadline, not permanent busy state.
- Queued start failure emits one structured completion and advances the queue.
Adopt an ambiguously started child already in the registry rather than emit a
second synthetic completion.
- On lost gateway process, allow the bounded completion grace window for a real
late bus event to win before synthesizing failure. Idle timeout `0` explicitly
disables the watchdog; cancellation still works.
## Output and compaction
- Emit `LemonCore.DeliveryIntent`; channel renderers decide payloads, send/edit,
truncation, buttons and platform indices. Never construct `OutboundPayload`.
- `SurfaceManager` coordinates answer/status/task surfaces; coalescers aggregate
semantic output. Preserve parent/task IDs and safe failure metadata upstream,
rather than reconstructing hierarchy in adapters.
- Finalization is terminal for a stream: late flushes must not overwrite the
final multi-chunk answer with an old truncated snapshot. If coalescer finalizing
fails, dispatch the final text directly. Already-streamed answers finalize the
existing answer surface before clearing coalescer state.
- `SurfaceManager.finalize_answer/3` returns `{:error, {:dispatch_failed, reason}}`
on rejected final delivery; do not finalize the streamed coalescer after that
rejection. Best-effort artifact enrichment and termination cancellation must
not prevent final-answer or terminal-event handling.
- Artifact enrichment failure cannot block answer finalization. Files and
`MEDIA:` directives reach channels only through checked `auto_send_files`:
existing files under cwd, no symlink escape. Strip host-only directive text.
- Child task updates stay on their original task surface across later assistant
turns. Chat status excludes internal reasoning; operator views may expose only
the intended structured visible action stream.
- Async task-surface subscribers restart transiently after abnormal exit and
re-subscribe; normal terminal events stop them. This does not replay lost deltas.
- Pending compaction prompt mutation belongs here. Prepare a fresh marker before
submit and consume only after coordinator acceptance. Submission failure leaves
it retryable; stale/empty markers may clear eagerly. Preserve secure envelopes
and whole-entry truncation when rebuilding context.
- Use `PendingCompactionStore` and shared domain wrappers. Channels own Telegram
state/resume indices; discover human-readable targets through `TargetDirectory`.
## Configuration and verification
Config supplies model/thinking defaults; `LemonCore.PolicyStore` owns effective
session/route/chat policy. Do not read app-env default models or policies as a
second runtime plane. Provider resolution uses `LemonAgent.ProviderConfigResolver`.
Validate configured runtime wiring while preserving router-only startup and
explicit unavailable-runtime behavior.
From the umbrella root: `mix test apps/lemon_router/test`.
Exercise concurrent fixed-ID submit/abort, unknown outcomes, queued start failure,
stream-finalization races and restart cleanup with explicit synchronization.
Changes to dependency boundaries also require the integrated architecture check.