Delivery Runtime
Delivery runtime converts output requests into durable session records, live events, artifacts, and channel items.
Every output send_* call also writes a delivery intent into the SQLite
runtime outbox projection and a matching delivery.send durable work item.
Outbox rows are owned by the foreground turn through owner_turn_id; delivery
work does not use the foreground turn id as a task id or parent work id.
DeliveryRuntime claims and completes the durable work through
HostWorkLifecycleRuntime, then owns dispatch through
SessionInteractionRouter, which looks up the active route for
InteractionOutbound.session_id. Channel adapters are bound to sessions; they
adapt payloads to platform APIs but do not own durable delivery state.
The in-memory InteractionItem.dispatch_status lifecycle is
pending -> scheduled -> delivered/failed/unrouted. The durable outbox
lifecycle is queued -> sending -> sent/failed/unknown/unrouted.
unrouted means no route is currently bound for the outbound session. It is
different from failed, which means a route existed and adapter delivery raised
an error.
Sources
Delivery requests can come from:
- output modules
- authored tools
- schedule runs
- channel adapter logic
History Policy
Persisted delivery becomes durable assistant history. Transient delivery is useful for progress, notices, and live-only output.
Artifacts
Artifacts are represented by host-owned records. Output modules may request
artifact delivery, but the host owns paths, metadata, and persistence.
Foreground delivery artifacts are owned by the session turn through
owner_turn_id; they are not task artifacts unless a real detached task creates
them through a future explicit task-artifact seam.
Session Routes
InteractionOutbound.session_id is required. The channel field is adapter
metadata; it no longer decides route ownership. Ordinary live delivery is routed
only by outbound.session_id.
SessionInteractionRouter owns the live route table:
bind(session_id, route)attaches a TUI, Telegram, or other adapter route to one session and returns a token.unbind(token)removes that live route.deliver(outbound)sends only to the route bound foroutbound.session_id.prompt_user(prompt)andrequest_approval(request)perform separate session-aware route lookups for interactive prompts and approvals.
Routes defensively reject outbound payloads for any session other than the one
they are bound to. InteractionRuntime.handle() binds the inbound route after
the runner resolves the final session. /new, /resume, and session switch
paths must rebind to the new session.
Durable channel ownership is tracked separately from live routes. The runtime
stores (core_id, channel, conversation_key) -> session_id, where
conversation_key is a host-owned external route key such as
telegram:dm:123, slack:channel:T1:C1:thread:123.4, or
matrix:room:%21room%3Aexample. Channel adapters must build those keys through
the shared runtime helper, not by hand. The canonical format intentionally does
not preserve older bindings such as telegram:123; old channel conversations
start fresh unless explicitly resumed.
Channel /resume is both a live route switch and a durable conversation
rebind. After a user resumes a session from an external channel, later inbound
messages from the same conversation_key resolve to that resumed session.
Channels
Channel adapters adapt delivery into platform-specific messages. They no longer
act as ambient bridges inherited by nested work. If a session has no active
route, ordinary deliveries and tool lifecycle items are marked unrouted and
are not returned as pending fallback output from InteractionRuntime.handle().
If bridge delivery fails after history was written, the history row remains
durable. Non-text delivery with write_history=True must provide explicit
history_text; the host does not invent artifact placeholder text. Optional
failure_history_text can replace the history row on first failure. Later retry
status updates must not rewrite that body.
The host claims a delivery before platform I/O starts. If the process crashes
after sending and before a platform result is durably recorded, recovery marks
that delivery unknown instead of replaying it automatically. A channel that
can reconcile platform state may resolve unknown; otherwise it remains
operator-visible state.
Operator-facing status should be read through HostWorkLifecycleRuntime
instead of separately joining outbox and runtime_work_items. The lifecycle
view reports the effective delivery status while retaining the underlying
durable claim state for debugging.
Subagents
Child agent runs use separate session_child_* sessions. The router does not
know parent/child lineage. Child ordinary output and tool lifecycle delivery go
only to a route explicitly bound for the child session. Without such a route,
they are unrouted; the parent sees the child only through explicit
AgentRunResult, task completion, or future observability events.
Boundary
Do not let output modules write session history or channel state directly. Do not route delivery by parent/child relationships, conversation keys, or ambient adapter state.