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 channel uses: 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 channel uses: `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.(abandon-draft! sid workspace-id reason)Permanently discard one active draft owned by sid's current repo. A parked
draft may be removed directly. If it is the caller's current draft, first
repoint the session to that draft's real trunk. Drafts from another repo or
pinned to another session are rejected. Returns refreshed canonical workspace
info.
Permanently discard one active draft owned by `sid`'s current repo. A parked draft may be removed directly. If it is the caller's current draft, first repoint the session to that draft's real trunk. Drafts from another repo or pinned to another session are rejected. Returns refreshed canonical workspace info.
(add-event-tap! k f)Register f ([sid event], canonical string-keyed event) under k, replacing
any previous tap with that key. A tap that throws is swallowed — an observer
must never break the appender.
Register `f` (`[sid event]`, canonical string-keyed event) under `k`, replacing any previous tap with that key. A tap that throws is swallowed — an observer must never break the appender.
(add-filesystem-root! sid path)Add path as an extra filesystem root for the session pinned to sid, then
return the refreshed session-workspace-info. Runs SERVER-SIDE in the daemon
so the draft backend-fork and DB write land where the session actually lives;
every channel (web footer, TUI picker/footer) then reads the same roots back
over the gateway. Channel-agnostic twin of set-session-model!.
Add `path` as an extra filesystem root for the session pinned to `sid`, then return the refreshed `session-workspace-info`. Runs SERVER-SIDE in the daemon so the draft backend-fork and DB write land where the session actually lives; every channel (web footer, TUI picker/footer) then reads the same roots back over the gateway. Channel-agnostic twin of `set-session-model!`.
(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.
The event is normalized to THE canonical wire shape (wire/canonical:
snake_case STRING keys) BEFORE it is stored/fanned/published, so every
consumer — in-process sink, replay ring, /poll, SSE, journal tail —
reads the IDENTICAL string-keyed map.
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. The event is normalized to THE canonical wire shape (`wire/canonical`: snake_case STRING keys) BEFORE it is stored/fanned/published, so every consumer — in-process sink, replay ring, `/poll`, SSE, journal tail — reads the IDENTICAL string-keyed map. 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.
(assign-project! sid pid)Assign a session to pid (nil clears / removes from project). Returns the refreshed soul.
Assign a session to `pid` (nil clears / removes from project). Returns the refreshed soul.
(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 (canonical string-keyed) 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 (canonical string-keyed) of `tid`.
(attachment-bytes {:keys [base64 storage-uri]})Raw bytes for ONE attachment map (an iteration-attachments element): its
inline :base64 decoded, else its external :storage-uri fetched through the
storage rail. nil when neither resolves.
Raw bytes for ONE attachment map (an [[iteration-attachments]] element): its inline `:base64` decoded, else its external `:storage-uri` fetched through the storage rail. nil when neither resolves.
(cancel-all-running!)Fire the cancellation token of EVERY running turn across all sessions. Called on gateway shutdown to break in-flight provider loops BEFORE the shared HTTP executor is torn down — a looping turn would otherwise redispatch its next iteration into the dying pool and die with a RejectedExecutionException surfaced as a bogus "Provider unavailable". Best-effort; returns the number of turns signalled.
Fire the cancellation token of EVERY running turn across all sessions. Called on gateway shutdown to break in-flight provider loops BEFORE the shared HTTP executor is torn down — a looping turn would otherwise redispatch its next iteration into the dying pool and die with a RejectedExecutionException surfaced as a bogus "Provider unavailable". Best-effort; returns the number of turns signalled.
(cancel-current-turn! sid)Tid-less twin of cancel-turn!: fire the cancellation token of WHATEVER
turn currently holds sid's :current-turn slot. For clients that lost
(or never learned) the turn id — an Esc that raced the turn.started
late-bind, or a client-side cancel self-heal that dropped its
:gateway-turn-id while the server turn kept running. Without this, that
ghost turn keeps :current-turn and every next submit silently queues
behind it. Returns {:status "cancelling" :turn_id tid} or
{:error :no-running-turn}.
Tid-less twin of `cancel-turn!`: fire the cancellation token of WHATEVER
turn currently holds `sid`'s `:current-turn` slot. For clients that lost
(or never learned) the turn id — an Esc that raced the `turn.started`
late-bind, or a client-side cancel self-heal that dropped its
`:gateway-turn-id` while the server turn kept running. Without this, that
ghost turn keeps `:current-turn` and every next submit silently queues
behind it. Returns `{:status "cancelling" :turn_id tid}` or
`{:error :no-running-turn}`.(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 ...}`.(change-root! sid path)Repoint the session pinned to sid at path as its PRIMARY root, then return
the refreshed session-workspace-info (whose :id is the newly pinned
workspace). Server-side so the change lands in the daemon that runs the turns.
Repoint the session pinned to `sid` at `path` as its PRIMARY root, then return the refreshed `session-workspace-info` (whose `:id` is the newly pinned workspace). Server-side so the change lands in the daemon that runs the turns.
(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-draft! sid label blank?)Create and enter a named draft for sid in the daemon. If the session is
already in a draft, park that draft first; creating from the picker is thus
non-destructive and always forks the real repo trunk. blank? creates an
empty draft lineage. Returns refreshed canonical workspace info.
Create and enter a named draft for `sid` in the daemon. If the session is already in a draft, park that draft first; creating from the picker is thus non-destructive and always forks the real repo trunk. `blank?` creates an empty draft lineage. Returns refreshed canonical workspace info.
(create-session! {:keys [channel external-id workspace-id root] :as opts})Create or adopt a gateway-managed session.
Ordinary default-workspace creates consume the gateway-owned warm pool and
replenish it in the background. :workspace-id, :root, and :external-id
require a purpose-built environment and bypass the pool.
Create or adopt a gateway-managed session. Ordinary default-workspace creates consume the gateway-owned warm pool and replenish it in the background. `:workspace-id`, `:root`, and `:external-id` require a purpose-built environment and bypass the pool.
(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-project! pid)Delete a project; its member sessions scatter back to project-less (never deleted).
Delete a project; its member sessions scatter back to project-less (never deleted).
(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.
(discard-prewarmed!)Cancel warmups and delete every unused pooled session. Gateway shutdown owns this cleanup; channel shutdowns must not discard a pool shared by other clients.
Cancel warmups and delete every unused pooled session. Gateway shutdown owns this cleanup; channel shutdowns must not discard a pool shared by other clients.
(drain-idle! sid)Start the oldest queued turn for sid IF the session is idle (no turn in
flight). No-op returning nil otherwise. Lets an attaching channel kick an
orphaned backlog — submitted from another channel while this one was away —
into motion the moment a client opens/resumes, instead of letting it sit
forever.
The cancel provenance gate is NOT re-implemented here: drain-next-queued!
owns it for every caller, so a backlog the user stopped with Esc can never be
resurrected by a background attach (tab open, project switch) either.
Safe to call redundantly: drain-next-queued! guards on :current-turn.
Start the oldest queued turn for `sid` IF the session is idle (no turn in flight). No-op returning nil otherwise. Lets an attaching channel kick an orphaned backlog — submitted from another channel while this one was away — into motion the moment a client opens/resumes, instead of letting it sit forever. The cancel provenance gate is NOT re-implemented here: `drain-next-queued!` owns it for every caller, so a backlog the user stopped with Esc can never be resurrected by a background attach (tab open, project switch) either. Safe to call redundantly: `drain-next-queued!` guards on `:current-turn`.
(ensure-prewarmed! channel)Asynchronously top up the gateway-owned warm-session pool for channel.
Idempotent and race-safe: ready plus in-flight sessions never exceed the pool
depth. Does nothing after gateway shutdown has stopped pool acceptance.
Asynchronously top up the gateway-owned warm-session pool for `channel`. Idempotent and race-safe: ready plus in-flight sessions never exceed the pool depth. Does nothing after gateway shutdown has stopped pool acceptance.
(ensure-project-for-root! root)(ensure-project-for-root! owner-id root name)Get-or-create the wire project bound to canonical workspace root.
Get-or-create the wire project bound to canonical workspace `root`.
(events-since sid cursor)Read-only peek at the replay ring: stored canonical (string-keyed) 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 canonical (string-keyed) 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-project-by-root root)(get-project-by-root owner-id root)Wire project bound to canonical workspace root for owner-id (default
"local"), or nil.
Wire project bound to canonical workspace `root` for `owner-id` (default "local"), or nil.
(get-turn sid tid)Canonical (string-keyed) wire view of one turn record, or nil.
Canonical (string-keyed) 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, already in the canonical string-keyed wire shape) into THIS process's registry so a 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, already in the canonical string-keyed wire shape) into THIS process's registry so a 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.
(iteration-attachments iid)Ordered OUTBOUND artifacts (matplotlib figures / produced images) a tool call
persisted under iteration iid, in the db-list-iteration-attachments shape
(each hydrated with inline :base64 or an external :storage-uri), or [].
THE canonical, ordered list the attachment byte endpoint indexes AND the live
iteration.completed descriptors mirror — so index N always names the same
artifact live, on re-fetch, and across a restart. nil/unparsable id -> [].
Ordered OUTBOUND artifacts (matplotlib figures / produced images) a tool call persisted under iteration `iid`, in the `db-list-iteration-attachments` shape (each hydrated with inline `:base64` or an external `:storage-uri`), or `[]`. THE canonical, ordered list the attachment byte endpoint indexes AND the live `iteration.completed` descriptors mirror — so index N always names the same artifact live, on re-fetch, and across a restart. nil/unparsable id -> `[]`.
(list-drafts sid)Active/stashed DRAFTS for the repo the session pinned to sid lives in, in
THE canonical string-keyed wire shape
[{"workspace_id" "label" "root" "repo_root" "fork_ms" "is_current"}],
newest first — the parked drafts any channel can resume-draft!. The session's
own current draft (when it is in one) rides "is_current" true. Server-side
twin of the /draft list slash: ONE gateway fact every channel (web picker,
TUI drafts view) reads instead of typing the slash. Never throws; returns []
when the session or its repo is unknown.
Active/stashed DRAFTS for the repo the session pinned to `sid` lives in, in
THE canonical string-keyed wire shape
`[{"workspace_id" "label" "root" "repo_root" "fork_ms" "is_current"}]`,
newest first — the parked drafts any channel can `resume-draft!`. The session's
own current draft (when it is in one) rides `"is_current" true`. Server-side
twin of the `/draft list` slash: ONE gateway fact every channel (web picker,
TUI drafts view) reads instead of typing the slash. Never throws; returns []
when the session or its repo is unknown.(list-projects)(list-projects opts)Wire projects for one owner view (see loop/projects) — projects are
cross-channel. opts keys: :owner-id, :include-archived?.
Wire projects for one owner view (see loop/projects) — projects are cross-channel. `opts` keys: :owner-id, :include-archived?.
(list-queued-turns sid)ONLY the still-queued rows for sid, oldest-first — the exact slice a tray
polls for, and nothing else.
A queued turn lives solely in the in-memory registry overlay: persistence
never holds one (a row reaches the DB after it RUNS). So this reads the
overlay and skips list-turns's whole-history DB hydration. That matters
on the wire, not just in the server: a companion polling the backlog every
5s was pulling the session's ENTIRE turn history — 600KB of completed
:content for a long session — just to learn the queue is empty.
ONLY the still-queued rows for `sid`, oldest-first — the exact slice a tray polls for, and nothing else. A queued turn lives solely in the in-memory registry overlay: persistence never holds one (a row reaches the DB after it RUNS). So this reads the overlay and skips [[list-turns]]'s whole-history DB hydration. That matters on the wire, not just in the server: a companion polling the backlog every 5s was pulling the session's ENTIRE turn history — 600KB of completed `:content` for a long session — just to learn the queue is empty.
(list-sessions)(list-sessions channel)Wire souls for every persisted session, each decorated with the bulk
summary facts (turn_count, modified_at, lean workspace) so ONE
GET /v1/sessions is enough to paint a session picker.
The gateway owns navigator ordering: live sessions first, followed by idle sessions in most-recently-active order. Clients must preserve this order.
CROSS-CHANNEL by default (channel = :all): a conversation started
in one channel is visible in the others and vice-versa. Pass a specific
channel keyword only when a caller genuinely needs a single-channel
slice (e.g. resolving a chat by external-id).
Wire souls for every persisted session, each decorated with the bulk summary facts (`turn_count`, `modified_at`, lean `workspace`) so ONE `GET /v1/sessions` is enough to paint a session picker. The gateway owns navigator ordering: live sessions first, followed by idle sessions in most-recently-active order. Clients must preserve this order. CROSS-CHANNEL by default (`channel` = `:all`): a conversation started in one channel is visible in the others and vice-versa. Pass a specific channel keyword only when a caller genuinely needs a single-channel slice (e.g. resolving a chat by external-id).
(list-turns sid)Canonical (string-keyed) wire views of every turn for sid, OLDEST-first (chronological, chat order):
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). Ordering matches transcript so every consumer
renders top-to-bottom directly — no channel re-reverses for display.
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.
Canonical (string-keyed) wire views of every turn for `sid`, OLDEST-first (chronological, chat order): 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). Ordering matches `transcript` so every consumer renders top-to-bottom directly — no channel re-reverses for display. 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, concurrency, replay-buffer, and JVM gauges for /metrics.
Global, per-session, concurrency, replay-buffer, and JVM gauges for /metrics.
(queue-paused-info sid)The live :queue-paused marker for sid ({:reason :held :fails :gen …}),
or nil when the queue is running.
The live `:queue-paused` marker for `sid` (`{:reason :held :fails :gen …}`),
or nil when the queue is running.(reconcile-orphaned-turns!)Mark turns left running by a dead process as interrupted.
Queued work is deliberately memory-only. Startup never reconstructs or resubmits messages from persisted user requests. Returns the persistence sweep result.
Mark turns left running by a dead process as interrupted. Queued work is deliberately memory-only. Startup never reconstructs or resubmits messages from persisted user requests. Returns the persistence sweep result.
(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.
Background resources (background shell processes, managed REPLs) are STOPPED here:
closing the view is the user walking away, and a bg child must not outlive
that — the transcript stays resumable, the processes do not.
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. Background resources (background `shell` processes, managed REPLs) are STOPPED here: closing the view is the user walking away, and a bg child must not outlive that — the transcript stays resumable, the processes do not.
(remove-filesystem-root! sid path)Remove path from the session's extra filesystem roots and return the
refreshed session-workspace-info. Server-side twin of add-filesystem-root!.
Remove `path` from the session's extra filesystem roots and return the refreshed `session-workspace-info`. Server-side twin of `add-filesystem-root!`.
(reorder-project-sessions! pid session-ids)Atomically adopt loose named sessions into pid, then persist their manual
order. Guests owned by another project are never stolen. Returns the member
count applied.
Atomically adopt loose named sessions into `pid`, then persist their manual order. Guests owned by another project are never stolen. Returns the member count applied.
(resume-draft! sid workspace-id)Switch the session pinned to sid INTO the stashed draft workspace-id, then
return the refreshed session-workspace-info. The target is validated against
the session's current repo BEFORE any current draft is stashed. When the session
is currently in another draft it is then stashed non-destructively, so this is a
true draft switch, not just an enter-from-trunk. Runs SERVER-SIDE in the daemon.
Throws ex-info with a :type when workspace-id is not resumable (see
workspace/resume!). Channel-agnostic twin of the /draft resume slash.
Switch the session pinned to `sid` INTO the stashed draft `workspace-id`, then return the refreshed `session-workspace-info`. The target is validated against the session's current repo BEFORE any current draft is stashed. When the session is currently in another draft it is then stashed non-destructively, so this is a true draft switch, not just an enter-from-trunk. Runs SERVER-SIDE in the daemon. Throws `ex-info` with a `:type` when `workspace-id` is not resumable (see `workspace/resume!`). Channel-agnostic twin of the `/draft resume` slash.
(resume-queue! sid {:keys [auto?]})Clear a paused backlog and start its head. :auto? marks a breaker/backoff
auto-resume; an explicit (user) resume ALSO resets the consecutive-failure
counter so the breaker re-arms. No-op when the queue is not paused. Returns
the started turn, or nil.
Clear a paused backlog and start its head. `:auto?` marks a breaker/backoff auto-resume; an explicit (user) resume ALSO resets the consecutive-failure counter so the breaker re-arms. No-op when the queue is not paused. Returns the started turn, or nil.
(running-turn-count)Number of live turns currently owned by this gateway process. Used by the daemon lifecycle gate: the server may only self-stop when this is zero AND the client refcount is zero.
Number of live turns currently owned by this gateway process. Used by the daemon lifecycle gate: the server may only self-stop when this is zero AND the client refcount is zero.
(running-turn-start-cursor sid)For a live-only subscriber joining a session mid-turn: the cursor (one below
the currently-running turn's turn.started seq) that replays the WHOLE
in-flight turn — user bubble, thinking, forms, activity — instead of only the
deltas that happen after connect. This is what lets a companion/web client
that OPENS a session already driven from the TUI paint the same live 'Vis is
running: …' bubble the originating channel shows. nil when no turn is running
locally or its start seq wasn't recorded (a foreign turn is handled instead by
subscribe!'s hydrate, which appends it above the live-only cursor).
For a live-only subscriber joining a session mid-turn: the cursor (one below the currently-running turn's `turn.started` seq) that replays the WHOLE in-flight turn — user bubble, thinking, forms, activity — instead of only the deltas that happen after connect. This is what lets a companion/web client that OPENS a session already driven from the TUI paint the same live 'Vis is running: …' bubble the originating channel shows. nil when no turn is running locally or its start seq wasn't recorded (a foreign turn is handled instead by `subscribe!`'s hydrate, which appends it above the live-only cursor).
(search-session-ids query)(search-session-ids channel query)Soul-id STRINGS whose TRANSCRIPT (user request + assistant iteration text)
matches query. The SERVER-side half of transcript search: clients match
title/project locally over the already-loaded list and union these ids for
the deep matches, so the 105MB of assistant text never crosses the wire.
Blank query → [].
Soul-id STRINGS whose TRANSCRIPT (user request + assistant iteration text) matches `query`. The SERVER-side half of transcript search: clients match title/project locally over the already-loaded list and union these ids for the deep matches, so the 105MB of assistant text never crosses the wire. Blank query → [].
(search-session-matches query)(search-session-matches channel query)Soul-id STRINGS whose TRANSCRIPT matches query, each TAGGED with WHERE it hit
and carrying up to a handful of MATCH SNIPPETS:
[{:session_id str :is_in_request bool :is_in_reply bool :request_snippet str :reply_snippet str :hits [{:side "request"|"reply" :snippet str :at ms}]}]
(wire-shaped: snake_case string-ish keys, is_<foo> flags). Same SERVER-side
deep search as search-session-ids — the assistant text never crosses the wire,
only these snippet windows. :is_in_request = the user's own request matched;
:is_in_reply = assistant reply text matched. Blank query → [].
Soul-id STRINGS whose TRANSCRIPT matches `query`, each TAGGED with WHERE it hit
and carrying up to a handful of MATCH SNIPPETS:
`[{:session_id str :is_in_request bool :is_in_reply bool
:request_snippet str :reply_snippet str
:hits [{:side "request"|"reply" :snippet str :at ms}]}]`
(wire-shaped: snake_case string-ish keys, `is_<foo>` flags). Same SERVER-side
deep search as `search-session-ids` — the assistant text never crosses the wire,
only these snippet windows. `:is_in_request` = the user's own request matched;
`:is_in_reply` = assistant reply text matched. Blank query → [].(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), in THE canonical string-keyed wire shape:
{"id" "draft?" "root" "repo_root" "label" "fork_ms" "filesystem_roots" "git"} 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
the PUBLIC element shape [{"dir" "isolated" "draft_dir"}]. 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), in THE canonical string-keyed wire shape:
`{"id" "draft?" "root" "repo_root" "label" "fork_ms"
"filesystem_roots" "git"}` 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
the PUBLIC element shape `[{"dir" "isolated" "draft_dir"}]`. 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)Canonical (string-keyed) wire soul for one session: persisted record + live gateway status. Running sessions include their request, start timestamp, and the gateway clock sampled in the same response so remote channels can derive one elapsed baseline without trusting their device wall clock.
Canonical (string-keyed) wire soul for one session: persisted record + live gateway status. Running sessions include their request, start timestamp, and the gateway clock sampled in the same response so remote channels can derive one elapsed baseline without trusting their device wall clock.
(start-prewarming! channels)Start gateway-owned warm pools for every supplied channel. This is the sole lifecycle entry point; channels never manage pools directly.
Start gateway-owned warm pools for every supplied channel. This is the sole lifecycle entry point; channels never manage pools directly.
(stash-draft! sid)Park the session's current draft — leave the draft row :active and its clone
on disk, repoint the session back to trunk — then return the refreshed
session-workspace-info. The non-destructive twin of abandoning; a no-op that
returns trunk info when the session is already on trunk. Runs SERVER-SIDE in
the daemon that owns the DB. Channel-agnostic twin of the /draft stash slash.
Park the session's current draft — leave the draft row `:active` and its clone on disk, repoint the session back to trunk — then return the refreshed `session-workspace-info`. The non-destructive twin of abandoning; a no-op that returns trunk info when the session is already on trunk. Runs SERVER-SIDE in the daemon that owns the DB. Channel-agnostic twin of the `/draft stash` slash.
(submit-turn! sid
{:keys [request messages idempotency-key model reasoning-default
cancel-token extra-body turn-features workspace
engine-opts attachments display-request]})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 (canonical string-keyed) for the submitted turn.
Returns an engine-shaped result map for in-process clients (CLI/TUI)
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 (canonical string-keyed) for the submitted turn. Returns an engine-shaped result map for in-process clients (CLI/TUI) 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 (canonical string-keyed
events with "seq" > cursor) ATOMICALLY with the registration, so no
event can fall between replay and live fan-out. The sink must be
NON-BLOCKING (fan-out runs on the appending turn thread); the caller
dedups via a seq guard, since a live event may land in both the replay
and the sink (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 (canonical string-keyed events with `"seq"` > `cursor`) ATOMICALLY with the registration, so no event can fall between replay and live fan-out. The sink must be NON-BLOCKING (fan-out runs on the appending turn thread); the caller dedups via a seq guard, since a live event may land in both the replay and the sink (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 in THE canonical wire shape
(wire/canonical): turns oldest-first, each carrying its persisted iteration
rows under :iterations. Canonicalizing AT THE SOURCE makes the HTTP hop an
identity — an in-process reader and a remote gateway client (TUI / web /
mobile) see the SAME maps, so there is exactly ONE transcript shape and a
channel can never again be written against a shape only one transport sees.
Rich persisted transcript rows for `sid` in THE canonical wire shape (`wire/canonical`): turns oldest-first, each carrying its persisted iteration rows under `:iterations`. Canonicalizing AT THE SOURCE makes the HTTP hop an identity — an in-process reader and a remote gateway client (TUI / web / mobile) see the SAME maps, so there is exactly ONE transcript shape and a channel can never again be written against a shape only one transport sees.
(transcript-page sid {:keys [limit offset]})A WINDOW of sid's transcript, hydrated LAZILY — the whole point is that only
the rows in the window pay for iteration/attachment hydration, which is where
a big session's cost lives (a 247-turn session: ~26 ms to list, ~750 ms to
hydrate all of it, ~50 ms to hydrate the newest 30).
The cursor is an INDEX into the oldest-first list, NOT :position — positions
are neither unique nor monotonic in practice (one real 247-turn session has
172 distinct positions), so they cannot page anything. New turns only ever
append, so an offset counted from the OLDEST row is stable while paging
backwards.
A windowed request is ALSO capped in bytes (TRANSCRIPT_PAGE_MAX_BYTES): the
newest rows are hydrated first and the oldest ones fall out of the page, so
:offset can come back HIGHER than the one asked for. Clients must page from
the RETURNED :offset, never from their own arithmetic.
opts: :limit window size (nil = every row, unbudgeted — the TUI's
whole-transcript read), :offset 0-based start in the oldest-first list
(nil = the NEWEST :limit rows).
Returns {:turns <oldest-first window> :total <turn count> :offset <window start> :has-more <older rows exist>}.
A WINDOW of `sid`'s transcript, hydrated LAZILY — the whole point is that only
the rows in the window pay for iteration/attachment hydration, which is where
a big session's cost lives (a 247-turn session: ~26 ms to list, ~750 ms to
hydrate all of it, ~50 ms to hydrate the newest 30).
The cursor is an INDEX into the oldest-first list, NOT `:position` — positions
are neither unique nor monotonic in practice (one real 247-turn session has
172 distinct positions), so they cannot page anything. New turns only ever
append, so an offset counted from the OLDEST row is stable while paging
backwards.
A windowed request is ALSO capped in bytes (`TRANSCRIPT_PAGE_MAX_BYTES`): the
newest rows are hydrated first and the oldest ones fall out of the page, so
`:offset` can come back HIGHER than the one asked for. Clients must page from
the RETURNED `:offset`, never from their own arithmetic.
`opts`: `:limit` window size (nil = every row, unbudgeted — the TUI's
whole-transcript read), `:offset` 0-based start in the oldest-first list
(nil = the NEWEST `:limit` rows).
Returns `{:turns <oldest-first window> :total <turn count> :offset <window
start> :has-more <older rows exist>}`.(turn-trace tid)THE canonical wire trace of ONE persisted turn: its iteration rows (each
with hydrated :attachments re-read from the attachment store) through
wire/canonical, same as transcript — canonicalizing AT THE SOURCE
keeps the HTTP hop an identity, so an in-process client and a remote
client (TUI / mobile) render from the SAME maps. Returns a (possibly empty)
vector for a valid turn id, nil for an unparsable id or a read failure —
callers use nil to fall back / retry.
THE canonical wire trace of ONE persisted turn: its iteration rows (each with hydrated `:attachments` re-read from the attachment store) through `wire/canonical`, same as [[transcript]] — canonicalizing AT THE SOURCE keeps the HTTP hop an identity, so an in-process client and a remote client (TUI / mobile) render from the SAME maps. Returns a (possibly empty) vector for a valid turn id, nil for an unparsable id or a read failure — callers use nil to fall back / retry.
(update-queued-turn! sid tid request)Replace the prompt text for a queued turn. Returns the updated turn or an error.
The row's presentation is re-derived from the NEW text: image chips are
re-resolved and the stale :display_request (which described the text the
submitter authored BEFORE this edit) is dropped, so an edited row never
keeps painting the old prompt.
Replace the prompt text for a queued turn. Returns the updated turn or an error. The row's presentation is re-derived from the NEW text: image chips are re-resolved and the stale `:display_request` (which described the text the submitter authored BEFORE this edit) is dropped, so an edited row never keeps painting the old prompt.
(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 |