HTTP/SSE client for the long-lived gateway daemon.
Interactive channels call this facade instead of gateway.state directly. It
discover-or-starts the one daemon for the current DB, then speaks the same
HTTP/SSE API every other client uses. This is the thin-client half of the
gateway-daemon plan: token refresh, turn execution, and live streaming happen
in ONE process.
WHICH daemon is a policy of this namespace: normally the one this machine
manages for the current DB, or — through connect-remote! (the --gateway
flag / VIS_GATEWAY_URL) — a gateway on another machine, attached to over HTTP
and never spawned, restarted or stopped from here.
HTTP/SSE client for the long-lived gateway daemon. Interactive channels call this facade instead of `gateway.state` directly. It discover-or-starts the one daemon for the current DB, then speaks the same HTTP/SSE API every other client uses. This is the thin-client half of the gateway-daemon plan: token refresh, turn execution, and live streaming happen in ONE process. WHICH daemon is a policy of this namespace: normally the one this machine manages for the current DB, or — through `connect-remote!` (the `--gateway` flag / `VIS_GATEWAY_URL`) — a gateway on another machine, attached to over HTTP and never spawned, restarted or stopped from here.
(assign-project! sid pid)Assign a session to a project (nil clears / removes from project). Returns the soul.
Assign a session to a project (nil clears / removes from project). Returns the soul.
(cancel-current-turn! sid owner-key)Tid-less cancel: kill the turn currently holding sid's :current-turn slot
in the daemon, iff THIS caller submitted it under owner-key (the
idempotency_key it sent). For callers that lost (or never learned) the
gateway turn id. A session is shared, so an unaddressed cancel would kill
whatever another channel happens to be running. Returns the parsed body
({"status" "cancelling", "turn_id" tid}); throws on HTTP error (409 when
the session is idle or the running turn is someone else's).
Tid-less cancel: kill the turn currently holding `sid`'s `:current-turn` slot
in the daemon, iff THIS caller submitted it under `owner-key` (the
`idempotency_key` it sent). For callers that lost (or never learned) the
gateway turn id. A session is shared, so an unaddressed cancel would kill
whatever another channel happens to be running. Returns the parsed body
(`{"status" "cancelling", "turn_id" tid}`); throws on HTTP error (409 when
the session is idle or the running turn is someone else's).(capabilities)The daemon's capability document, string-keyed, or nil when it cannot answer. The attachment contract a channel admits file drops against comes from here.
The daemon's capability document, string-keyed, or nil when it cannot answer. The attachment contract a channel admits file drops against comes from here.
(change-root! sid path)Repoint sid's PRIMARY filesystem root to path IN THE DAEMON, returning the
refreshed session-workspace-info (whose :id is the newly pinned workspace).
Repoint `sid`'s PRIMARY filesystem root to `path` IN THE DAEMON, returning the refreshed `session-workspace-info` (whose `:id` is the newly pinned workspace).
(compatibility)This client's verdict on the daemon it last probed — the SAME pure comparison the gateway runs on us ([[gateway-contract/verdict]]), so the two halves never disagree about which one is out of date.
This client's verdict on the daemon it last probed — the SAME pure comparison the gateway runs on us ([[gateway-contract/verdict]]), so the two halves never disagree about which one is out of date.
(connect-remote! {:keys [url token]})Aim EVERY gateway call in this process at the gateway at url (the --gateway
flag, VIS_GATEWAY_URL) instead of the daemon this machine manages: attach over
HTTP, and never spawn, restart or stop it — a gateway on another machine is not
ours to run. Returns the target entry, or nil when url is blank (stay local).
Aim EVERY gateway call in this process at the gateway at `url` (the `--gateway` flag, `VIS_GATEWAY_URL`) instead of the daemon this machine manages: attach over HTTP, and never spawn, restart or stop it — a gateway on another machine is not ours to run. Returns the target entry, or nil when `url` is blank (stay local).
(consume-provider-reset-credit! provider-id account-id idempotency-key)Consume the reset confirmed for this account. The caller keeps the same idempotency key until a recognized outcome is received. No route probing or daemon restart is appropriate for a mutation.
Consume the reset confirmed for this account. The caller keeps the same idempotency key until a recognized outcome is received. No route probing or daemon restart is appropriate for a mutation.
(cycle-setting! id)Atomically advance one enum setting in the gateway and return its refreshed string-keyed settings row.
Atomically advance one enum setting in the gateway and return its refreshed string-keyed settings row.
(daemon-idle? status)(daemon-idle? status {:keys [tolerate-clients user-owned-ok?]})THE one definition of "this daemon may be bounced", read off an admin status
map (canonical STRING keys, e.g. from status).
A managed daemon with no client and no running turn is free to release: it was
auto-spawned for whoever needed it, it self-reaps anyway, and the next client
spawns a fresh one from whatever is on disk. A busy one holds work - someone's
TUI, someone's turn - that a stop would abort. A user-owned one
(vis-agent gateway start, nohup, systemd) is never ours to stop, whatever it
is doing.
opts calibrates that one rule for a caller that is itself attached:
:tolerate-clients is how many of the leases belong to the caller, and
:user-owned-ok? admits the self-heal path that must replace a daemon whose
classpath lacks a route no matter who started it.
Returns {:idle? :reason :clients :running-turns :managed? :pid}, where :reason
is one of :idle :not-running :user-owned :clients :running-turns. A status this
build cannot read - no map at all, or a count in a shape it does not know
([[wire-count]]) - is :not-running: never evidence that stopping is free.
THE one definition of "this daemon may be bounced", read off an admin status
map (canonical STRING keys, e.g. from [[status]]).
A managed daemon with no client and no running turn is free to release: it was
auto-spawned for whoever needed it, it self-reaps anyway, and the next client
spawns a fresh one from whatever is on disk. A busy one holds work - someone's
TUI, someone's turn - that a stop would abort. A user-owned one
(`vis-agent gateway start`, nohup, systemd) is never ours to stop, whatever it
is doing.
`opts` calibrates that one rule for a caller that is itself attached:
`:tolerate-clients` is how many of the leases belong to the caller, and
`:user-owned-ok?` admits the self-heal path that must replace a daemon whose
classpath lacks a route no matter who started it.
Returns {:idle? :reason :clients :running-turns :managed? :pid}, where `:reason`
is one of :idle :not-running :user-owned :clients :running-turns. A status this
build cannot read - no map at all, or a count in a shape it does not know
([[wire-count]]) - is :not-running: never evidence that stopping is free.(delete-project! pid)(delete-project! pid {:keys [is-recursive?]})DELETE /v1/projects/:pid. Default: member sessions scatter back to
project-less. With {:is-recursive? true} every member session is deleted
too, and the response names the deleted ids.
DELETE /v1/projects/:pid. Default: member sessions scatter back to
project-less. With `{:is-recursive? true}` every member session is deleted
too, and the response names the deleted ids.(ensure-gateway!)(ensure-gateway! {:keys [port host] :as opts})Return a fresh daemon registry entry for the current DB, auto-starting the
detached gateway if needed. :memory is a programmer error for this client;
headless one-shots stay in-process and should not call here.
Optional :port/:host overrides the bind used WHEN THIS CALL SPAWNS a fresh
daemon (e.g. vis-agent channels web --port); a fresh daemon already registered for
the DB is a singleton and is attached to as-is, so the override is moot there.
Freshness is DEBOUNCED: the full HTTP /healthz probe (via probe-entry?)
runs at most once per entry-probe-ttl-ms. Within that window a cached entry
whose pid is still alive is trusted directly, so the TUI's chatty poll loop
stops paying for a doubled HTTP round-trip (and its JSON/reflection churn) on
every gateway call.
The slow discover/start path is single-flight per canonical DB inside this process. Callers re-check the cache after acquiring that lock, so concurrent startup callbacks share one spawn/wait instead of each waiting for readiness.
A daemon running a DIFFERENT build than this one is also replaced here when
replacing it is free ([[bounce-stale-daemon!]]) - that is how the first vis
started after vis-agent update, or after a rebuild of a dev checkout, comes up
on the new code with nobody stopping anything by hand. That decision comes BEFORE
the compatibility assert: a daemon too old to speak this build's wire protocol is
the one most worth replacing, so the mismatch screen is left for the daemon
somebody is still using.
Return a fresh daemon registry entry for the current DB, auto-starting the detached gateway if needed. `:memory` is a programmer error for this client; headless one-shots stay in-process and should not call here. Optional `:port`/`:host` overrides the bind used WHEN THIS CALL SPAWNS a fresh daemon (e.g. `vis-agent channels web --port`); a fresh daemon already registered for the DB is a singleton and is attached to as-is, so the override is moot there. Freshness is DEBOUNCED: the full HTTP /healthz probe (via `probe-entry?`) runs at most once per `entry-probe-ttl-ms`. Within that window a cached entry whose pid is still alive is trusted directly, so the TUI's chatty poll loop stops paying for a doubled HTTP round-trip (and its JSON/reflection churn) on every gateway call. The slow discover/start path is single-flight per canonical DB inside this process. Callers re-check the cache after acquiring that lock, so concurrent startup callbacks share one spawn/wait instead of each waiting for readiness. A daemon running a DIFFERENT build than this one is also replaced here when replacing it is free ([[bounce-stale-daemon!]]) - that is how the first vis started after `vis-agent update`, or after a rebuild of a dev checkout, comes up on the new code with nobody stopping anything by hand. That decision comes BEFORE the compatibility assert: a daemon too old to speak this build's wire protocol is the one most worth replacing, so the mismatch screen is left for the daemon somebody is still using.
(ensure-gateway-serving! path)(ensure-gateway-serving! path opts)Like ensure-gateway!, but tries to GUARANTEE the returned daemon actually
serves path. When ensure-gateway! attaches to an already-running daemon
that 404s on path (started from a classpath missing the extension that owns
it), respawn a fresh daemon from THIS process — whose classpath, by
construction, carries the route. This is what lets vis-agent channels web
self-heal instead of parking on a /ui that 404s.
Optional opts ({:port :host}) overrides the bind used when THIS call has
to spawn a fresh daemon (the vis-agent channels web --port/--host flags); it is
moot when a fresh daemon is already registered for the DB.
The respawn is NON-DESTRUCTIVE. A blind POST /v1/admin/stop is refcount-blind: it would abort every in-flight turn and kill every session's background resources. So we force-restart the stale daemon ONLY when it is idle — no OTHER clients and no running turn. Otherwise we leave it untouched and surface a clear error. A transport blip on the probe (not a real 404) never triggers a restart. Returns the entry.
Like [[ensure-gateway!]], but tries to GUARANTEE the returned daemon actually
serves `path`. When [[ensure-gateway!]] attaches to an already-running daemon
that 404s on `path` (started from a classpath missing the extension that owns
it), respawn a fresh daemon from THIS process — whose classpath, by
construction, carries the route. This is what lets `vis-agent channels web`
self-heal instead of parking on a `/ui` that 404s.
Optional `opts` (`{:port :host}`) overrides the bind used when THIS call has
to spawn a fresh daemon (the `vis-agent channels web --port/--host` flags); it is
moot when a fresh daemon is already registered for the DB.
The respawn is NON-DESTRUCTIVE. A blind POST /v1/admin/stop is refcount-blind:
it would abort every in-flight turn and kill every session's background
resources. So we force-restart the stale daemon ONLY when it is idle — no OTHER
clients and no running turn. Otherwise we leave it untouched and surface a clear
error. A transport blip on the probe (not a real 404) never triggers a restart.
Returns the entry.(ensure-project-for-root! root)(ensure-project-for-root! root name)POST /v1/projects/actions/ensure — get-or-create the project bound to canonical
workspace root (a project IS a TUI tab set). name seeds a fresh project.
Returns the project.
POST /v1/projects/actions/ensure — get-or-create the project bound to canonical workspace `root` (a project IS a TUI tab set). `name` seeds a fresh project. Returns the project.
(fleet-subscribe! sink)Watch the FLEET stream — GET /v1/events?scope=fleet — and hand every frame
to sink. One frame per session whose list-visible state changed
(session.status: is_live / is_awaiting_input / current_turn_id) or
that was renamed (session.title_updated). Returns a zero-arg stop fn.
This is what a session LIST subscribes to instead of asking about sessions one
by one: the fleet answers WHICH sessions changed, so a picker holding a
windowed read never polls a row again. There is no replay and no cursor — the
feed is a delta layered on a cold /v1/sessions window, so a reconnect costs
nothing to arrange and a missed frame heals on the next read. sink runs on
the reader thread and must not block it; drops reconnect with the multiplexed
mirror's backoff until the returned fn is called.
Watch the FLEET stream — `GET /v1/events?scope=fleet` — and hand every frame to `sink`. One frame per session whose list-visible state changed (`session.status`: `is_live` / `is_awaiting_input` / `current_turn_id`) or that was renamed (`session.title_updated`). Returns a zero-arg stop fn. This is what a session LIST subscribes to instead of asking about sessions one by one: the fleet answers WHICH sessions changed, so a picker holding a windowed read never polls a row again. There is no replay and no cursor — the feed is a delta layered on a cold `/v1/sessions` window, so a reconnect costs nothing to arrange and a missed frame heals on the next read. `sink` runs on the reader thread and must not block it; drops reconnect with the multiplexed mirror's backoff until the returned fn is called.
(input-views sid)Pending input Views for sid IN THE DAEMON, oldest first, in
canonical wire shape. The live view.open event is the fast path;
this is how a client that attached LATER still finds the open form instead of
watching a turn that never moves.
Pending input Views for `sid` IN THE DAEMON, oldest first, in canonical wire shape. The live `view.open` event is the fast path; this is how a client that attached LATER still finds the open form instead of watching a turn that never moves.
(iteration-attachment-bytes sid iid idx)Raw bytes (a byte-array) of ONE outbound artifact — iteration iid, its 0-based
idx in the iteration's ordered attachment list — fetched from the daemon's
attachment byte endpoint, or nil (404 / no bytes). The lazy-fetch companion to
a live iteration.completed attachment descriptor: a client sees {:index :media_type …} on the frame, then pulls the bytes here. HISTORY resolves the
same way (the trace iteration's :id + attachment index).
Raw bytes (a byte-array) of ONE outbound artifact — iteration `iid`, its 0-based
`idx` in the iteration's ordered attachment list — fetched from the daemon's
attachment byte endpoint, or nil (404 / no bytes). The lazy-fetch companion to
a live `iteration.completed` attachment descriptor: a client sees `{:index
:media_type …}` on the frame, then pulls the bytes here. HISTORY resolves the
same way (the trace iteration's `:id` + attachment index).(list-projects)(list-projects {:keys [owner archived?]})GET /v1/projects — projects are CROSS-CHANNEL. opts: :owner (string),
:archived? (bool). Returns the :projects vector.
GET /v1/projects — projects are CROSS-CHANNEL. `opts`: :owner (string), :archived? (bool). Returns the :projects vector.
(list-resources sid)Vector of the session's live resource DATA maps from the daemon's registry
(string-keyed, same shape resources/list-resources returns in-process).
Vector of the session's live resource DATA maps from the daemon's registry (string-keyed, same shape `resources/list-resources` returns in-process).
(list-resources-cached sid)Footer-frequency read: the session's resource list served from a per-sid cache that NEVER blocks the caller. A stale (or cold) entry kicks a background single-flight refresh and this returns the last-known value immediately (nil before the first success). Keeping the daemon HTTP round-trip OFF the render thread is what stops a busy daemon from stalling every TUI frame.
Footer-frequency read: the session's resource list served from a per-sid cache that NEVER blocks the caller. A stale (or cold) entry kicks a background single-flight refresh and this returns the last-known value immediately (nil before the first success). Keeping the daemon HTTP round-trip OFF the render thread is what stops a busy daemon from stalling every TUI frame.
(list-sessions opts)The ROWS of one window of the session list, in the gateway's own order. opts names
the cut - see session-window-path.
The ROWS of one window of the session list, in the gateway's own order. `opts` names the cut - see `session-window-path`.
(list-sessions-page opts)One window of the session list WITH the walk that continues it:
{:sessions rows :next-cursor str-or-nil :has-more bool :total n}.
opts names the cut (session-window-path). A surface that pages - the session picker
:after :next-cursor, so a list of
a thousand sessions is read a screen at a time instead of downloaded whole.One window of the session list WITH the walk that continues it:
`{:sessions rows :next-cursor str-or-nil :has-more bool :total n}`.
`opts` names the cut (`session-window-path`). A surface that pages - the session picker
- holds this window and asks for the next one with `:after` `:next-cursor`, so a list of
a thousand sessions is read a screen at a time instead of downloaded whole.(live-views sid)The live views session sid is SHOWING in the daemon right now, oldest first,
in canonical wire shape. The view.* events with kind=live are the fast path; this
is how a client that attached MID-RUN paints the whole picture at once instead
of waiting for the next patch to tell it a view exists.
The live views session `sid` is SHOWING in the daemon right now, oldest first, in canonical wire shape. The `view.*` events with `kind=live` are the fast path; this is how a client that attached MID-RUN paints the whole picture at once instead of waiting for the next patch to tell it a view exists.
(mcp-auth-complete! server flow-id input)Finish a flow with the redirect URL the user pasted back (or a bare code).
Finish a flow with the redirect URL the user pasted back (or a bare code).
(mcp-auth-logout! server)Forget the gateway's persisted OAuth tokens for a server.
Forget the gateway's persisted OAuth tokens for a server.
(mcp-auth-poll! server flow-id)Read a flow's verdict without blocking: pending, ok, or error.
Read a flow's verdict without blocking: `pending`, `ok`, or `error`.
(mcp-auth-start! server)Begin headless OAuth for an HTTP MCP server. Returns the wire flow
(flow_id, kind, url, redirect_uri, expires_at_ms, status).
Begin headless OAuth for an HTTP MCP server. Returns the wire flow (`flow_id`, `kind`, `url`, `redirect_uri`, `expires_at_ms`, `status`).
(mcp-kill-server! server)Stop a server NOW and hold it down until it is started again. Runtime only — nothing in the user's config changes.
Stop a server NOW and hold it down until it is started again. Runtime only — nothing in the user's config changes.
(mcp-save-server! server spec)Create or replace a gateway-managed server. spec is the string-keyed wire
spec (transport, command/args/cwd/env, or url/headers, plus the
optional enabled and timeout_ms). The DAEMON validates it, persists it in
its own machine state, and reconnects — nothing is written on this side, so a
TUI attached to a REMOTE gateway adds servers exactly like the app does.
Secrets survive an omitting save: see mcp.core/with-preserved-secrets.
Returns the saved sanitized row.
Create or replace a gateway-managed server. `spec` is the string-keyed wire spec (`transport`, `command`/`args`/`cwd`/`env`, or `url`/`headers`, plus the optional `enabled` and `timeout_ms`). The DAEMON validates it, persists it in its own machine state, and reconnects — nothing is written on this side, so a TUI attached to a REMOTE gateway adds servers exactly like the app does. Secrets survive an omitting save: see `mcp.core/with-preserved-secrets`. Returns the saved sanitized row.
(mcp-servers)Sanitized MCP inventory (string-keyed rows: name, transport, enabled,
is_connected, is_managed, is_killed, tools, is_authorized, …).
Sanitized MCP inventory (string-keyed rows: `name`, `transport`, `enabled`, `is_connected`, `is_managed`, `is_killed`, `tools`, `is_authorized`, …).
(mcp-set-server-enabled! server enabled)Persist a server's on/off switch in the gateway's own state.
Persist a server's on/off switch in the gateway's own state.
(mcp-start-server! server)Release a kill and connect the server again.
Release a kill and connect the server again.
(mcp-test-server! server spec)Connect a CANDIDATE spec without saving it and return {name, is_connected, tools}. The gateway opens and closes the connection, so a bad command or an
unreachable endpoint is reported before it is ever persisted.
Connect a CANDIDATE spec without saving it and return `{name, is_connected,
tools}`. The gateway opens and closes the connection, so a bad command or an
unreachable endpoint is reported before it is ever persisted.(mux-subscribe! sid sink cursor)Add sid's sink to the ONE process-wide multiplexed event stream, starting
at cursor (its current-seq for a live-only stream). The connection is
(re)opened only when the session set changes; multiple local listeners for
the SAME session share one cursor and one remote subscription. Returns a
zero-arg cleanup fn. Every sink sees gateway.connected / gateway.disconnected
on connection changes.
Add `sid`'s `sink` to the ONE process-wide multiplexed event stream, starting at `cursor` (its `current-seq` for a live-only stream). The connection is (re)opened only when the session set changes; multiple local listeners for the SAME session share one cursor and one remote subscription. Returns a zero-arg cleanup fn. Every sink sees gateway.connected / gateway.disconnected on connection changes.
(mux-unsubscribe! sid)(mux-unsubscribe! sid sub-id)Drop one local listener from the multiplexed stream and reconnect only when the last listener for that sid is gone (or tear the connection down when it was the last watched session).
Drop one local listener from the multiplexed stream and reconnect only when the last listener for that sid is gone (or tear the connection down when it was the last watched session).
(pairing-info)Connection details for the daemon registered for the current DB, so a caller
can build a companion pairing QR on demand (not only at --pair boot time).
Returns {:running? :host :port :token :loopback?}; :running? is false when no
fresh daemon is registered, and :loopback? flags a 127.0.0.1/::1/localhost
bind that a phone can never reach.
Connection details for the daemon registered for the current DB, so a caller
can build a companion pairing QR on demand (not only at `--pair` boot time).
Returns {:running? :host :port :token :loopback?}; `:running?` is false when no
fresh daemon is registered, and `:loopback?` flags a 127.0.0.1/::1/localhost
bind that a phone can never reach.(prepare-speech-model! direction {:keys [engine-id voice-id on-progress]})Prepare one gateway-owned speech engine and wait until it is ready. direction
is :transcribe or :synthesize; on-progress receives the gateway's
string-keyed model state. No model or native runtime is initialized here.
Prepare one gateway-owned speech engine and wait until it is ready. `direction` is `:transcribe` or `:synthesize`; `on-progress` receives the gateway's string-keyed model state. No model or native runtime is initialized here.
(projects-overview)GET /v1/projects/overview — every project with its counts plus the gateway's
totals, in one answer (state/projects-overview). The whole map.
GET /v1/projects/overview — every project with its counts plus the gateway's totals, in one answer (`state/projects-overview`). The whole map.
(provider-auth-cancel! provider-id flow-id)Forget an abandoned flow. Idempotent.
Forget an abandoned flow. Idempotent.
(provider-auth-complete! provider-id flow-id redirect-url)Finish a pkce flow with the redirect URL the user pasted back.
Finish a `pkce` flow with the redirect URL the user pasted back.
(provider-auth-poll! provider-id flow-id)Read a device flow's verdict: pending, ok, or error. Never blocks.
Read a `device` flow's verdict: `pending`, `ok`, or `error`. Never blocks.
(provider-auth-start! provider-id)Begin OAuth for provider-id. Returns the string-keyed wire flow
(flow_id, kind, url, user_code, verification_uri, interval_ms,
instructions) or nil when the daemon refused.
Begin OAuth for `provider-id`. Returns the string-keyed wire flow (`flow_id`, `kind`, `url`, `user_code`, `verification_uri`, `interval_ms`, `instructions`) or nil when the daemon refused.
(provider-auth-submit-key! provider-id flow-id api-key)Finish an api-key flow: hand the key the user typed to the DAEMON, which
persists it in ITS OWN config. The calling process never writes the
credential — same boundary as OAuth.
Finish an `api-key` flow: hand the key the user typed to the DAEMON, which persists it in ITS OWN config. The calling process never writes the credential — same boundary as OAuth.
(provider-logout! provider-id)Clear provider-id's persisted credentials IN THE DAEMON.
Clear `provider-id`'s persisted credentials IN THE DAEMON.
(provider-models provider-id show-all?)GET /v1/providers/:id/models — the LIVE model catalog resolved DAEMON-side,
where the gateway owns OAuth token resolution. A thin client NEVER builds a
token-resolving svar router to list models; it asks the daemon, which runs
the svar/models! probe (and any token refresh) against its own credential.
Returns the engine-shaped {:models [id …] :hidden-count n}.
GET /v1/providers/:id/models — the LIVE model catalog resolved DAEMON-side,
where the gateway owns OAuth token resolution. A thin client NEVER builds a
token-resolving svar router to list models; it asks the daemon, which runs
the `svar/models!` probe (and any token refresh) against its own credential.
Returns the engine-shaped `{:models [id …] :hidden-count n}`.(provider-remove! provider-id)DELETE /v1/providers/:id — drop provider-id from the fleet IN THE DAEMON,
credential included. Removal is the daemon's to do because it owns BOTH the
config file and the token file: a row dropped while its credential stays on
disk comes straight back as an authenticated preset. Idempotent — is_removed
is false when the id was not in the persisted fleet.
DELETE /v1/providers/:id — drop `provider-id` from the fleet IN THE DAEMON, credential included. Removal is the daemon's to do because it owns BOTH the config file and the token file: a row dropped while its credential stays on disk comes straight back as an authenticated preset. Idempotent — `is_removed` is false when the id was not in the persisted fleet.
(reconcile-running-turns!)Clients do not sweep. Only the daemon may reconcile its own startup orphans.
Clients do not sweep. Only the daemon may reconcile its own startup orphans.
(release-session! sid)Release a session VIEW when the owning channel exits: tell the daemon to
stop the session's background resources (background shell children, REPLs) and drop
its live runtime, then release the process-level client lease. This is NOT
a per-session delete (the transcript stays resumable) and never sends daemon
shutdown; the daemon stops itself only when refcount AND running-turn-count
hit zero. Best-effort and never daemon-spawning — if no fresh daemon is
registered there is nothing to release against.
Release a session VIEW when the owning channel exits: tell the daemon to stop the session's background resources (background `shell` children, REPLs) and drop its live runtime, then release the process-level client lease. This is NOT a per-session delete (the transcript stays resumable) and never sends daemon shutdown; the daemon stops itself only when refcount AND running-turn-count hit zero. Best-effort and never daemon-spawning — if no fresh daemon is registered there is nothing to release against.
(release-session-runtime! sid)Release a session's live RUNTIME on the daemon WITHOUT touching the process
client lease: stop its background resources (background shell children, managed REPLs)
and drop its loop/env, keeping the transcript resumable. Used when ONE view of
a session closes (e.g. a single TUI tab) while the owning process stays
connected — so the whole-process refcount lease is left intact and the daemon
is never nudged toward self-reap while other tabs remain open. Best-effort and
never daemon-spawning — nothing to release against when no fresh daemon is
registered.
Release a session's live RUNTIME on the daemon WITHOUT touching the process client lease: stop its background resources (background `shell` children, managed REPLs) and drop its loop/env, keeping the transcript resumable. Used when ONE view of a session closes (e.g. a single TUI tab) while the owning process stays connected — so the whole-process refcount lease is left intact and the daemon is never nudged toward self-reap while other tabs remain open. Best-effort and never daemon-spawning — nothing to release against when no fresh daemon is registered.
(remote-gateway)The remote gateway target for this process, or nil when the locally managed
daemon owns the work. connect-remote! wins; otherwise VIS_GATEWAY_URL and
VIS_GATEWAY_TOKEN are read exactly once.
The remote gateway target for this process, or nil when the locally managed daemon owns the work. `connect-remote!` wins; otherwise `VIS_GATEWAY_URL` and `VIS_GATEWAY_TOKEN` are read exactly once.
(reorder-project-sessions! pid session-ids)Persist a project's manual session order in one gateway call. Loose named sessions are adopted atomically; guests owned by another project are not moved.
Persist a project's manual session order in one gateway call. Loose named sessions are adopted atomically; guests owned by another project are not moved.
(request! method path)(request! method path opts)Canonical authenticated HTTP request for gateway development and diagnostics.
Resolves or starts the registered daemon for the current DB, acquires this process's client lease, adds protocol/authentication headers without exposing the registry secret, and delegates to the production babashka.http-client transport.
method may be a keyword or string. opts accepts :body, :as,
:timeout-ms, additional :headers, and :raw-body?. A body is JSON-encoded
by default; :raw-body? true sends an InputStream or byte array unchanged for
binary gateway routes. Gateway-owned authentication and protocol headers cannot
be overridden. The raw non-throwing response map is returned.
Canonical authenticated HTTP request for gateway development and diagnostics. Resolves or starts the registered daemon for the current DB, acquires this process's client lease, adds protocol/authentication headers without exposing the registry secret, and delegates to the production babashka.http-client transport. `method` may be a keyword or string. `opts` accepts `:body`, `:as`, `:timeout-ms`, additional `:headers`, and `:raw-body?`. A body is JSON-encoded by default; `:raw-body? true` sends an InputStream or byte array unchanged for binary gateway routes. Gateway-owned authentication and protocol headers cannot be overridden. The raw non-throwing response map is returned.
(resource-logs sid rid)Captured output lines for a background via its daemon-side logs-fn, or nil.
Captured output lines for a background via its daemon-side logs-fn, or nil.
(router)GET /v1/router — the unified router dialog payload assembled by the gateway:
{"providers" [{"id" … "label" … "base_url" … "models" [...] "status" {"is_authenticated" …} "limits" {…}} …]}. Returned VERBATIM with
snake_case STRING keys — NO keyword restoration. Consumers read the string
keys directly ((get status "is_authenticated")).
GET /v1/router — the unified router dialog payload assembled by the gateway:
`{"providers" [{"id" … "label" … "base_url" … "models" [...]
"status" {"is_authenticated" …} "limits" {…}} …]}`. Returned VERBATIM with
snake_case STRING keys — NO keyword restoration. Consumers read the string
keys directly (`(get status "is_authenticated")`).(router-diagnostics)The WHOLE provider dialog in ONE gateway call.
GET /v1/router already carries every provider's status and limits, so a
client that wants both for N providers reads it once instead of firing 2×N
per-provider probes. Keyed by provider-id keyword:
{:openai {:status {"is_authenticated" …} :limits {…}}} — :status stays
VERBATIM snake_case strings (same shape provider-status returns) and
:limits is restored to the engine shape provider-limits returns, so both
values drop straight into the callers those two functions already have.
The WHOLE provider dialog in ONE gateway call.
`GET /v1/router` already carries every provider's `status` and `limits`, so a
client that wants both for N providers reads it once instead of firing 2×N
per-provider probes. Keyed by provider-id keyword:
`{:openai {:status {"is_authenticated" …} :limits {…}}}` — `:status` stays
VERBATIM snake_case strings (same shape `provider-status` returns) and
`:limits` is restored to the engine shape `provider-limits` returns, so both
values drop straight into the callers those two functions already have.(run-tui! argv)Run a terminal child while holding this runtime's local gateway lease. Discovery owns native/JVM re-exec, locking and existing-daemon reuse. Never kill a gateway here: its reaper alone decides when all clients and work ended. Connection credentials travel only in the child's environment, never argv.
Run a terminal child while holding this runtime's local gateway lease. Discovery owns native/JVM re-exec, locking and existing-daemon reuse. Never kill a gateway here: its reaper alone decides when all clients and work ended. Connection credentials travel only in the child's environment, never argv.
(search-session-ids query)GET /v1/sessions/actions/search?q= — soul-id STRINGS whose transcript (user request +
assistant text) matches query. Blank query → []. The heavy assistant text
never crosses the wire; callers union these ids into a local title filter.
GET /v1/sessions/actions/search?q= — soul-id STRINGS whose transcript (user request + assistant text) matches `query`. Blank query → []. The heavy assistant text never crosses the wire; callers union these ids into a local title filter.
(search-session-matches query)GET /v1/sessions/actions/search?q= — like search-session-ids but each hit is
TAGGED with WHERE it matched, RANKED by the server, and carries up to a handful
of snippets:
[{:id str :rank 0-3 :in-title? bool :in-request? bool :in-reply? bool :in-thinking? bool :request-snippet str :reply-snippet str :hits [{:side :request|:reply|:thinking :snippet str :at ms}]}].
:in-title? = the session's own name matched; :in-request? = the user's own
request; :in-reply? = the assistant's answer; :in-thinking? = only its
reasoning aside. The vector arrives in the gateway's own order — running
sessions first, then FRESHEST first, the same order its session list is in —
and is painted in it; :rank (0 best) says WHERE the query hit and a surface
never re-orders. Blank query → []. Heavy assistant text never crosses the
wire.
GET /v1/sessions/actions/search?q= — like `search-session-ids` but each hit is
TAGGED with WHERE it matched, RANKED by the server, and carries up to a handful
of snippets:
`[{:id str :rank 0-3 :in-title? bool :in-request? bool :in-reply? bool
:in-thinking? bool :request-snippet str :reply-snippet str
:hits [{:side :request|:reply|:thinking :snippet str :at ms}]}]`.
`:in-title?` = the session's own name matched; `:in-request?` = the user's own
request; `:in-reply?` = the assistant's answer; `:in-thinking?` = only its
reasoning aside. The vector arrives in the gateway's own order — running
sessions first, then FRESHEST first, the same order its session list is in —
and is painted in it; `:rank` (0 best) says WHERE the query hit and a surface
never re-orders. Blank query → []. Heavy assistant text never crosses the
wire.(session-artifacts sid)Every durable artifact sid has produced, string-keyed and in gateway order,
or nil when the daemon cannot answer. nil is UNAVAILABLE — a channel must
paint it differently from an index that is genuinely empty.
Every durable artifact `sid` has produced, string-keyed and in gateway order, or nil when the daemon cannot answer. nil is UNAVAILABLE — a channel must paint it differently from an index that is genuinely empty.
(session-model-cached sid)Footer-frequency read of the session's model pref served from a per-sid
cache that NEVER blocks the caller (issue #29, gateway leg: this used to
be a live session-model HTTP round-trip per footer frame). A stale (or
cold) entry kicks a background single-flight refresh and this returns the
last-known value immediately (nil before the first success).
set-session-model! writes through, so a pick made in THIS client shows
on the very next frame.
Footer-frequency read of the session's model pref served from a per-sid cache that NEVER blocks the caller (issue #29, gateway leg: this used to be a live `session-model` HTTP round-trip per footer frame). A stale (or cold) entry kicks a background single-flight refresh and this returns the last-known value immediately (nil before the first success). `set-session-model!` writes through, so a pick made in THIS client shows on the very next frame.
(session-slashes sid)(session-slashes sid channel)GET the gateway-owned slash catalog for sid and channel. The first call may
initialize Python extensions in the gateway, so it uses the cold-load timeout.
GET the gateway-owned slash catalog for `sid` and `channel`. The first call may initialize Python extensions in the gateway, so it uses the cold-load timeout.
(set-router-default! provider-id model)PATCH /v1/router — tag the PRIMARY provider/model pair (the router root every
turn starts on). Returns {:provider-id … :model …}.
PATCH /v1/router — tag the PRIMARY provider/model pair (the router root every
turn starts on). Returns `{:provider-id … :model …}`.(set-router-fallback!)(set-router-fallback! provider-id model)PATCH /v1/router — tag the FALLBACK provider/model pair: the router's second
root, on a provider the primary does NOT use (the daemon refuses the primary's
own with a 400). Zero args, or a nil provider, CLEARS the tag. Returns the
resulting {:provider-id … :model …}, or nil once cleared.
PATCH /v1/router — tag the FALLBACK provider/model pair: the router's second
root, on a provider the primary does NOT use (the daemon refuses the primary's
own with a 400). Zero args, or a nil provider, CLEARS the tag. Returns the
resulting `{:provider-id … :model …}`, or nil once cleared.(set-session-model! sid provider model)PATCH the session's model pref in the daemon. Writes the returned pref
straight through into the session-model-cached snapshot so the footer
chip flips on the very next frame instead of waiting out the cache TTL.
PATCH the session's model pref in the daemon. Writes the returned pref straight through into the `session-model-cached` snapshot so the footer chip flips on the very next frame instead of waiting out the cache TTL.
(sse-event-action event wanted-turn-id)Pure classifier for one parsed SSE event while blocking on wanted-turn-id.
Returns [action event']:
:terminal — the wanted turn reached a terminal event (return event')
:forward — hand to on-event (own-turn progress OR a sibling turn's
queue-mirror event — see gateway-contract/queue-mirror-event-types),
then keep reading
:skip — another turn's non-queue event, drop it.
A turn.queued.deleted for the WANTED turn is terminal too: the queued
record was pulled back into an editor before it ever ran, so a cancelled
terminal is synthesized instead of blocking on a turn that never starts.
Pure classifier for one parsed SSE event while blocking on `wanted-turn-id`.
Returns `[action event']`:
:terminal — the wanted turn reached a terminal event (return `event'`)
:forward — hand to on-event (own-turn progress OR a sibling turn's
queue-mirror event — see `gateway-contract/queue-mirror-event-types`),
then keep reading
:skip — another turn's non-queue event, drop it.
A `turn.queued.deleted` for the WANTED turn is terminal too: the queued
record was pulled back into an editor before it ever ran, so a cancelled
terminal is synthesized instead of blocking on a turn that never starts.(stale-bounce-verdict {:keys [ours theirs our-build their-build status]})THE rule for replacing a daemon that is merely OLD - a pure decision over what the two halves advertise about themselves and an admin status map (canonical STRING keys).
vis-agent update releases an idle daemon itself, but one a TUI held open
survives the install and would keep serving the old image to every session after
it. The next client to attach is the one that can fix that: it learns the other
half's version AND build out of the /healthz handshake every attach already
pays for. [[protocol/superseded?]] owns that comparison - the release version
where the two carry an order, the build commit where they do not - which is what
makes this work for a dev checkout and for two builds of one VIS_VERSION, in a
native image exactly as in a source JVM.
Use decides the rest, exactly as everywhere else: daemon-idle? over the status
map, tolerating NO client, because this runs before this process takes its lease.
Nobody's open session or running turn is ever aborted to pick up a build, and a
status that could not be read is not evidence of an idle daemon.
Returns {:bounce? :reason :from :to}: :reason is :fresh when there is nothing
to pick up, otherwise the daemon-idle? reason.
THE rule for replacing a daemon that is merely OLD - a pure decision over what
the two halves advertise about themselves and an admin status map (canonical
STRING keys).
`vis-agent update` releases an idle daemon itself, but one a TUI held open
survives the install and would keep serving the old image to every session after
it. The next client to attach is the one that can fix that: it learns the other
half's version AND build out of the `/healthz` handshake every attach already
pays for. [[protocol/superseded?]] owns that comparison - the release version
where the two carry an order, the build commit where they do not - which is what
makes this work for a `dev` checkout and for two builds of one VIS_VERSION, in a
native image exactly as in a source JVM.
Use decides the rest, exactly as everywhere else: [[daemon-idle?]] over the status
map, tolerating NO client, because this runs before this process takes its lease.
Nobody's open session or running turn is ever aborted to pick up a build, and a
status that could not be read is not evidence of an idle daemon.
Returns {:bounce? :reason :from :to}: `:reason` is `:fresh` when there is nothing
to pick up, otherwise the [[daemon-idle?]] reason.(status)Admin status of the gateway this process drives — the REMOTE target when one is configured, else the daemon registered for the current DB. Always the daemon's own wire map (STRING keys), including when nothing is running.
Admin status of the gateway this process drives — the REMOTE target when one is configured, else the daemon registered for the current DB. Always the daemon's own wire map (STRING keys), including when nothing is running.
(stop-daemon!)Stop the daemon registered for this DB, escalating when it stops answering.
POST /v1/admin/stop first; when that is met with silence from a daemon that
still holds its port, signal the pid the registry names ([[kill-registered-daemon!]])
rather than reporting a live orphan and handing the human an lsof. A port held
by a process this registry cannot claim is still reported, never signalled.
Stop the daemon registered for this DB, escalating when it stops answering. `POST /v1/admin/stop` first; when that is met with silence from a daemon that still holds its port, signal the pid the registry names ([[kill-registered-daemon!]]) rather than reporting a live orphan and handing the human an `lsof`. A port held by a process this registry cannot claim is still reported, never signalled.
(stop-daemon-if-idle!)Release the daemon registered for this DB when releasing it is free, and leave
it strictly alone otherwise. This is what runs after vis-agent update: every
live daemon is then older than the runtime on disk, and stopping an unused
managed one costs nothing because the next client spawns the new build. A
--gateway target belongs to another machine and is never touched.
Returns the daemon-idle? verdict plus :stopped? and, when it acted, the
:stop result.
Release the daemon registered for this DB when releasing it is free, and leave it strictly alone otherwise. This is what runs after `vis-agent update`: every live daemon is then older than the runtime on disk, and stopping an unused managed one costs nothing because the next client spawns the new build. A --gateway target belongs to another machine and is never touched. Returns the [[daemon-idle?]] verdict plus `:stopped?` and, when it acted, the `:stop` result.
(stop-resource! sid rid)Run the resource's stop-fn in the daemon and unregister it. Returns the
daemon's stop result map ({:result "stopped"|"unknown"|… :id …}).
Run the resource's stop-fn in the daemon and unregister it. Returns the
daemon's stop result map (`{:result "stopped"|"unknown"|… :id …}`).(synthesize-speech! sid text {:keys [engine-id voice-id on-progress]})Ask the gateway-owned speech engine to synthesize text and return a temporary
WAV file owned by the caller. Progress is streamed for asynchronous jobs. A nil sid needs no conversation.
Ask the gateway-owned speech engine to synthesize `text` and return a temporary WAV file owned by the caller. Progress is streamed for asynchronous jobs. A nil `sid` needs no conversation.
(toggle-setting! id)Atomically flip one boolean setting in the gateway and return its refreshed string-keyed settings row. The gateway owns both persistence and live runtime fan-out; clients must not mutate a process-local toggle registry instead.
Atomically flip one boolean setting in the gateway and return its refreshed string-keyed settings row. The gateway owns both persistence and live runtime fan-out; clients must not mutate a process-local toggle registry instead.
(transcribe-audio! sid audio-path {:keys [engine-id on-progress]})Upload a WAV to the gateway-owned transcription engine, stream its progress,
and return the transcript. audio-path is read by this client only; Sherpa and
its model live solely in the gateway process. A nil sid needs no conversation.
Upload a WAV to the gateway-owned transcription engine, stream its progress, and return the transcript. `audio-path` is read by this client only; Sherpa and its model live solely in the gateway process. A nil `sid` needs no conversation.
(transcript sid)Every turn of sid, hydrated. UNBOUNDED — the whole session is listed AND
hydrated, which on a long session is seconds of work and megabytes of JSON.
Prefer transcript-page for anything interactive.
Every turn of `sid`, hydrated. UNBOUNDED — the whole session is listed AND hydrated, which on a long session is seconds of work and megabytes of JSON. Prefer `transcript-page` for anything interactive.
(transcript-html sid)The gateway-rendered STANDALONE HTML transcript for sid — the canonical
transcript->html, the HTML sibling of transcript-md. Returns the string,
or nil on a non-2xx.
The gateway-rendered STANDALONE HTML transcript for `sid` — the canonical `transcript->html`, the HTML sibling of `transcript-md`. Returns the string, or nil on a non-2xx.
(transcript-md sid)The gateway-rendered user/assistant dialog Markdown for sid — the canonical
transcript->md :dialog. Returns the string, or nil on a non-2xx.
The gateway-rendered user/assistant dialog Markdown for `sid` — the canonical `transcript->md :dialog`. Returns the string, or nil on a non-2xx.
(transcript-page sid {:keys [limit offset]})A WINDOW of sid's transcript — the paging counterpart of transcript.
opts: :limit window size (nil = the whole transcript), :offset 0-based
start in the OLDEST-FIRST list (nil = the NEWEST :limit turns). The gateway
also caps a window in BYTES, so the reply's offset can come back HIGHER
than the one asked for — page from the RETURNED offset, never from your own
arithmetic.
Returns the canonical wire map {"turns" [...] "total" n "offset" n "has_more" bool} (oldest-first turns).
A WINDOW of `sid`'s transcript — the paging counterpart of `transcript`.
`opts`: `:limit` window size (nil = the whole transcript), `:offset` 0-based
start in the OLDEST-FIRST list (nil = the NEWEST `:limit` turns). The gateway
also caps a window in BYTES, so the reply's `offset` can come back HIGHER
than the one asked for — page from the RETURNED `offset`, never from your own
arithmetic.
Returns the canonical wire map `{"turns" [...] "total" n "offset" n
"has_more" bool}` (oldest-first turns).(turn-trace sid tid)Canonical wire iterations of ONE persisted turn (nil when the id is unknown to the daemon).
Canonical wire iterations of ONE persisted turn (nil when the id is unknown to the daemon).
(view-action! sid view-id action)Apply one operator action to the DAEMON-side View view-id of sid.
action is the closed View map: {:action :submit :values …},
{:action :cancel}, {:action :select :node-id … :item-ids …}, or
{:action :interrupt :note …}. Kind is resolved by the daemon from the View,
never encoded into this route. Returns the engine's canonical action outcome.
Apply one operator action to the DAEMON-side View `view-id` of `sid`.
`action` is the closed View map: `{:action :submit :values …}`,
`{:action :cancel}`, `{:action :select :node-id … :item-ids …}`, or
`{:action :interrupt :note …}`. Kind is resolved by the daemon from the View,
never encoded into this route. Returns the engine's canonical action outcome.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 |