Gateway session manager.
One process-global registry over the live session fleet: per-session
ordered event log (monotonic :seq, ring-buffered), SSE subscriber
fan-out, async turn submission with idempotency keys, cancellation,
and turn/cost metrics.
The engine is reached ONLY through the same internal surfaces the
TUI/Telegram channels use: loop/create!-send!-close! for the
lifecycle, :hooks {:on-chunk ...} phased chunks for the live
stream, ctx-loop/session-snapshot for the context. No engine state
lives here - this namespace owns wire bookkeeping (events, turn
records, subscribers), nothing else.
Gateway session manager.
One process-global registry over the live session fleet: per-session
ordered event log (monotonic `:seq`, ring-buffered), SSE subscriber
fan-out, async turn submission with idempotency keys, cancellation,
and turn/cost metrics.
The engine is reached ONLY through the same internal surfaces the
TUI/Telegram channels use: `loop/create!`-`send!`-`close!` for the
lifecycle, `:hooks {:on-chunk ...}` phased chunks for the live
stream, `ctx-loop/session-snapshot` for the context. No engine state
lives here - this namespace owns wire bookkeeping (events, turn
records, subscribers), nothing else.(append-event! sid type payload)(append-event! sid type payload {:keys [store?]})Append one event for sid, fan it out to LOCAL subscribers, and publish
it on the cross-process bus so watchers in OTHER processes stream it too.
Assigns the next monotonic :seq atomically. :store? false events
are fanned out live but kept OUT of the replay ring, so neither a
cursor replay nor a /poll pull (both read the ring) re-delivers
them - reserve it for genuinely ephemeral fan-out where SSE/poll
divergence is acceptable (no current caller; :store? defaults true).
A subscriber sink that throws is dropped - one dead SSE connection
must never poison the appender or sibling subscribers.
Append one event for `sid`, fan it out to LOCAL subscribers, and publish it on the cross-process bus so watchers in OTHER processes stream it too. Assigns the next monotonic `:seq` atomically. `:store? false` events are fanned out live but kept OUT of the replay ring, so neither a cursor replay nor a `/poll` pull (both read the ring) re-delivers them - reserve it for genuinely ephemeral fan-out where SSE/poll divergence is acceptable (no current caller; `:store?` defaults true). A subscriber sink that throws is dropped - one dead SSE connection must never poison the appender or sibling subscribers.
(attach-turn-sync! sid tid {:keys [on-event]})Attach to an ALREADY-submitted turn tid on sid and block until it reaches a
terminal event, returning the same engine-shaped result as submit-turn-sync!.
Creates NO new turn: it drives in-process (TUI) rendering for a turn the gateway
queued and then auto-drains, so a busy-time submission becomes a real gateway
queued record instead of a client-side shadow queue. Optional :on-event fires
for every replay/live event of tid.
Attach to an ALREADY-submitted turn `tid` on `sid` and block until it reaches a terminal event, returning the same engine-shaped result as `submit-turn-sync!`. Creates NO new turn: it drives in-process (TUI) rendering for a turn the gateway queued and then auto-drains, so a busy-time submission becomes a real gateway queued record instead of a client-side shadow queue. Optional `:on-event` fires for every replay/live event of `tid`.
(cancel-turn! sid tid)Fire the cancellation token of a running turn. Returns
{:status "cancelling"} or {:error ...}.
Fire the cancellation token of a running turn. Returns
`{:status "cancelling"}` or `{:error ...}`.(close-session! sid)DELETE a session: dispose the live environment, trash the session's draft clones (primary + auto-cloned filesystem roots — only DRAFTS have clones; a trunk workspace's roots are the user's real dirs and are never touched), then delete the session tree. Idempotent. NOTE: this is the DELETE path — merely quitting/closing a session (navigating away, no server call) keeps the draft intact so it can be resumed.
DELETE a session: dispose the live environment, trash the session's draft clones (primary + auto-cloned filesystem roots — only DRAFTS have clones; a trunk workspace's roots are the user's real dirs and are never touched), then delete the session tree. Idempotent. NOTE: this is the DELETE path — merely quitting/closing a session (navigating away, no server call) keeps the draft intact so it can be resumed.
(context-snapshot sid)The read-only ctx mirror the model sees as its bound session
(ctx-loop/session-snapshot), for an existing session, ENRICHED for
the USER with :session/archived (the GC'd/summarized entities that are
no longer in the model's live ctx). Resolving the env
through lp/env-for rehydrates an evicted session on demand.
nil when the session does not exist.
The read-only ctx mirror the model sees as its bound `session` (`ctx-loop/session-snapshot`), for an existing session, ENRICHED for the USER with `:session/archived` (the GC'd/summarized entities that are no longer in the model's live ctx). Resolving the env through `lp/env-for` rehydrates an evicted session on demand. nil when the session does not exist.
(create-session! {:keys [channel title external-id workspace-id]})Create a fresh gateway-managed session. Defaults to :api, but in-process
clients such as the TUI can pass :channel :tui and still use the same
gateway turn/event machinery without pretending to be an HTTP client.
Create a fresh gateway-managed session. Defaults to `:api`, but in-process clients such as the TUI can pass `:channel :tui` and still use the same gateway turn/event machinery without pretending to be an HTTP client.
(current-seq sid)Highest event :seq assigned for sid so far. Subscribing with this
as the cursor yields a live-only stream (empty replay).
Highest event `:seq` assigned for `sid` so far. Subscribing with this as the cursor yields a live-only stream (empty replay).
(delete-queued-turn! sid tid)Remove a queued turn before it starts. Returns deleted status or an error.
Remove a queued turn before it starts. Returns deleted status or an error.
(events-since sid cursor)Read-only peek at the replay ring: stored events with :seq > cursor,
oldest first. Lets a page renderer locate the running turn's
turn.started seq so its SSE reconnect can replay the WHOLE in-flight
turn instead of only what happens after connect.
Read-only peek at the replay ring: stored events with `:seq` > cursor, oldest first. Lets a page renderer locate the running turn's `turn.started` seq so its SSE reconnect can replay the WHOLE in-flight turn instead of only what happens after connect.
(get-turn sid tid)Wire view of one turn record, or nil.
Wire view of one turn record, or nil.
(ingest-mirrored-event! sid store? event)Deliver a FOREIGN gateway event (produced in another process, arriving via the cross-process bus) into THIS process's registry so a web / Telegram / TUI watcher streams a turn running elsewhere in real time.
The foreign event is RE-SEQUENCED onto this process's OWN monotonic :seq,
never the producer's. Each process runs an independent seq counter, but the
SSE wire treats :seq as a single strictly-increasing per-connection cursor;
adopting the producer's raw counter would let a watcher whose local seq is
already past that value (e.g. it ran an earlier turn on this session) silently
drop the entire foreign turn. Re-sequencing keeps THIS process's stream
monotonic for its own subscribers regardless of the producer's counter — and
is safe because only the producer persists the turn; the mirror is live-only.
Stored in the ring when store?; :current-turn mirrored so the session
list lights up while the turn runs elsewhere. A running TURN ROW is
materialized in :turns/:turn-order on turn.started (and marked terminal
on turn.completed/turn.failed) so list-turns frames the mirrored turn
exactly like a locally-started one — user bubble, running chip, correct live
placement — instead of leaking bare deltas under the previous answer.
Ignores sessions this process has never touched (no local registry entry), so no state accrues for conversations nobody here is watching.
Deliver a FOREIGN gateway event (produced in another process, arriving via the cross-process bus) into THIS process's registry so a web / Telegram / TUI watcher streams a turn running elsewhere in real time. The foreign event is RE-SEQUENCED onto this process's OWN monotonic `:seq`, never the producer's. Each process runs an independent seq counter, but the SSE wire treats `:seq` as a single strictly-increasing per-connection cursor; adopting the producer's raw counter would let a watcher whose local seq is already past that value (e.g. it ran an earlier turn on this session) silently drop the entire foreign turn. Re-sequencing keeps THIS process's stream monotonic for its own subscribers regardless of the producer's counter — and is safe because only the producer persists the turn; the mirror is live-only. Stored in the ring when `store?`; `:current-turn` mirrored so the session list lights up while the turn runs elsewhere. A running TURN ROW is materialized in `:turns`/`:turn-order` on `turn.started` (and marked terminal on `turn.completed`/`turn.failed`) so `list-turns` frames the mirrored turn exactly like a locally-started one — user bubble, running chip, correct live placement — instead of leaking bare deltas under the previous answer. Ignores sessions this process has never touched (no local registry entry), so no state accrues for conversations nobody here is watching.
(list-sessions)(list-sessions channel)Wire souls for every persisted session.
CROSS-CHANNEL by default (channel = :all): a conversation started
in the web is visible in the TUI and vice-versa. Pass a specific
channel keyword only when a caller genuinely needs a single-channel
slice (e.g. Telegram resolving a chat by external-id).
Wire souls for every persisted session. CROSS-CHANNEL by default (`channel` = `:all`): a conversation started in the web is visible in the TUI and vice-versa. Pass a specific channel keyword only when a caller genuinely needs a single-channel slice (e.g. Telegram resolving a chat by external-id).
(list-turns sid)Wire views of every turn for sid, newest first: persisted history hydrated
from the engine DB (survives daemon restarts), plus only genuinely live gateway
overlay rows (running/queued or terminal rows not yet visible in persistence).
DEDUP: the gateway's tid is NOT the engine's persisted row id - the engine
mints its own id inside send!. Once the durable row is visible, prefer it:
it owns the iteration trace. Keeping the completed gateway row alongside the
persisted row rendered the last request/response twice after refresh, with the
transient duplicate missing the iterations disclosure.
Wire views of every turn for `sid`, newest first: persisted history hydrated from the engine DB (survives daemon restarts), plus only genuinely live gateway overlay rows (running/queued or terminal rows not yet visible in persistence). DEDUP: the gateway's `tid` is NOT the engine's persisted row id - the engine mints its own id inside `send!`. Once the durable row is visible, prefer it: it owns the iteration trace. Keeping the completed gateway row alongside the persisted row rendered the last request/response twice after refresh, with the transient duplicate missing the iterations disclosure.
(metrics-snapshot)Global + per-session counters for /metrics.
Global + per-session counters for /metrics.
(reconcile-running-turns!)Gateway facade for startup/client resume reconciliation of orphaned running turns.
Gateway facade for startup/client resume reconciliation of orphaned running turns.
(release-session! sid)Release the live runtime for a session while keeping persisted data resumable.
This is the gateway facade for local clients that are merely closing a view
(for example a TUI tab or process exit). Use close-session! for DELETE.
Release the live runtime for a session while keeping persisted data resumable. This is the gateway facade for local clients that are merely closing a view (for example a TUI tab or process exit). Use `close-session!` for DELETE.
(session-model sid)The session's persisted model preference as {:provider :model}
(DB-backed shared store), or nil for the router default.
The session's persisted model preference as `{:provider :model}`
(DB-backed shared store), or nil for the router default.(session-model-cached sid)Cached variant of session-model for hot render paths. Still part of the
gateway facade: callers do not reach into the session-model store directly.
Cached variant of `session-model` for hot render paths. Still part of the gateway facade: callers do not reach into the session-model store directly.
(session-workspace-info sid)Workspace state for a channel surface (the web footer AND the TUI
directory picker): {:id :draft? :root :repo-root :label :fork-ms :filesystem-roots} for the session pinned to sid (soul id), or nil.
:id is the workspace-id every filesystem-root mutation
(add/remove-filesystem-root!) needs — WITHOUT it the TUI picker treats the
session as read-only and C-a silently no-ops. :filesystem-roots is the
normalized [{:trunk :clone :fork-ms}]. Lets the footer announce that the
session — and its extra roots — are isolated drafts. Resolves soul → latest
state → workspace; never throws.
Workspace state for a channel surface (the web footer AND the TUI
directory picker): `{:id :draft? :root :repo-root :label :fork-ms
:filesystem-roots}` for the session pinned to `sid` (soul id), or nil.
`:id` is the workspace-id every filesystem-root mutation
(`add/remove-filesystem-root!`) needs — WITHOUT it the TUI picker treats the
session as read-only and C-a silently no-ops. `:filesystem-roots` is the
normalized `[{:trunk :clone :fork-ms}]`. Lets the footer announce that the
session — and its extra roots — are isolated drafts. Resolves soul → latest
state → workspace; never throws.(set-session-model! sid provider model)Set (or clear, with blank model) the per-session PROVIDER + MODEL
preference. Every turn submitted for sid routes through it (the engine
reads it at turn start; router-for-model hoists the model, an unknown
name degrades to the default order). Channel-agnostic: web + TUI + embedded
callers all set it here, persisted in the DB and shared across channels.
Set (or clear, with blank model) the per-session PROVIDER + MODEL preference. Every turn submitted for `sid` routes through it (the engine reads it at turn start; `router-for-model` hoists the model, an unknown name degrades to the default order). Channel-agnostic: web + TUI + embedded callers all set it here, persisted in the DB and shared across channels.
(soul sid)Wire soul for one session: persisted record + live gateway status.
Wire soul for one session: persisted record + live gateway status.
(submit-turn! sid
{:keys [request messages idempotency-key model reasoning-default
cancel-token extra-body turn-features workspace
engine-opts attachments]})Submit one turn for sid. Async: starts immediately when idle, otherwise queues.
Returns {:turn record} (plus :idempotent? true on an idempotency
replay) or {:error :session-not-found | :invalid-request, ...}. One engine
turn still runs per session; busy submissions become visible queued records.
Submit one turn for `sid`. Async: starts immediately when idle, otherwise queues.
Returns `{:turn record}` (plus `:idempotent? true` on an idempotency
replay) or `{:error :session-not-found | :invalid-request, ...}`. One engine
turn still runs per session; busy submissions become visible queued records.(submit-turn-sync! sid {:keys [on-event] :as opts})Submit one turn through the gateway and block until that turn reaches a terminal event.
Accepts the same request keys as submit-turn!; optional :on-event is called
for every replay/live event for the submitted turn. Returns an engine-shaped
result map for in-process clients (CLI/TUI/Telegram) that need a blocking
call without bypassing the canonical gateway machinery.
Submit one turn through the gateway and block until that turn reaches a terminal event. Accepts the same request keys as `submit-turn!`; optional `:on-event` is called for every replay/live event for the submitted turn. Returns an engine-shaped result map for in-process clients (CLI/TUI/Telegram) that need a blocking call without bypassing the canonical gateway machinery.
(subscribe! sid sub-id sink cursor)Register an SSE sink and return the replay vector (events with
:seq > cursor) ATOMICALLY with the registration, so no event can
fall between replay and live fan-out. The caller serializes replay
writes against live sink calls (see server.clj).
Before capturing replay, HYDRATE any turn currently running in a sibling
process from the cross-process journal (bus/hydrate!) — but only when this
process isn't already tracking a live turn (:current-turn unset), so an
already-mirrored turn isn't re-delivered to existing subscribers. This
materializes the running turn's row + ring HERE, so a watcher joining a turn
in flight elsewhere replays it from turn.started (user bubble + running
frame) instead of catching only the bare deltas after connect.
Register an SSE sink and return the replay vector (events with `:seq` > `cursor`) ATOMICALLY with the registration, so no event can fall between replay and live fan-out. The caller serializes replay writes against live sink calls (see server.clj). Before capturing replay, HYDRATE any turn currently running in a sibling process from the cross-process journal (`bus/hydrate!`) — but only when this process isn't already tracking a live turn (`:current-turn` unset), so an already-mirrored turn isn't re-delivered to existing subscribers. This materializes the running turn's row + ring HERE, so a watcher joining a turn in flight elsewhere replays it from `turn.started` (user bubble + running frame) instead of catching only the bare deltas after connect.
(transcript sid)Rich persisted transcript rows for sid: turns oldest-first, each carrying
its persisted iteration rows under :iterations. This is the gateway facade
escape hatch for in-process renderers that need full historical trace detail
without reaching around the gateway into persistence directly.
Rich persisted transcript rows for `sid`: turns oldest-first, each carrying its persisted iteration rows under `:iterations`. This is the gateway facade escape hatch for in-process renderers that need full historical trace detail without reaching around the gateway into persistence directly.
(update-queued-turn! sid tid request)Replace the prompt text for a queued turn. Returns the updated turn or an error.
Replace the prompt text for a queued turn. Returns the updated turn or an error.
(warm-db!)Force the persistence backend + shared connection on the CALLER's thread. The gateway runs this on its single-threaded boot path so the heavyweight backend namespace never lazy-loads under request concurrency (see require-backend-ns! in internal/persistance.clj).
Force the persistence backend + shared connection on the CALLER's thread. The gateway runs this on its single-threaded boot path so the heavyweight backend namespace never lazy-loads under request concurrency (see require-backend-ns! in internal/persistance.clj).
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |