Liking cljdoc? Tell your friends :D

com.blockether.vis.internal.gateway.state

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.
raw docstring

add-event-tap!clj

(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.
sourceraw docstring

append-event!clj

(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 normally fanned out live but kept out of the replay ring. block.activity is the deliberate exception: the ring materializes exactly its latest snapshot per running form, replacing the previous one, so SSE reconnect and /poll recover canonical current state without retaining the transient stream. 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
normally fanned out live but kept out of the replay ring. `block.activity` is
the deliberate exception: the ring materializes exactly its latest snapshot
per running form, replacing the previous one, so SSE reconnect and `/poll`
recover canonical current state without retaining the transient stream.
A subscriber sink that throws is dropped - one dead SSE connection must never
poison the appender or sibling subscribers.
sourceraw docstring

append-iteration-attachment!clj

(append-iteration-attachment! iid att)

Store a HUMAN's revision of an artifact the model produced, into the very iteration that produced it, and hand back its wire descriptor.

The version rule is the engine's own and lives in the writer: re-using the filename is the next CUT of that artifact, so a note the human annotated in the companion becomes v2 of that note rather than a second file with the same name. Everything a client already knows how to do with an artifact - list it, thread it by name, fetch its bytes by index - then works on the revision unchanged.

att is {:filename :media-type :base64}. nil when the iteration is unknown or the payload could not be stored.

Store a HUMAN's revision of an artifact the model produced, into the very
iteration that produced it, and hand back its wire descriptor.

The version rule is the engine's own and lives in the writer: re-using the
filename is the next CUT of that artifact, so a note the human annotated in
the companion becomes `v2` of that note rather than a second file with the
same name. Everything a client already knows how to do with an artifact -
list it, thread it by name, fetch its bytes by index - then works on the
revision unchanged.

`att` is `{:filename :media-type :base64}`. nil when the iteration is unknown
or the payload could not be stored.
sourceraw docstring

assign-project!clj

(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.
sourceraw docstring

attach-turn-sync!clj

(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`.
sourceraw docstring

attachment-bytesclj

(attachment-bytes {:keys [base64 storage-uri id has-bytes]})

Raw bytes for ONE attachment map (an iteration-attachments element), fetched LAZILY — listing describes, only this reads. An inline :base64 the caller already holds is decoded; an external :storage-uri goes through the storage rail; otherwise the row's own :id is re-read from the store, which is why the metadata listers can stay byte-free.

TOTAL: a corrupt payload, a vanished row or an unreachable storage backend is nil — the endpoint answers a clean 404 instead of throwing a 500 out of a Base64 decoder.

Raw bytes for ONE attachment map (an [[iteration-attachments]] element),
fetched LAZILY — listing describes, only this reads. An inline `:base64` the
caller already holds is decoded; an external `:storage-uri` goes through the
storage rail; otherwise the row's own `:id` is re-read from the store, which
is why the metadata listers can stay byte-free.

TOTAL: a corrupt payload, a vanished row or an unreachable storage backend is
`nil` — the endpoint answers a clean 404 instead of throwing a 500 out of a
`Base64` decoder.
sourceraw docstring

bus-wiringclj

source

cancel-all-running!clj

(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.
sourceraw docstring

cancel-current-turn!clj

(cancel-current-turn! sid owner-key)

Tid-less twin of cancel-turn!: fire the cancellation token of the turn currently holding sid's :current-turn slot, but ONLY when owner-key is the idempotency_key that turn was submitted with. 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. Without this, that ghost turn keeps :current-turn and every next submit silently queues behind it.

A session is SHARED, so "whatever is running here" never proves "the turn I submitted": an unaddressed cancel used to kill the turn another channel (the companion app, the web, a second TUI) was running the moment a client opened the session. The correlation id the submitter already sent is the proof, and a turn submitted without one is reachable only by cancel-turn!'s id-addressed route — which every client learns from turn.started.

Returns {:status "cancelling" :turn_id tid}, {:error :not-owner :turn_id tid} for someone else's turn, or {:error :no-running-turn}.

Tid-less twin of `cancel-turn!`: fire the cancellation token of the turn
currently holding `sid`'s `:current-turn` slot, but ONLY when `owner-key` is
the `idempotency_key` that turn was submitted with. 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`. Without
this, that ghost turn keeps `:current-turn` and every next submit silently
queues behind it.

A session is SHARED, so "whatever is running here" never proves "the turn I
submitted": an unaddressed cancel used to kill the turn another channel (the
companion app, the web, a second TUI) was running the moment a client opened
the session. The correlation id the submitter already sent is the proof, and
a turn submitted without one is reachable only by `cancel-turn!`'s id-addressed
route — which every client learns from `turn.started`.

Returns `{:status "cancelling" :turn_id tid}`, `{:error :not-owner :turn_id
tid}` for someone else's turn, or `{:error :no-running-turn}`.
sourceraw docstring

cancel-session-turns!clj

(cancel-session-turns! sid)
(cancel-session-turns! sid source)

Fire the cancellation token of EVERY running turn in sid, answering the ids signalled.

A stop means "stop what this session is doing", and a session runs more than one turn: the :current-turn slot names ONE of them, so an id-addressed cancel used to leave the rest burning. Measured from a live gateway — a turn cancelled cleanly in 1.3s while two others in the same session kept running for another 56 seconds and spent five more provider requests, until a gateway shutdown finally ended them. The user pressed stop once and watched the session carry on.

Each turn goes through cancel-turn!, so each gets the stamped source, the cancelling_at mark and its own terminal backstop.

Fire the cancellation token of EVERY running turn in `sid`, answering the ids
signalled.

A stop means "stop what this session is doing", and a session runs more than
one turn: the `:current-turn` slot names ONE of them, so an id-addressed
cancel used to leave the rest burning. Measured from a live gateway — a turn
cancelled cleanly in 1.3s while two others in the same session kept running
for another 56 seconds and spent five more provider requests, until a gateway
shutdown finally ended them. The user pressed stop once and watched the
session carry on.

Each turn goes through `cancel-turn!`, so each gets the stamped source, the
`cancelling_at` mark and its own terminal backstop.
sourceraw docstring

cancel-turn!clj

(cancel-turn! sid tid)
(cancel-turn! sid tid source)

Fire the cancellation token of a running turn. Returns {:status "cancelling"} or {:error ...}.

source is stamped on the token and logged. Every cancel reads downstream as one interrupt, so an unattributed cancel leaves a post mortem unable to tell a user stop from the daemon stopping its own turn — this line is the only durable record of WHO stopped it.

Fire the cancellation token of a running turn. Returns
`{:status "cancelling"}` or `{:error ...}`.

`source` is stamped on the token and logged. Every cancel reads downstream
as one interrupt, so an unattributed cancel leaves a post mortem unable to
tell a user stop from the daemon stopping its own turn — this line is the
only durable record of WHO stopped it.
sourceraw docstring

change-root!clj

(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.
sourceraw docstring

close-session!clj

(close-session! sid)

DELETE a session: 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), delete the session tree, and drop it from this process. Idempotent. Returns the teardown Future.

THE RESPONSE COSTS THE DB REMOVAL, NOTHING MORE. Disposing the live runtime (background shells, managed REPLs, the Python session) used to run right here on the request thread, so deleting a session the user had just worked in held DELETE open for seconds — long enough for the companion's confirm modal to read as a frozen screen. Teardown now runs on teardown-session-async!, after the session has already stopped existing for every client.

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: 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), delete the session tree, and drop it
from this process. Idempotent. Returns the teardown Future.

THE RESPONSE COSTS THE DB REMOVAL, NOTHING MORE. Disposing the live runtime
(background shells, managed REPLs, the Python session) used to run right
here on the request thread, so deleting a session the user had just worked in
held DELETE open for seconds — long enough for the companion's confirm modal
to read as a frozen screen. Teardown now runs on `teardown-session-async!`,
after the session has already stopped existing for every client.

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.
sourceraw docstring

context-snapshotclj

(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.
sourceraw docstring

council-operation!clj

(council-operation! sid operation opts)

Transport adapter. Derive publication identity from the owning runtime; idle pings may dispatch a turn.

Transport adapter. Derive publication identity from the owning runtime; idle pings may dispatch a turn.
sourceraw docstring

create-project!clj

(create-project! opts)
source

create-session!clj

(create-session! {:keys [channel] :as opts})

Create one session and answer its wire map.

Cold, always. A pool of empty sessions used to stand in front of this, built at gateway start and refilled after every claim, so the FIRST create in a channel skipped a Python startup. It cost two idle sessions per channel from boot — every one of them a full environment with its own interpreter — to save that one wait, and it paid for them while the gateway was still coming up. The wait belongs to whoever asks for a session.

Create one session and answer its wire map.

Cold, always. A pool of empty sessions used to stand in front of this, built
at gateway start and refilled after every claim, so the FIRST create in a
channel skipped a Python startup. It cost two idle sessions per channel from
boot — every one of them a full environment with its own interpreter — to
save that one wait, and it paid for them while the gateway was still coming
up. The wait belongs to whoever asks for a session.
sourceraw docstring

current-seqclj

(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).
sourceraw docstring

current-turn-idclj

(current-turn-id sid)

Turn id this process is running for sid right now, or nil when the session is idle. The registry's :current-turn mirror: set on turn.started, cleared by that turn's terminal event, and maintained for FOREIGN turns too (a sibling process's turn is hydrated into this registry on subscribe).

This is the one fact a reconnecting client needs and cannot infer from its own stream: whether the turn it is still painting is the turn the daemon is still running. sse-ready! ships it with every subscription so the answer costs no round trip.

Turn id this process is running for `sid` right now, or nil when the session
is idle. The registry's `:current-turn` mirror: set on `turn.started`, cleared
by that turn's terminal event, and maintained for FOREIGN turns too (a sibling
process's turn is hydrated into this registry on subscribe).

This is the one fact a reconnecting client needs and cannot infer from its own
stream: whether the turn it is still painting is the turn the daemon is still
running. `sse-ready!` ships it with every subscription so the answer costs no
round trip.
sourceraw docstring

delete-project!clj

(delete-project! pid)
(delete-project! pid {:keys [is-recursive]})

Delete a project. By DEFAULT its member sessions scatter back to project-less (conversations are never deleted).

With {:is-recursive true} every member session is DELETED first via close-session! (draft clones trashed, session tree dropped, runtime torn down off-thread) and only then is the project row removed — sessions first, so an interrupted teardown leaves a project holding survivors rather than orphaned sessions. Real workspace directories are never touched.

Membership comes from the DB (lp/project-session-ids), not from any client's filtered list. Returns {:project_id … :deleted_session_ids […] :session_count n}; the ids let a caller prune local state without racing a re-read.

Delete a project. By DEFAULT its member sessions scatter back to project-less
(conversations are never deleted).

With `{:is-recursive true}` every member session is DELETED first via
`close-session!` (draft clones trashed, session tree dropped, runtime torn
down off-thread) and only then is the project row removed — sessions first, so
an interrupted teardown leaves a project holding survivors rather than
orphaned sessions. Real workspace directories are never touched.

Membership comes from the DB (`lp/project-session-ids`), not from any client's
filtered list. Returns `{:project_id … :deleted_session_ids […]
:session_count n}`; the ids let a caller prune local state without racing a
re-read.
sourceraw docstring

delete-queued-turn!clj

(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.
sourceraw docstring

drain-idle!clj

(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`.
sourceraw docstring

ensure-project-for-root!clj

(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`.
sourceraw docstring

events-sinceclj

(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.
sourceraw docstring

fleet-snapshotclj

(fleet-snapshot)

What the fleet is DOING right now: {sid {"is_live" … "is_awaiting_input" … "current_turn_id" …}}, holding only sessions that are running or parked on a human — its size is the busy fleet, never the store.

Read from the cross-process markers (bus/live-turns, bus/waiting-requests), never from this registry: a sibling process's turn is mirrored here only once somebody SUBSCRIBES to that session, so a registry answer would light up exactly the sessions the asking client already watched.

What the fleet is DOING right now: `{sid {"is_live" … "is_awaiting_input" …
"current_turn_id" …}}`, holding only sessions that are running or parked on a
human — its size is the busy fleet, never the store.

Read from the cross-process markers (`bus/live-turns`, `bus/waiting-requests`),
never from this registry: a sibling process's turn is mirrored here only once
somebody SUBSCRIBES to that session, so a registry answer would light up
exactly the sessions the asking client already watched.
sourceraw docstring

fleet-status-framesclj

(fleet-status-frames before after)

PURE diff of two fleet-snapshots: one session.status frame per session whose fleet-visible state changed. A session that left both indexes gets the idle frame — that transition is exactly what a list needs to stop painting a spinner, and it is the one a snapshot cannot carry by absence.

Carries no seq/ts: publish-fleet! stamps those, so this stays a function of its arguments and a test can state the contract without a clock.

PURE diff of two [[fleet-snapshot]]s: one `session.status` frame per session
whose fleet-visible state changed. A session that left both indexes gets the
idle frame — that transition is exactly what a list needs to stop painting a
spinner, and it is the one a snapshot cannot carry by absence.

Carries no `seq`/`ts`: `publish-fleet!` stamps those, so this stays a function
of its arguments and a test can state the contract without a clock.
sourceraw docstring

fork-pointsclj

(fork-points sid)

Every turn of sid a fork can be cut AT, oldest-first, as lean wire rows {turn_id request created_at}.

The picker needs the id and the words that OPENED each turn, never the transcript hanging off it — list-turns carries whole content vectors and a long session's picker would pull megabytes to paint one list of lines.

Every turn of `sid` a fork can be cut AT, oldest-first, as lean wire rows
`{turn_id request created_at}`.

The picker needs the id and the words that OPENED each turn, never the
transcript hanging off it — `list-turns` carries whole content vectors and a
long session's picker would pull megabytes to paint one list of lines.
sourceraw docstring

fork-session!clj

(fork-session! sid through-turn-id)

Fork sid into a NEW INDEPENDENT session holding a deep copy of every turn from the start THROUGH through-turn-id — nil means the session's last turn, which is the plain "fork this session" the TUI's y runs. The source is left untouched; the fork gets its own soul (so it opens as its own row/tab) and its own trunk workspace at the source's root, because a session_state owns its workspace 1:1.

Returns the fork's wire soul. Throws ex-info with :type :session/no-turns, :session/unknown-turn or :session/fork-failed.

Fork `sid` into a NEW INDEPENDENT session holding a deep copy of every turn
from the start THROUGH `through-turn-id` — nil means the session's last turn,
which is the plain "fork this session" the TUI's `y` runs. The source is left
untouched; the fork gets its own soul (so it opens as its own row/tab) and its
own trunk workspace at the source's root, because a session_state owns its
workspace 1:1.

Returns the fork's wire `soul`. Throws `ex-info` with `:type`
`:session/no-turns`, `:session/unknown-turn` or `:session/fork-failed`.
sourceraw docstring

get-projectclj

(get-project pid)
source

get-project-by-rootclj

(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.
sourceraw docstring

get-turnclj

(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.
sourceraw docstring

goal-listenerclj

source

ingest-mirrored-event!clj

(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.
sourceraw docstring

iteration-attachmentsclj

(iteration-attachments iid)

Ordered OUTBOUND artifacts (matplotlib figures / produced images) a tool call persisted under iteration iid as METADATA ONLY — the db-list-iteration-attachments-meta shape, never a byte of payload — or []. THE canonical, ordered, UNFILTERED list. Everything a client sees is derived from it by user-iteration-attachments, which is what both the descriptors and the byte endpoint go through; the ONE artifact the endpoint then serves fetches its own bytes in attachment-bytes.

Listing the bytes here made LISTING cost the whole iteration: every iteration.completed frame read (and base64-encoded) every figure it was only going to describe, and serving image N of a gallery re-read all N — a 9-image gallery paid 81 image reads for 9. nil/unparsable id -> [].

Ordered OUTBOUND artifacts (matplotlib figures / produced images) a tool call
persisted under iteration `iid` as METADATA ONLY — the
`db-list-iteration-attachments-meta` shape, never a byte of payload — or `[]`.
THE canonical, ordered, UNFILTERED list. Everything a client sees is derived
from it by [[user-iteration-attachments]], which is what both the descriptors
and the byte endpoint go through; the ONE artifact the endpoint then serves
fetches its own bytes in [[attachment-bytes]].

Listing the bytes here made LISTING cost the whole iteration: every
`iteration.completed` frame read (and base64-encoded) every figure it was
only going to describe, and serving image N of a gallery re-read all N — a
9-image gallery paid 81 image reads for 9. nil/unparsable id -> `[]`.
sourceraw docstring

list-projectsclj

(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?.
sourceraw docstring

list-queued-turnsclj

(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.
sourceraw docstring

list-sessions-pageclj

(list-sessions-page opts)
(list-sessions-page channel
                    {:keys [limit after root project-id id-prefix ids dirty]})

A WINDOW of the navigator list, in the gateway's own order: {:sessions rows :awaiting rows :total n :limit l :next-cursor s :has-more bool}.

THE GATEWAY OWNS THE LIST - which sessions are in it and where each one sits (session-ranking, session-listed?) - so total, this window and a client's page count are one arithmetic. They were two: the app re-filtered and re-ordered what it was sent, which made ?root=&limit= a DIFFERENT list at the same address (1034 rows counted against 763 painted, its last page 27 pages beyond the pager's), and left every client downloading the whole fleet in order to cut a page of ten.

:dirty is the one fact this process cannot see: the ids holding words the ASKING device has typed and not sent. They are listed even when they are otherwise empty and they band above the rest, exactly as the composer's owner expects, and a device that names none simply gets the list without that band.

The window is a KEYSET, not an offset: :after is the cursor of the last row a client already holds (:next-cursor of its previous answer) and the answer is the rows that sort strictly after it. An offset indexes a list that is RECOMPUTED per request, so content moving during a walk - another machine finishes a turn - made one row arrive twice (a duplicate id) and dropped another entirely, with the merged count still equal to total so nothing downstream could notice. A cursor names a ROW, so the same page comes back however much the fleet moved meanwhile: the tear is not detected, it cannot happen. An unparsable cursor is no cursor here (the server answers 400 first, see parse-session-cursor).

:awaiting stands BESIDE the window: the sessions parked on an unanswered human-input request, complete however deep in the fleet they sit, because a client pins them above the list instead of the ordering lifting them into it (see session-ranking).

nil limit means "the rest". No ROUTE leaves it nil any more: a read that names no cut is answered with the head window (list-sessions-handler), so a whole-list build is a deliberate in-process call and no client can ask for one.

:root narrows the listing to ONE project before the window is cut, so a client paging a project asks the gateway for that project's page instead of downloading the fleet and slicing it locally. total/has-more then describe that project, which is what a pager prints.

:project-id, :id-prefix and :ids answer the questions a channel used to answer by downloading the fleet and filtering it locally: ONE project's tab set, the session a short id names, and the ROWS a set of ids names. All three cut the ORDERING - before total, before the window - so each answer costs the rows it returns. :ids is how a picker that holds a WINDOW paints a search hit: the store ranks the query across every session, and only the matched rows the window does not already hold cross the wire.

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).

The window is cut BEFORE decoration: the ranking is built from cheap facts, and only the ids that survive the cut pay for soul + workspace resolution. A 100-row page of a 448-session store therefore costs about a fifth of the full build (~257ms) and a fifth of its ~300KB, which is what makes a polled session list affordable.

A WINDOW of the navigator list, in the gateway's own order:
`{:sessions rows :awaiting rows :total n :limit l :next-cursor s :has-more bool}`.

THE GATEWAY OWNS THE LIST - which sessions are in it and where each one sits
(`session-ranking`, `session-listed?`) - so `total`, this window and a client's
page count are one arithmetic. They were two: the app re-filtered and re-ordered
what it was sent, which made `?root=&limit=` a DIFFERENT list at the same
address (1034 rows counted against 763 painted, its last page 27 pages beyond
the pager's), and left every client downloading the whole fleet in order to cut a
page of ten.

`:dirty` is the one fact this process cannot see: the ids holding words the
ASKING device has typed and not sent. They are listed even when they are
otherwise empty and they band above the rest, exactly as the composer's owner
expects, and a device that names none simply gets the list without that band.

The window is a KEYSET, not an offset: `:after` is the cursor of the last row a
client already holds (`:next-cursor` of its previous answer) and the answer is
the rows that sort strictly after it. An offset indexes a list that is RECOMPUTED
per request, so content moving during a walk - another machine finishes a turn -
made one row arrive twice (a duplicate id) and dropped another entirely, with the
merged count still equal to `total` so nothing downstream could notice. A cursor
names a ROW, so the same page comes back however much the fleet moved meanwhile:
the tear is not detected, it cannot happen. An unparsable cursor is no cursor here
(the server answers 400 first, see `parse-session-cursor`).

`:awaiting` stands BESIDE the window: the sessions parked on an unanswered
human-input request, complete however deep in the fleet they sit, because a
client pins them above the list instead of the ordering lifting them into it
(see `session-ranking`).

`nil` limit means "the rest". No ROUTE leaves it nil any more: a read that names
no cut is answered with the head window (`list-sessions-handler`), so a whole-list
build is a deliberate in-process call and no client can ask for one.

`:root` narrows the listing to ONE project before the window is cut, so a client
paging a project asks the gateway for that project's page instead of downloading
the fleet and slicing it locally. `total`/`has-more` then describe that project,
which is what a pager prints.

`:project-id`, `:id-prefix` and `:ids` answer the questions a channel used to answer
by downloading the fleet and filtering it locally: ONE project's tab set, the session
a short id names, and the ROWS a set of ids names. All three cut the ORDERING - before
`total`, before the window - so each answer costs the rows it returns. `:ids` is how a
picker that holds a WINDOW paints a search hit: the store ranks the query across every
session, and only the matched rows the window does not already hold cross the wire.

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).

The window is cut BEFORE decoration: the ranking is built from cheap facts, and
only the ids that survive the cut pay for `soul` + workspace resolution. A 100-row
page of a 448-session store therefore costs about a fifth of the full build
(~257ms) and a fifth of its ~300KB, which is what makes a polled session list
affordable.
sourceraw docstring

list-turnsclj

(list-turns sid)

Canonical (string-keyed) wire views of every turn for sid, oldest-first.

A turn has one id from submission through persistence. While its durable row is still running, the live gateway row owns the paint. Once persistence settles that same id, the durable row replaces the overlay exactly.

Canonical (string-keyed) wire views of every turn for `sid`, oldest-first.

A turn has one id from submission through persistence. While its durable row
is still `running`, the live gateway row owns the paint. Once persistence
settles that same id, the durable row replaces the overlay exactly.
sourceraw docstring

metrics-snapshotclj

(metrics-snapshot)

Global, per-session, concurrency, replay-buffer, and JVM gauges for /metrics.

Global, per-session, concurrency, replay-buffer, and JVM gauges for /metrics.
sourceraw docstring

model-listenerclj

source

parse-session-cursorclj

(parse-session-cursor cursor)

"<band>:<sort-key>:<id>" back to [^long band ^long sort-key ^String id], or nil when the string is not a cursor.

Public because the SERVER refuses an unparsable ?after= with a 400 rather than silently answering the head of the list (server/list-sessions-handler); list-sessions-page takes the wire string and treats anything else as no cursor at all, so it stays total.

It carried <recency-ms>:<id> while the order had one band and could not name a row in a banded one: a starred row and a plain row can share a recency, and the page after the last star is not the page after its timestamp.

`"<band>:<sort-key>:<id>"` back to `[^long band ^long sort-key ^String id]`, or
nil when the string is not a cursor.

Public because the SERVER refuses an unparsable `?after=` with a 400 rather than
silently answering the head of the list (`server/list-sessions-handler`);
`list-sessions-page` takes the wire string and treats anything else as no cursor
at all, so it stays total.

It carried `<recency-ms>:<id>` while the order had one band and could not name a
row in a banded one: a starred row and a plain row can share a recency, and the
page after the last star is not the page after its timestamp.
sourceraw docstring

projects-overviewclj

(projects-overview)
(projects-overview channel)
(projects-overview channel dirty)

ONE answer for the navigator's header row: every PROJECT this gateway holds with its own counts, plus the gateway's totals beside them.

{:projects [{root project_id name session_count live_count awaiting_count last_activity_ms} ...] :project_count n :session_count n :live_count n :awaiting_count n :server_time_ms ms}

A client used to DERIVE this: download the fleet, group it by working directory, tally each group. That made a project header cost the whole session list, so switching gateways repainted projects and their numbers page by page as the windows landed - the flicker in the report. Here the numbers are computed once, by the process that already holds the facts, from the same cheap sources the ordering uses (session-ranking + session-project-root), so a client paints headers before it has read a single session row.

The group key is the PROJECT ROOT (session-project-root), exactly the key a root= window of list-sessions-page takes, so a header and its page agree by construction - the same ranking, the same filter, the same dirty overlay, so a header that says 763 is the number of rows its pages hold. name is the persisted project's name when the root is bound to one and "" otherwise - the folder is the client's own fallback.

Projects are ordered by canonical root, ascending. Activity, liveness and demand update counts only; they never move project headers.

ONE answer for the navigator's header row: every PROJECT this gateway holds
with its own counts, plus the gateway's totals beside them.

`{:projects [{root project_id name session_count live_count awaiting_count
              last_activity_ms} ...]
  :project_count n :session_count n :live_count n :awaiting_count n
  :server_time_ms ms}`

A client used to DERIVE this: download the fleet, group it by working
directory, tally each group. That made a project header cost the whole
session list, so switching gateways repainted projects and their numbers
page by page as the windows landed - the flicker in the report. Here the
numbers are computed once, by the process that already holds the facts, from
the same cheap sources the ordering uses (`session-ranking` +
`session-project-root`), so a client paints headers before it has read a
single session row.

The group key is the PROJECT ROOT (`session-project-root`), exactly the key
a `root=` window of `list-sessions-page` takes, so a header and its page
agree by construction - the same ranking, the same filter, the same `dirty`
overlay, so a header that says 763 is the number of rows its pages hold.
`name` is the persisted project's name when the root is bound to one and ""
otherwise - the folder is the client's own fallback.

Projects are ordered by canonical root, ascending. Activity, liveness and
demand update counts only; they never move project headers.
sourceraw docstring

queue-paused-infoclj

(queue-paused-info sid)

The live :queue-paused marker for sid ({:reason :held :gen …}), or nil when the queue is running.

The live `:queue-paused` marker for `sid` (`{:reason :held :gen …}`), or nil
when the queue is running.
sourceraw docstring

reconcile-orphaned-turns!clj

(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.
sourceraw docstring

reconcile-running-turns!clj

(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.
sourceraw docstring

release-session!clj

(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.

A BUSY SESSION IS NEVER TORN DOWN. Sessions are shared across channels, so a closing view only proves THAT view is gone — the companion app, web, or another TUI may be attached to and streaming the very turn this would kill (lp/close! drops the runtime mid-turn, which the other client sees as its work being cancelled). A client that wants to STOP work cancels the turn explicitly; this endpoint is a view-lifecycle hint and is a no-op while work is in flight. Real process exit is covered by the daemon's own gate (client refcount + running-turn-count).

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.

A BUSY SESSION IS NEVER TORN DOWN. Sessions are shared across channels, so a
closing view only proves THAT view is gone — the companion app, web, or another
TUI may be attached to and streaming the very turn this would kill (`lp/close!`
drops the runtime mid-turn, which the other client sees as its work being
cancelled). A client that wants to STOP work cancels the turn explicitly; this
endpoint is a view-lifecycle hint and is a no-op while work is in flight. Real
process exit is covered by the daemon's own gate (client refcount +
`running-turn-count`).
sourceraw docstring

remove-event-tap!clj

(remove-event-tap! k)
source

reorder-project-sessions!clj

(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.
sourceraw docstring

replay-floorclj

(replay-floor sid)

Highest "seq" this session's replay ring has already dropped, 0 while it still holds everything it stored.

A subscriber resuming BELOW the floor asks for events the ring no longer has: the honest answer is not the tail that happens to remain but a rewind to a whole picture (see server/resolve-sse-cursor), because a partial tail paints deltas onto blocks whose openings were evicted.

Highest `"seq"` this session's replay ring has already dropped, 0 while it
still holds everything it stored.

A subscriber resuming BELOW the floor asks for events the ring no longer has:
the honest answer is not the tail that happens to remain but a rewind to a
whole picture (see `server/resolve-sse-cursor`), because a partial tail paints
deltas onto blocks whose openings were evicted.
sourceraw docstring

resume-queue!clj

(resume-queue! sid {:keys [auto?]})

Clear a paused backlog and start its head. A failed turn is never replayed; resume advances only to a distinct queued request. No-op when not paused.

Clear a paused backlog and start its head. A failed turn is never replayed;
resume advances only to a distinct queued request. No-op when not paused.
sourceraw docstring

running-turn-countclj

(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.
sourceraw docstring

running-turn-progressclj

(running-turn-progress)

How many turns this gateway still counts as running, and how far their event rings have advanced. A turn that is really working moves :seq every few hundred milliseconds; a GHOST turn - one whose worker died, was killed mid-launch, or is parked on human input nobody will ever answer - keeps :current-turn set forever while this marker stands still. The daemon lifecycle gate reads exactly that difference, because running-turn-count alone cannot tell the two apart.

How many turns this gateway still counts as running, and how far their event
rings have advanced. A turn that is really working moves `:seq` every few
hundred milliseconds; a GHOST turn - one whose worker died, was killed
mid-launch, or is parked on human input nobody will ever answer - keeps
`:current-turn` set forever while this marker stands still. The daemon
lifecycle gate reads exactly that difference, because `running-turn-count`
alone cannot tell the two apart.
sourceraw docstring

running-turn-start-cursorclj

(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).
sourceraw docstring

search-session-idsclj

(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 → [].
sourceraw docstring

search-session-matchesclj

(search-session-matches query)
(search-session-matches channel query)

Soul-id STRINGS whose TITLE or TRANSCRIPT matches query, each TAGGED with WHERE it hit, RANKED by the server, and carrying up to a handful of MATCH SNIPPETS: [{:session_id str :rank 0-3 :is_in_title bool :is_in_request bool :is_in_reply bool :is_in_thinking bool :request_snippet str :reply_snippet str :hits [{:side "request"|"reply"|"thinking" :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_title = the session's own name matched; :is_in_request = the user's own request matched; :is_in_reply = the assistant's answer; :is_in_thinking = only its reasoning aside.

THE ORDER IS THE ANSWER, and it is the LIST's own: freshest first, which is exactly the key order-session-summaries gives the navigator - db-search-session-matches sorts by the instant each session last moved (the modified_at a list read prints). A search therefore FILTERS the list instead of reshuffling it, and the dates only fall as a client scans down. Sessions RUNNING right now are no longer lifted over that: a band that flips when a turn starts moved results under the reader's finger, which is the defect the navigator's own key just lost. :rank travels so a surface can say WHERE the query hit and break a tie; it is not the order and no surface re-derives one from the flags. Blank query → [].

Soul-id STRINGS whose TITLE or TRANSCRIPT matches `query`, each TAGGED with
WHERE it hit, RANKED by the server, and carrying up to a handful of MATCH
SNIPPETS:
`[{:session_id str :rank 0-3 :is_in_title bool :is_in_request bool
   :is_in_reply bool :is_in_thinking bool
   :request_snippet str :reply_snippet str
   :hits [{:side "request"|"reply"|"thinking" :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_title` = the session's own name matched;
`:is_in_request` = the user's own request matched; `:is_in_reply` = the
assistant's answer; `:is_in_thinking` = only its reasoning aside.

THE ORDER IS THE ANSWER, and it is the LIST's own: freshest first, which is
exactly the key `order-session-summaries` gives the navigator -
`db-search-session-matches` sorts by the instant each session last moved (the
`modified_at` a list read prints). A search therefore FILTERS the list instead
of reshuffling it, and the dates only fall as a client scans down. Sessions
RUNNING right now are no longer lifted over that: a band that flips when a turn
starts moved results under the reader's finger, which is the defect the
navigator's own key just lost. `:rank` travels so a surface can say WHERE the
query hit and break a tie; it is not the order and no surface re-derives one
from the flags.
Blank query → [].
sourceraw docstring

session-agent-nameclj

(session-agent-name sid)

Resolve identity on the gateway from this session's workspace, never a client's cwd.

Resolve identity on the gateway from this session's workspace, never a client's cwd.
sourceraw docstring

session-artifactsclj

(session-artifacts sid)

EVERY artifact the whole SESSION produced, oldest turn first — the index behind a client's artifacts gallery.

A transcript is read newest-page-first, so a client that derives its gallery from the rows it happens to HOLD can only list what the reader already scrolled past: a 200-turn session opens on an empty sheet and grows one page at a time. This answers the whole session in ONE metadata query — never a byte of payload — and the client fetches the blobs it renders from GET /v1/sessions/:sid/iterations/:iid/attachments/:idx.

The rows are the SAME [[attachment-descriptors]] the live frame and the transcript ship, with :turn added: the 1-based ordinal of the turn that produced it, counted from the start of the session, so index names the same artifact here, in history and at the byte endpoint. Model-only rows are dropped by the very filter the byte endpoint indexes (user-iteration-attachments), and a user's own uploaded image is not an artifact the model produced, so it is not here either.

EVERY artifact the whole SESSION produced, oldest turn first — the index
behind a client's artifacts gallery.

A transcript is read newest-page-first, so a client that derives its gallery
from the rows it happens to HOLD can only list what the reader already
scrolled past: a 200-turn session opens on an empty sheet and grows one page
at a time. This answers the whole session in ONE metadata query — never a
byte of payload — and the client fetches the blobs it renders from
`GET /v1/sessions/:sid/iterations/:iid/attachments/:idx`.

The rows are the SAME [[attachment-descriptors]] the live frame and the
transcript ship, with `:turn` added: the 1-based ordinal of the turn that
produced it, counted from the start of the session, so `index` names the same
artifact here, in history and at the byte endpoint. Model-only rows are
dropped by the very filter the byte endpoint indexes
([[user-iteration-attachments]]), and a user's own uploaded image is not an
artifact the model produced, so it is not here either.
sourceraw docstring

session-busy?clj

(session-busy? sid)

True when sid still has work the daemon owns: a live :current-turn, or a turn parked in the queue. THE guard for view-close teardown — a session is shared, so "my last view closed" never proves "nobody is working here": another channel (companion app, web, a second TUI) may be attached to and streaming that very turn.

True when `sid` still has work the daemon owns: a live `:current-turn`, or a
turn parked in the queue. THE guard for view-close teardown — a session is
shared, so "my last view closed" never proves "nobody is working here":
another channel (companion app, web, a second TUI) may be attached to and
streaming that very turn.
sourceraw docstring

session-idsclj

(session-ids)
(session-ids channel)

Every persisted session id of channel as STRINGS, unfiltered and undecorated.

The LOOKUP answer, not the navigator's. list-sessions paints a picker, so it leaves out the sessions nobody has used yet - while resolving --session <prefix> must find exactly those: the CLI creates a title-less, turn-less session and only then runs it. Cheap on purpose: no soul, no workspace resolution, just the ids.

Every persisted session id of `channel` as STRINGS, unfiltered and undecorated.

The LOOKUP answer, not the navigator's. `list-sessions` paints a picker, so it
leaves out the sessions nobody has used yet - while resolving `--session
<prefix>` must find exactly those: the CLI creates a title-less, turn-less
session and only then runs it. Cheap on purpose: no `soul`, no workspace
resolution, just the ids.
sourceraw docstring

session-modelclj

(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.
sourceraw docstring

session-model-cachedclj

(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.
sourceraw docstring

session-usage-infoclj

(session-usage-info sid)

Whole-session USAGE rollup for sid in THE canonical string-keyed wire shape, or nil when the session has no turns yet.

cache_read_share_percent is provider cache reads over ALL logical input, so it describes cost. reusable_prefix_coverage_percent is provider reads over what the previous same-route request could still have left in the cache — a fold, a rewrite or an expiry is CLASSIFIED and stays in that denominator, so the number cannot flatter itself by dropping its own misses, and prompt_cache_sample_count says how many calls carried one. Both percentages are measured over ONE population: every LLM call the session made, including the calls an interrupted turn's rollup never recorded. Their token numerators and denominators ride beside both percentages. health is the persisted request enriched by request-health-metrics, the single owner of metric calculations for Companion and TUI. Clients format values but never reconstruct them. Never throws.

Whole-session USAGE rollup for `sid` in THE canonical string-keyed wire shape,
or nil when the session has no turns yet.

`cache_read_share_percent` is provider cache reads over ALL logical input, so
it describes cost. `reusable_prefix_coverage_percent` is provider reads over
what the previous same-route request could still have left in the cache — a
fold, a rewrite or an expiry is CLASSIFIED and stays in that denominator, so
the number cannot flatter itself by dropping its own misses, and
`prompt_cache_sample_count` says how many calls carried one. Both percentages
are measured over ONE population: every LLM call the session made, including
the calls an interrupted turn's rollup never recorded. Their token numerators
and denominators ride beside both percentages. `health` is the persisted request
enriched by `request-health-metrics`, the single owner of metric calculations
for Companion and TUI. Clients format values but never reconstruct them.
Never throws.
sourceraw docstring

session-workspace-infoclj

(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" "git"} for the session pinned to sid, plus "backend" "branch" "ahead" for a draft, or nil. 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"
"git"}` for the session pinned to `sid`, plus `"backend"` `"branch"`
`"ahead"` for a draft, or nil. Resolves soul → latest state → workspace;
never throws.
sourceraw docstring

set-agent-name!clj

(set-agent-name! value)

Save the gateway identity before notifying every open session. Reconnects read the current identity from session metadata, not stale replayed rename events.

Save the gateway identity before notifying every open session. Reconnects read
the current identity from session metadata, not stale replayed rename events.
sourceraw docstring

set-favorite!clj

(set-favorite! sid is-favorite)

Star (true) or unstar (false) sid. Returns the refreshed soul (its favorite_rank is the rank the gateway allocated), or nil when no such session exists.

Star (`true`) or unstar (`false`) `sid`. Returns the refreshed soul (its
`favorite_rank` is the rank the gateway allocated), or nil when no such
session exists.
sourceraw docstring

set-session-model!clj

(set-session-model! sid provider model)

Set (or clear, with blank model) the per-session PROVIDER + MODEL preference used by the next submitted turn. This is composer state, not a command for the active request: submission snapshots the pair, so a later selector change neither cancels a running turn nor rewrites work already queued. Channel-agnostic: web + TUI + embedded callers all set it here, persisted in the DB and shared across channels.

A changed manual preference also receives a small durable audit sidecar for the usage section of read_session(); the live session.model_updated event remains non-replayable so old cursor events cannot overwrite a newer preference.

Set (or clear, with blank model) the per-session PROVIDER + MODEL preference
used by the next submitted turn. This is composer state, not a command for the
active request: submission snapshots the pair, so a later selector change
neither cancels a running turn nor rewrites work already queued. Channel-agnostic:
web + TUI + embedded callers all set it here, persisted in the DB and shared
across channels.

A changed manual preference also receives a small durable audit sidecar for
the `usage` section of `read_session()`; the live `session.model_updated`
event remains non-replayable so old cursor events cannot overwrite a newer preference.
sourceraw docstring

set-title!clj

(set-title! sid title)
source

soulclj

(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.

Carries the SAME turn_count / modified_at freshness pair the list rows get from session-summary-extras (one session-scoped query here, not a whole-store scan). Without them a client holding only a detail row cannot tell that a session moved: its transcript stamp is constant, so a cached transcript never revalidates and an unread mark can only count the page it happens to hold.

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.

Carries the SAME `turn_count` / `modified_at` freshness pair the list rows
get from `session-summary-extras` (one session-scoped query here, not a
whole-store scan). Without them a client holding only a detail row cannot
tell that a session moved: its transcript stamp is constant, so a cached
transcript never revalidates and an unread mark can only count the page it
happens to hold.
sourceraw docstring

submit-turn!clj

(submit-turn! sid
              {:keys [request messages idempotency-key provider model
                      reasoning-default cancel-token extra-body turn-features
                      workspace engine-opts attachments display-request
                      council-ping]})

Submit one turn for sid. Async: starts immediately when idle, otherwise queues. A fresh idle submission clears a provider-failure pause before it starts; success drains the backlog and another failure holds it again.

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.
A fresh idle submission clears a provider-failure pause before it starts;
success drains the backlog and another failure holds it again.

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.
sourceraw docstring

submit-turn-sync!clj

(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.
sourceraw docstring

subscribe!clj

(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.
sourceraw docstring

subscribe-fleet!clj

(subscribe-fleet! sub-id sink)

Attach sink to the FLEET stream: every session's status transitions, not one session's events. The sink must be NON-BLOCKING for the same reason subscribe!'s must be.

There is NO replay and no cursor. The stream is a delta feed layered on a cold /v1/sessions read: a client resyncs by re-reading the window — one windowed read, not a rewind — so a missed frame heals itself and a reconnect costs nothing to arrange.

Attach `sink` to the FLEET stream: every session's status transitions, not one
session's events. The sink must be NON-BLOCKING for the same reason
[[subscribe!]]'s must be.

There is NO replay and no cursor. The stream is a delta feed layered on a cold
`/v1/sessions` read: a client resyncs by re-reading the window — one windowed
read, not a rewind — so a missed frame heals itself and a reconnect costs
nothing to arrange.
sourceraw docstring

title-listenerclj

source

transcriptclj

(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.
sourceraw docstring

transcript-pageclj

(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>}`.
sourceraw docstring

turn-answer-textclj

(turn-answer-text sid tid)

Plain-text projection of ONE finished turn's answer content, or nil.

Read straight from the live registry (finish-turn! has already merged the content patch by the time a terminal event is appended), so this costs one map lookup and never touches the DB. Exists for the push alert: a notification that only says "turn finished" makes you open the app to learn anything at all.

Plain-text projection of ONE finished turn's answer content, or nil.

Read straight from the live registry (`finish-turn!` has already merged the
content patch by the time a terminal event is appended), so this costs one
map lookup and never touches the DB. Exists for the push alert: a
notification that only says "turn finished" makes you open the app to learn
anything at all.
sourceraw docstring

turn-attachmentsclj

(turn-attachments sid tid)

The FULL attachments (filename / media_type / base64) of ONE turn.

The live rail and the queue mirror ship byte-free :attachment_previews, and a turn's persisted row only exists once it LANDS — so between submit and landing the SENDER's own in-memory copy was the only thing that could paint the user bubble's images. Restart the app (or open the session on a second device) mid-turn and they were gone for good.

The gateway can reach those bytes the whole time, from all THREE sources a user's image has: :attachments on the registry entry of a running/queued turn (inline uploads), the paths the turn's own request text names (the TUI's drag-drop and clipboard paste, which upload nothing), and the attachment store once the turn lands. This serves them, in the SAME shape a transcript row carries, so a channel can lazily fetch what it does not have. nil when the turn is unknown or carried no images.

The FULL attachments (filename / media_type / base64) of ONE turn.

The live rail and the queue mirror ship byte-free `:attachment_previews`, and
a turn's persisted row only exists once it LANDS — so between submit and
landing the SENDER's own in-memory copy was the only thing that could paint
the user bubble's images. Restart the app (or open the session on a second
device) mid-turn and they were gone for good.

The gateway can reach those bytes the whole time, from all THREE sources a
user's image has: `:attachments` on the registry entry of a running/queued
turn (inline uploads), the paths the turn's own request text names (the TUI's
drag-drop and clipboard paste, which upload nothing), and the attachment store
once the turn lands. This serves them, in the SAME shape a transcript row
carries, so a channel can lazily fetch what it does not have. nil when the
turn is unknown or carried no images.
sourceraw docstring

turn-traceclj

(turn-trace _sid tid)

The canonical persisted iteration trace for one turn id. Returns a possibly empty vector for a valid id, nil for an invalid id or read failure.

The canonical persisted iteration trace for one turn id. Returns a possibly
empty vector for a valid id, nil for an invalid id or read failure.
sourceraw docstring

unsubscribe!clj

(unsubscribe! sid sub-id)
source

unsubscribe-fleet!clj

(unsubscribe-fleet! sub-id)

Drop one fleet stream. The watcher stays parked — without sinks it scans nothing — and the last subscriber out removes the title tap.

Drop one fleet stream. The watcher stays parked — without sinks it scans
nothing — and the last subscriber out removes the title tap.
sourceraw docstring

update-project!clj

(update-project! pid opts)
source

update-queued-turn!clj

(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.
sourceraw docstring

user-iteration-attachmentsclj

(user-iteration-attachments iid)

iteration-attachments minus the rows a human is never shown (audience model) — THE list index N addresses.

One list, filtered ONCE, is the whole contract: the descriptors number this seq and GET /v1/sessions/:sid/iterations/:iid/attachments/:idx serves from it. Filtering on the descriptor side alone re-numbered what survived while the byte endpoint still indexed the raw rows, so an iteration whose first artifact was model-only handed every later index the wrong bytes — and handed the human the artifact that was hidden from it.

[[iteration-attachments]] minus the rows a human is never shown (audience
`model`) — THE list index N addresses.

One list, filtered ONCE, is the whole contract: the descriptors number this
seq and `GET /v1/sessions/:sid/iterations/:iid/attachments/:idx` serves from
it. Filtering on the descriptor side alone re-numbered what survived while
the byte endpoint still indexed the raw rows, so an iteration whose first
artifact was model-only handed every later index the wrong bytes — and handed
the human the artifact that was hidden from it.
sourceraw docstring

warm-db!clj

(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/core.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/core.clj).
sourceraw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
Move to previous article
Move to next article
Ctrl+/Jump to the search field
× close