SQLite store - V1 schema implementation.
Every public defn in this file is dispatched dynamically by
vis-sdk.core/defdelegate via ns-resolve; clj-kondo never sees
the call sites. The ns-level config above silences
:unused-public-var for the whole file. The actual call surface
is verified through the storage facade tests.
Tables (V1__schema.sql): session_soul, session_state, session_turn_soul, session_turn_state, session_turn_iteration, llm_routing_event, extension_aggregate, log
Connection lifecycle: (db-open! db-spec) -> {:datasource ds :path ...} (db-close! store) -> idempotent dispose
SQLite store - V1 schema implementation.
Every public defn in this file is dispatched dynamically by
`vis-sdk.core/defdelegate` via `ns-resolve`; clj-kondo never sees
the call sites. The ns-level config above silences
`:unused-public-var` for the whole file. The actual call surface
is verified through the storage facade tests.
Tables (V1__schema.sql):
session_soul, session_state,
session_turn_soul, session_turn_state,
session_turn_iteration, llm_routing_event,
extension_aggregate,
log
Connection lifecycle:
(db-open! db-spec) -> {:datasource ds :path ...}
(db-close! store) -> idempotent dispose(db-activity-apply! db sid aid raw-event)Persist one admitted lifecycle edge without retaining the history in heap.
Persist one admitted lifecycle edge without retaining the history in heap.
(db-activity-page db sid aid {:keys [after limit q] :or {after 0 limit 32}})A session-authorized, keyset page of complete records. Search never clips retention.
A session-authorized, keyset page of complete records. Search never clips retention.
(db-activity-settle! db aid outcome summary)Close still-running invocations in bounded batches at the enclosing evaluation boundary.
Close still-running invocations in bounded batches at the enclosing evaluation boundary.
(db-adopt-and-reorder-project-sessions! db-info project-id session-ids)Persist a manual tab order for project-id in one atomic transaction. Named
sessions with no project are adopted first; sessions owned by another project
remain guests and are never stolen. Every target-project member is then
renumbered exactly as in db-reorder-project-sessions!. Returns the member
count.
Persist a manual tab order for `project-id` in one atomic transaction. Named sessions with no project are adopted first; sessions owned by another project remain guests and are never stolen. Every target-project member is then renumbered exactly as in `db-reorder-project-sessions!`. Returns the member count.
(db-append-iteration-attachment! db-info iteration-id att)Append ONE artifact to an EXISTING iteration - a human's own revision of a document the model produced (a markdown note annotated in the companion).
Same table, same grain and the SAME version rule as
[[store-iteration-attachments!]]: re-using a :filename is the NEXT CUT of
that artifact, never a stranger beside it, so an annotated note lands as v2
of the file it came from and the gallery threads the two. The row joins the
iteration's NULL tool-call group (the one group no tool call owns) at the
end of it, which keeps the table's (iteration, tool_call_id, position)
UNIQUE constraint.
The owning turn soul is read off the ITERATION itself, through the turn state its row points at: a live view a human stops long after the block ended files the FIRST artifact that iteration ever gets, and reading the soul off a sibling row would drop it for having no siblings.
att is {:media-type :base64 :filename? :kind? :audience?}. Returns
{:id :version :position} for the stored row, or nil when the iteration is
unknown or the payload does not decode.
Append ONE artifact to an EXISTING iteration - a human's own revision of a
document the model produced (a markdown note annotated in the companion).
Same table, same grain and the SAME version rule as
[[store-iteration-attachments!]]: re-using a `:filename` is the NEXT CUT of
that artifact, never a stranger beside it, so an annotated note lands as `v2`
of the file it came from and the gallery threads the two. The row joins the
iteration's `NULL` tool-call group (the one group no tool call owns) at the
end of it, which keeps the table's `(iteration, tool_call_id, position)`
UNIQUE constraint.
The owning turn soul is read off the ITERATION itself, through the turn state
its row points at: a live view a human stops long after the block ended files
the FIRST artifact that iteration ever gets, and reading the soul off a
sibling row would drop it for having no siblings.
`att` is `{:media-type :base64 :filename? :kind? :audience?}`. Returns
`{:id :version :position}` for the stored row, or nil when the iteration is
unknown or the payload does not decode.(db-claim-session! db-info session-id)Mark an unclaimed warm-pool soul as a real conversation. Idempotent.
Mark an unclaimed warm-pool soul as a real conversation. Idempotent.
(db-close! store)Idempotent dispose. Releases our reference on the per-file Hikari pool when
we own it - the pool itself is closed (after draining in-flight leases, see
close-pool!) once the last holder lets go. For :external mode
(caller-supplied DataSource) it's a no-op - the caller still owns the handle
they passed in.
Idempotent means idempotent PER STORE, enforced by the store's own
:dispose-once latch: the reference is dropped exactly once however often
the same store is disposed. dispose-environment! is reachable from several
teardown paths (eviction, delete!, close-all!, the main finally), and
a second release would close a pool other environments are still using.
Idempotent dispose. Releases our reference on the per-file Hikari pool when we own it - the pool itself is closed (after draining in-flight leases, see `close-pool!`) once the last holder lets go. For `:external` mode (caller-supplied DataSource) it's a no-op - the caller still owns the handle they passed in. Idempotent means idempotent PER STORE, enforced by the store's own `:dispose-once` latch: the reference is dropped exactly once however often the same store is disposed. `dispose-environment!` is reachable from several teardown paths (eviction, `delete!`, `close-all!`, the `main` finally), and a second release would close a pool other environments are still using.
(db-compare-session-goal! db-info session-id revision goal)Replace a goal only if its persisted revision still matches. Missing sessions refuse.
Replace a goal only if its persisted revision still matches. Missing sessions refuse.
(db-council-bind-wake! db entry-id sid activation)Claim one newly published idle ping for the runtime's selected activation.
Claim one newly published idle ping for the runtime's selected activation.
(db-council-exchanged? db thread a b)Whether two sessions have a committed request/reply pair in this exact thread. A shared broadcast, log read or unanswered outgoing ping is not an exchange.
Whether two sessions have a committed request/reply pair in this exact thread. A shared broadcast, log read or unanswered outgoing ping is not an exchange.
(db-council-insert! db row recipients infer-reply?)Entry, inferred reply and frozen recipients commit together; replays recheck inside the writer.
Entry, inferred reply and frozen recipients commit together; replays recheck inside the writer.
(db-council-page db gid thread roots? after limit)Keyset pages. A thread uses a root PK seek and an indexed continuation seek, never a group scan.
Keyset pages. A thread uses a root PK seek and an indexed continuation seek, never a group scan.
(db-council-pending db sid activation gid after limit)Normal pings are activation-scoped. Unacknowledged reply notifications survive activation changes.
Normal pings are activation-scoped. Unacknowledged reply notifications survive activation changes.
(db-council-source db sid source)Resolve immutable execution identities from trusted host coordinates, never client fields. Missing execution coordinates stay nil; session_id is the session soul identity.
Resolve immutable execution identities from trusted host coordinates, never client fields. Missing execution coordinates stay nil; session_id is the session soul identity.
(db-create-project! db-info
{:keys [name color owner-id position workspace-root]})Create a project (always cross-channel). :name is required (non-blank).
:owner-id defaults to "local". :position, when omitted, appends after
the owner's current projects. Returns the created project (canonical shape).
Create a `project` (always cross-channel). `:name` is required (non-blank). `:owner-id` defaults to "local". `:position`, when omitted, appends after the owner's current projects. Returns the created project (canonical shape).
(db-delete-project! db-info project-id)Delete a project. Member souls are scattered back to project-less by the
ON DELETE SET NULL FK - conversations are NEVER deleted.
Delete a `project`. Member souls are scattered back to project-less by the `ON DELETE SET NULL` FK - conversations are NEVER deleted.
(db-error->user-message e)Translate SQLite persistence exceptions into actionable user text.
Return nil for non-SQLite errors so the facade can try other adapters
or fall back to (ex-message e).
Translate SQLite persistence exceptions into actionable user text. Return nil for non-SQLite errors so the facade can try other adapters or fall back to `(ex-message e)`.
(db-fork-session! db-info
session-id
{:keys [system-prompt provider model title workspace-id]})Fork a session. Creates a new session_state with parent_state_id pointing to the current latest state. The forked state gets a '(fork)' suffix when no title is supplied. The parent state gets a '[forked]' suffix to mark the divergence point. Returns the new state UUID.
Required opt: :workspace-id — every session_state must be pinned to
exactly one workspace. Callers (screen handlers, cli) call
workspace/spawn-branch! or workspace/ensure-trunk! first and pass
the returned id here.
Fork a session. Creates a new session_state with parent_state_id pointing to the current latest state. The forked state gets a '(fork)' suffix when no title is supplied. The parent state gets a '[forked]' suffix to mark the divergence point. Returns the new state UUID. Required opt: `:workspace-id` — every session_state must be pinned to exactly one workspace. Callers (screen handlers, cli) call `workspace/spawn-branch!` or `workspace/ensure-trunk!` first and pass the returned id here.
(db-fork-session-at-turn! db-info
session-id
{:keys [title workspace-id through-turn-id]})Fork a session UP TO AND INCLUDING the turn whose session_turn_soul id is
:through-turn-id, into a brand-new INDEPENDENT session.
Unlike db-fork-session! (which adds a new session_state UNDER the same
soul, so the soul's latest-leaf chain keeps ALL prior turns), this mints a
fresh session_soul + root session_state (version 0, parent_state_id
NULL) and DEEP-COPIES every transcript turn from the start through
:through-turn-id into it — each turn's session_turn_soul, its LATEST
session_turn_state, that state's session_turn_iteration rows, and the
turn's session_attachment rows (ids remapped). The SOURCE session is left
completely untouched; the fork is a separate tab/session whose history is
exactly those turns and which continues fresh from there.
Copies rows generically (SELECT * → re-insert with remapped id/FK columns),
so it stays correct as columns evolve. Required opt: :workspace-id. Returns
the new SESSION SOUL UUID (for resume-session, which resolves sessions by
session_soul.id), or nil when :through-turn-id
is not a turn of the source session (or the env has no datasource).
Fork a session UP TO AND INCLUDING the turn whose `session_turn_soul` id is `:through-turn-id`, into a brand-new INDEPENDENT session. Unlike [[db-fork-session!]] (which adds a new `session_state` UNDER the same soul, so the soul's latest-leaf chain keeps ALL prior turns), this mints a fresh `session_soul` + root `session_state` (version 0, `parent_state_id` NULL) and DEEP-COPIES every transcript turn from the start through `:through-turn-id` into it — each turn's `session_turn_soul`, its LATEST `session_turn_state`, that state's `session_turn_iteration` rows, and the turn's `session_attachment` rows (ids remapped). The SOURCE session is left completely untouched; the fork is a separate tab/session whose history is exactly those turns and which continues fresh from there. Copies rows generically (`SELECT *` → re-insert with remapped id/FK columns), so it stays correct as columns evolve. Required opt: `:workspace-id`. Returns the new SESSION SOUL UUID (for `resume-session`, which resolves sessions by `session_soul.id`), or nil when `:through-turn-id` is not a turn of the source session (or the env has no datasource).
(db-get-project db-info project-id)Return one project (canonical shape, with live :session-count) or nil.
Return one `project` (canonical shape, with live `:session-count`) or nil.
(db-get-project-by-root db-info owner-id root)Return the project bound to canonical workspace root for owner-id
(default "local"), or nil. Backs the TUI's launch-dir -> project (tab set)
resolution. root matches the stored project.workspace_root exactly
(callers pass a canonical path).
Return the `project` bound to canonical workspace `root` for `owner-id` (default "local"), or nil. Backs the TUI's launch-dir -> project (tab set) resolution. `root` matches the stored `project.workspace_root` exactly (callers pass a canonical path).
(db-get-session-goal db-info session-id)Read the canonical string-keyed goal, or nil. Goals belong to a soul, not a fold.
Read the canonical string-keyed goal, or nil. Goals belong to a soul, not a fold.
(db-get-session-model-pref db-info session-id)The persisted model preference for a session (soul id) as
{:provider p :model m}, or nil for the router default. Channel-neutral:
web + TUI route through the same value.
The persisted model preference for a session (soul id) as
`{:provider p :model m}`, or nil for the router default. Channel-neutral:
web + TUI route through the same value.(db-get-session-prompt-cache-state db-info session-state-id)Return the latest exact provider-prefix checkpoint stored on one session state.
Return the latest exact provider-prefix checkpoint stored on one session state.
(db-latest-session-state-id db-info session-id)Return the latest session_state.id UUID for the soul behind
session-id (which is the soul id). Used by the iteration
loop to bind TURN_SESSION_STATE_ID so the agent can reference
the exact session_state row this turn was attached to.
Returns nil when the session is unknown or the env has no
datasource.
Return the latest `session_state.id` UUID for the soul behind `session-id` (which is the soul id). Used by the iteration loop to bind `TURN_SESSION_STATE_ID` so the agent can reference the exact `session_state` row this turn was attached to. Returns nil when the session is unknown or the env has no datasource.
(db-list-iteration-attachments db-info iteration-id)Ordered OUTBOUND tool artifacts persisted for ONE session_turn_iteration
(its id, not the soul - the tool rail only). Returns [{:id :source :tool-call-id :position :kind :media-type :filename :size :base64}] -
base64-encoded inline bytes matching the shape the prompt assembler + web
history re-render consume - ordered by (tool_call_id, position), or []
when none / no datasource.
Ordered OUTBOUND tool artifacts persisted for ONE `session_turn_iteration`
(its id, not the soul - the `tool` rail only). Returns `[{:id :source
:tool-call-id :position :kind :media-type :filename :size :base64}]` -
base64-encoded inline bytes matching the shape the prompt assembler + web
history re-render consume - ordered by `(tool_call_id, position)`, or `[]`
when none / no datasource.(db-list-iteration-attachments-meta db-info iteration-id)db-list-iteration-attachments WITHOUT the bytes - the same rows in the
same (tool_call_id, position) order, :base64 replaced by :has-bytes.
THE lister for everything that only NUMBERS or DESCRIBES an iteration's
artifacts; the one blob a caller actually serves is then read by id with
db-read-attachment, so serving image N costs one blob and not all N.
[[db-list-iteration-attachments]] WITHOUT the bytes - the same rows in the same `(tool_call_id, position)` order, `:base64` replaced by `:has-bytes`. THE lister for everything that only NUMBERS or DESCRIBES an iteration's artifacts; the one blob a caller actually serves is then read by id with [[db-read-attachment]], so serving image N costs one blob and not all N.
(db-list-iterations-attachments db-info iteration-ids)Batch variant of db-list-iteration-attachments: OUTBOUND tool artifacts
for MANY session_turn_iteration ids in ONE query, grouped as
{iteration-id-string [{:id :source :tool-call-id :position :kind :media-type :filename :size :base64} …]} (each vector ordered by (tool_call_id, position)). Missing ids are simply absent. Safe: no ids / no datasource ->
{}. Lets a history replay hydrate a whole conversation's generated images
without an N+1 per-iteration query.
Batch variant of [[db-list-iteration-attachments]]: OUTBOUND tool artifacts
for MANY `session_turn_iteration` ids in ONE query, grouped as
`{iteration-id-string [{:id :source :tool-call-id :position :kind :media-type
:filename :size :base64} …]}` (each vector ordered by `(tool_call_id,
position)`). Missing ids are simply absent. Safe: no ids / no datasource ->
`{}`. Lets a history replay hydrate a whole conversation's generated images
without an N+1 per-iteration query.(db-list-iterations-attachments-meta db-info iteration-ids)Batch variant of db-list-iteration-attachments-meta: bytes-free rows for
MANY session_turn_iteration ids in ONE query, grouped as
{iteration-id-string [{:id :source :tool-call-id :position :kind :media-type :filename :audience :size :has-bytes :storage-uri} …]}. Missing ids are
simply absent. Safe: no ids / no datasource -> {}. A transcript page reads
every iteration's artifact LIST this way and stays byte-free.
Batch variant of [[db-list-iteration-attachments-meta]]: bytes-free rows for
MANY `session_turn_iteration` ids in ONE query, grouped as
`{iteration-id-string [{:id :source :tool-call-id :position :kind :media-type
:filename :audience :size :has-bytes :storage-uri} …]}`. Missing ids are
simply absent. Safe: no ids / no datasource -> `{}`. A transcript page reads
every iteration's artifact LIST this way and stays byte-free.(db-list-projects db-info {:keys [owner-id include-archived?]})List projects for owner-id (default "local"), each with a live
:session-count, ordered by (position, created_at). Projects are
CROSS-CHANNEL by construction — every channel sees the same set.
Archived projects are hidden unless :include-archived? is truthy.
List `project`s for `owner-id` (default "local"), each with a live `:session-count`, ordered by (position, created_at). Projects are CROSS-CHANNEL by construction — every channel sees the same set. Archived projects are hidden unless `:include-archived?` is truthy.
(db-list-session-attachments db-info session-id)EVERY attachment across a whole SESSION - user images AND tool artifacts -
scoped to the active branch's state chain (root->leaf). ONE join through
session_turn_soul scopes session_attachment by the session's states;
both rails answer because tool rows denormalize the soul. Returns [{:id
:source :turn-soul-id :iteration-id :tool-call-id :position :kind :media-type
:filename :size :storage-uri :base64}] ordered by (turn position, source
[user first], position) so callers split by :source (:user / :tool) or
slice per :turn-soul-id - or [] when none / no datasource. The
whole-session roll-up sibling of db-list-turn-all-attachments (one turn)
and db-list-iteration-attachments (one iteration); replaces the
introspection reader's hand-rolled per-turn N+1 walk.
EVERY attachment across a whole SESSION - user images AND tool artifacts -
scoped to the active branch's state chain (root->leaf). ONE join through
`session_turn_soul` scopes `session_attachment` by the session's states;
both rails answer because tool rows denormalize the soul. Returns [{:id
:source :turn-soul-id :iteration-id :tool-call-id :position :kind :media-type
:filename :size :storage-uri :base64}] ordered by (turn position, source
[user first], position) so callers split by `:source` (`:user` / `:tool`) or
slice per `:turn-soul-id` - or `[]` when none / no datasource. The
whole-session roll-up sibling of [[db-list-turn-all-attachments]] (one turn)
and [[db-list-iteration-attachments]] (one iteration); replaces the
introspection reader's hand-rolled per-turn N+1 walk.(db-list-session-attachments-meta db-info session-id)db-list-session-attachments WITHOUT the bytes — the same rows in the same
order, :base64 replaced by :has-bytes. THE lister for a whole-session
artifact INDEX: a client that wants to know everything a session produced
asks once and pays metadata, then fetches the one blob it renders by id.
[[db-list-session-attachments]] WITHOUT the bytes — the same rows in the same order, `:base64` replaced by `:has-bytes`. THE lister for a whole-session artifact INDEX: a client that wants to know everything a session produced asks once and pays metadata, then fetches the one blob it renders by id.
(db-list-session-states db-info session-id)List every session_state row for the soul behind session-id,
oldest version first. Each row maps to
{:state-id :version :parent-state-id :title :system-prompt :provider :model :created-at :turn-count} - the raw fork tree of one session soul.
The trunk is :version 0 with :parent-state-id nil. A fork is any row
whose :parent-state-id points at another :state-id in the same vector;
group-by :parent-state-id to walk the tree.
:turn-count is the number of session_turn_soul rows hanging off that specific
state - cheap to compute, useful when triaging which branch is active.
Returns [] (never nil) when the session is unknown or the env has no
datasource.
List every `session_state` row for the soul behind `session-id`,
oldest version first. Each row maps to
`{:state-id :version :parent-state-id :title :system-prompt :provider :model
:created-at :turn-count}` - the raw fork tree of one session soul.
The trunk is `:version 0` with `:parent-state-id nil`. A fork is any row
whose `:parent-state-id` points at another `:state-id` in the same vector;
group-by `:parent-state-id` to walk the tree.
`:turn-count` is the number of `session_turn_soul` rows hanging off that specific
state - cheap to compute, useful when triaging which branch is active.
Returns `[]` (never nil) when the session is unknown or the env has no
datasource.(db-list-session-turn-iterations db-info session-turn-id)List the latest state's iterations for canonical turn soul session-turn-id.
List the latest state's iterations for canonical turn soul `session-turn-id`.
(db-list-session-turn-states db-info session-turn-id)List every session_turn_state row (i.e. every retry version) for the soul behind
session-turn-id, oldest version first. Each row maps to
{:state-id :version :forked-from-session-turn-state-id :status :prior-outcome :provider :model :created-at :iteration-count}.
Version 0 with :forked-from-session-turn-state-id nil is the original run; any
higher version is a retry, with :forked-from-session-turn-state-id pointing at
the previous :state-id. :iteration-count is the number of iteration
rows attached to that specific state - retries get their own iteration
trace.
Returns [] (never nil) when the turn is unknown or the env has no
datasource.
List every `session_turn_state` row (i.e. every retry version) for the soul behind
`session-turn-id`, oldest version first. Each row maps to
`{:state-id :version :forked-from-session-turn-state-id :status :prior-outcome
:provider :model :created-at :iteration-count}`.
Version 0 with `:forked-from-session-turn-state-id nil` is the original run; any
higher version is a retry, with `:forked-from-session-turn-state-id` pointing at
the previous `:state-id`. `:iteration-count` is the number of `iteration`
rows attached to that specific state - retries get their own iteration
trace.
Returns `[]` (never nil) when the turn is unknown or the env has no
datasource.(db-list-sessions db-info channel)Top-level sessions, newest-first.
channel filters by the session_soul.channel column; pass :all
(or nil) for the CROSS-CHANNEL view — every channel's sessions in one
list. This is the shared view web + TUI use so a conversation started
in one surface is visible and resumable from the other.
Top-level sessions, newest-first. `channel` filters by the `session_soul.channel` column; pass `:all` (or nil) for the CROSS-CHANNEL view — every channel's sessions in one list. This is the shared view web + TUI use so a conversation started in one surface is visible and resumable from the other.
(db-list-turn-all-attachments db-info session-turn-soul-id)EVERY attachment hanging off one TURN - user images AND tool artifacts
together - in ONE indexed filter (session_turn_soul_id = ?), the roll-up
that single-table unification makes trivial. Tool rows denormalize the soul,
so both rails answer the same WHERE. Returns [{:id :source :tool-call-id :position :kind :media-type :filename :size :base64}] ordered by (source, position) so user images lead, then tool artifacts - or [] when none / no
datasource. Callers split by :source (:user / :tool) as needed.
EVERY attachment hanging off one TURN - user images AND tool artifacts
together - in ONE indexed filter (`session_turn_soul_id = ?`), the roll-up
that single-table unification makes trivial. Tool rows denormalize the soul,
so both rails answer the same `WHERE`. Returns `[{:id :source :tool-call-id
:position :kind :media-type :filename :size :base64}]` ordered by `(source,
position)` so user images lead, then tool artifacts - or `[]` when none / no
datasource. Callers split by `:source` (`:user` / `:tool`) as needed.(db-list-turn-attachments db-info session-turn-soul-id)Ordered INBOUND user images persisted for one session_turn_soul (the user
rail only - session_turn_iteration_id IS NULL). Returns [{:id :source :position :kind :media-type :filename :size :base64}] - :id is the bare row
uuid for read-back via db-read-attachment, base64-encoded bytes match the
shape the prompt assembler + web history re-render consume - or [] when
none / no datasource. For user+tool combined see db-list-turn-all-attachments.
Ordered INBOUND user images persisted for one `session_turn_soul` (the `user`
rail only - `session_turn_iteration_id IS NULL`). Returns `[{:id :source
:position :kind :media-type :filename :size :base64}]` - `:id` is the bare row
uuid for read-back via `db-read-attachment`, base64-encoded bytes match the
shape the prompt assembler + web history re-render consume - or `[]` when
none / no datasource. For user+tool combined see [[db-list-turn-all-attachments]].(db-list-turns-attachments db-info session-turn-soul-ids)Batch variant of db-list-turn-attachments: INBOUND user images for MANY
session_turn_soul ids in ONE query (the user rail only -
session_turn_iteration_id IS NULL), grouped as {soul-id-string [{:id :source :position :kind :media-type :filename :size :base64} …]} (each vector
ordered by :position). Missing ids are simply absent from the map. Safe: no
ids / no datasource -> {}. Lets the gateway hydrate a whole session's user
images without an N+1 per-turn query.
Batch variant of [[db-list-turn-attachments]]: INBOUND user images for MANY
`session_turn_soul` ids in ONE query (the `user` rail only -
`session_turn_iteration_id IS NULL`), grouped as `{soul-id-string [{:id
:source :position :kind :media-type :filename :size :base64} …]}` (each vector
ordered by `:position`). Missing ids are simply absent from the map. Safe: no
ids / no datasource -> `{}`. Lets the gateway hydrate a whole session's user
images without an N+1 per-turn query.(db-load-ctx-history db-info session-id)Return a sorted-by-turn vec of [turn-n ctx-map] pairs for the session.
Each ctx-map is the Nippy-decoded :session/turn_state.ctx for the
latest version of that turn. Used by introspect-* verbs to reach
archived entries and replay any past turn snapshot through the engine's
pure-fn introspect-spec / -task / -fact / -archived / -ctx-at helpers.
The session_state chain is honoured (forks see ancestor snapshots ordered by parent_state -> child_state and then by turn position).
Return a sorted-by-turn vec of `[turn-n ctx-map]` pairs for the session. Each ctx-map is the Nippy-decoded `:session/turn_state.ctx` for the latest version of that turn. Used by introspect-* verbs to reach archived entries and replay any past turn snapshot through the engine's pure-fn introspect-spec / -task / -fact / -archived / -ctx-at helpers. The session_state chain is honoured (forks see ancestor snapshots ordered by parent_state -> child_state and then by turn position).
(db-load-latest-ctx db-info session-id)Load the CTX snapshot (Nippy BLOB) from the latest session_turn_state that has a non-NULL ctx column, scoped to this session_state. Returns the decoded CTX map or nil when the session has no persisted CTX yet. STRING-KEYED: ctx blobs are frozen string-keyed (strings-only end to end).
This is the resume path: on a new turn, the loop reads this back into the ctx-atom so the model picks up where (done …) left off. The cursor is intentionally NOT restored — it's iter-local and gets stamped fresh by the renderer.
Load the CTX snapshot (Nippy BLOB) from the latest session_turn_state that has a non-NULL ctx column, scoped to this session_state. Returns the decoded CTX map or nil when the session has no persisted CTX yet. STRING-KEYED: ctx blobs are frozen string-keyed (strings-only end to end). This is the resume path: on a new turn, the loop reads this back into the ctx-atom so the model picks up where (done …) left off. The cursor is intentionally NOT restored — it's iter-local and gets stamped fresh by the renderer.
(db-read-attachment db-info attachment-id)Read ONE persisted attachment by its bare row id (as returned by the
db-list-*-attachments listers). There is exactly ONE session_attachment
table, so this is a single indexed lookup - no source prefix, no dispatch, no
cross-table fallback. :source is derived from the row (:tool when it
carries an iteration, else :user). Returns {:id :source :tool-call-id :position :kind :media-type :filename :size :storage-uri :base64} (
:tool-call-id nil for user images) or nil when the id is absent / no
datasource. The read-back twin of the listers: a tool re-fetches an artifact
it (or an earlier turn, or the user) produced.
Read ONE persisted attachment by its bare row id (as returned by the
`db-list-*-attachments` listers). There is exactly ONE `session_attachment`
table, so this is a single indexed lookup - no source prefix, no dispatch, no
cross-table fallback. `:source` is derived from the row (`:tool` when it
carries an iteration, else `:user`). Returns `{:id :source :tool-call-id
:position :kind :media-type :filename :size :storage-uri :base64}` (
`:tool-call-id` nil for user images) or nil when the id is absent / no
datasource. The read-back twin of the listers: a tool re-fetches an artifact
it (or an earlier turn, or the user) produced.(db-reorder-project-sessions! db-info project-id session-ids)Persist a manual order for the sessions inside project-id. session-ids is
the desired leading order; any members not named are kept and appended in their
current order. Every member receives a gap-free, 0-based project_position.
Only souls already belonging to project-id are touched. Returns the member
count.
Persist a manual order for the sessions inside `project-id`. `session-ids` is the desired leading order; any members not named are kept and appended in their current order. Every member receives a gap-free, 0-based `project_position`. Only souls already belonging to `project-id` are touched. Returns the member count.
(db-repo-focus-get db-info repo-id)Return the workspace_id currently pinned as the focus pointer for
repo-id. Nil when no entry exists yet.
Return the `workspace_id` currently pinned as the focus pointer for `repo-id`. Nil when no entry exists yet.
(db-repo-focus-set! db-info repo-id workspace-id)Upsert the per-repo focus pointer to workspace-id. Updates
updated_at_ms to now. Returns the new pointer map.
Upsert the per-repo focus pointer to `workspace-id`. Updates `updated_at_ms` to now. Returns the new pointer map.
(db-retry-session-turn! db-info
session-turn-soul-id
{:keys [status provider model]})Create a new session_turn_state (version N+1) for an existing session_turn_soul. Used when re-running a turn with a different provider/model or settings. Returns the new session-turn-state UUID.
Create a new session_turn_state (version N+1) for an existing session_turn_soul. Used when re-running a turn with a different provider/model or settings. Returns the new session-turn-state UUID.
(db-search db-info query)(db-search db-info query {:keys [limit owner-table field]})Search persisted prompts, answers, and thinking through SQLite FTS5.
Search persisted prompts, answers, and thinking through SQLite FTS5.
(db-search-session-ids db-info channel query)Soul ids whose TRANSCRIPT text matches query.
Thin id-only projection of db-search-session-matches (see it for the
where-it-hit tags and snippets). channel filters like db-list-sessions
(:all/nil = cross-channel). Blank query returns [].
Soul ids whose TRANSCRIPT text matches `query`. Thin id-only projection of `db-search-session-matches` (see it for the where-it-hit tags and snippets). `channel` filters like `db-list-sessions` (`:all`/nil = cross-channel). Blank query returns `[]`.
(db-search-session-matches db-info channel query)Sessions whose TITLE or TRANSCRIPT text matches query, RANKED, each carrying
WHERE it hit and up to transcript-hits-per-session MATCH SNIPPETS:
{:id uuid :rank 0-3 :in-title? bool :in-request? bool :in-reply? bool :in-thinking? bool :request-snippet str-or-nil :reply-snippet str-or-nil :hits [{:side :request|:reply|:thinking :snippet str :at Date}]}
:in-title? = the session's own name matched (title-hit-rows, substring);
:in-request? = the user's request (session_turn_soul.user_request) matched;
:in-reply? = the assistant's ANSWER (llm_assistant_prose) matched;
:in-thinking? = its reasoning aside (llm_thinking) matched — the weakest
band, since it is what neither of them said. Any of the four can be true
together.
The RESULT ORDER IS THE ANSWER, and the answer is FRESHNESS: sessions come
back newest-first by the instant each one last moved (session-activity-at
— the same fact a list read prints as modified_at), so a result list reads
like the session list it filters and the dates only ever fall as you scan
down. Someone hunting a conversation looks for the one they were in, not for
the strongest lexical band: a title match from a year ago must not sit above
this morning's session. :rank still travels — it breaks a tie between two
sessions that last moved at the same instant, orders the hits INSIDE a
session, and tells a surface WHERE the query hit without it re-deciding
anything. See transcript-hit-side-rank.
Transcript matching is served by the V1 FTS5 indexes (transcript_request_fts /
transcript_reply_fts) — single-digit milliseconds instead of the ~400ms full
LIKE '%q%' scan of the whole corpus, which is what made TUI/app session search
feel frozen. It is token PREFIX matching (dia finds dialogs); a query with
NO alphanumeric token at all (???) still searches TITLES, which are matched
as a plain substring.
This is the SERVER-side half of session search: the assistant text never
crosses the wire, only these snippet windows. channel filters like
db-list-sessions (:all/nil = cross-channel). Blank query returns [].
Sessions whose TITLE or TRANSCRIPT text matches `query`, RANKED, each carrying
WHERE it hit and up to `transcript-hits-per-session` MATCH SNIPPETS:
`{:id uuid :rank 0-3 :in-title? bool :in-request? bool :in-reply? bool
:in-thinking? bool :request-snippet str-or-nil :reply-snippet str-or-nil
:hits [{:side :request|:reply|:thinking :snippet str :at Date}]}`
`:in-title?` = the session's own name matched (`title-hit-rows`, substring);
`:in-request?` = the user's request (`session_turn_soul.user_request`) matched;
`:in-reply?` = the assistant's ANSWER (`llm_assistant_prose`) matched;
`:in-thinking?` = its reasoning aside (`llm_thinking`) matched — the weakest
band, since it is what neither of them said. Any of the four can be true
together.
The RESULT ORDER IS THE ANSWER, and the answer is FRESHNESS: sessions come
back newest-first by the instant each one last moved (`session-activity-at`
— the same fact a list read prints as `modified_at`), so a result list reads
like the session list it filters and the dates only ever fall as you scan
down. Someone hunting a conversation looks for the one they were in, not for
the strongest lexical band: a title match from a year ago must not sit above
this morning's session. `:rank` still travels — it breaks a tie between two
sessions that last moved at the same instant, orders the hits INSIDE a
session, and tells a surface WHERE the query hit without it re-deciding
anything. See `transcript-hit-side-rank`.
Transcript matching is served by the V1 FTS5 indexes (`transcript_request_fts` /
`transcript_reply_fts`) — single-digit milliseconds instead of the ~400ms full
`LIKE '%q%'` scan of the whole corpus, which is what made TUI/app session search
feel frozen. It is token PREFIX matching (`dia` finds `dialogs`); a query with
NO alphanumeric token at all (`???`) still searches TITLES, which are matched
as a plain substring.
This is the SERVER-side half of session search: the assistant text never
crosses the wire, only these snippet windows. `channel` filters like
`db-list-sessions` (`:all`/nil = cross-channel). Blank query returns `[]`.(db-session-state-list-for-workspace db-info workspace-id)List session_state rows whose workspace_id = workspace-id,
newest first. The 1:1 invariant guarantees at most one ACTIVE row
per workspace, but historical version rows (from forks) may exist
so the projection is a vec, not a single row. Returns canonical
{:id :session-soul-id :title :version :created-at …} shape.
List session_state rows whose `workspace_id` = `workspace-id`,
newest first. The 1:1 invariant guarantees at most one ACTIVE row
per workspace, but historical version rows (from forks) may exist
so the projection is a vec, not a single row. Returns canonical
`{:id :session-soul-id :title :version :created-at …}` shape.(db-session-state-set-workspace! db-info session-state-id workspace-id)Pin session-state-id to workspace-id. Caller guarantees the
target session_state row exists and has no other workspace pinned
(UNIQUE on session_state.workspace_id enforces 1:1).
Pin `session-state-id` to `workspace-id`. Caller guarantees the target session_state row exists and has no other workspace pinned (UNIQUE on session_state.workspace_id enforces 1:1).
(db-session-turn-stats db-info)(db-session-turn-stats db-info session-id)Per-session turn aggregates: {:turn-count n :latest-turn-at Date}.
1-arity: the WHOLE store in ONE grouped query, keyed by soul-id string —
{soul-id-str stats}. Powers the session picker summaries (turn_count +
modified_at folded into list-sessions) without an N+1 per-session
db-list-session-turns hydration.
2-arity: the SAME aggregate for ONE session, returned unwrapped (nil when
the session has no state rows). GET /v1/sessions/:id needs these two facts
as much as the list does — without them a client cannot tell that a session
moved — and a detail poll must not scan every session to learn them.
COUNTS AND CLOCKS ONLY, never turn TEXT. This runs on EVERY list read, and a
second grouped query for each session's opening user_request pulled half a
megabyte of prose (1041 rows, 32ms measured) out of the store to decorate a
20-row window — where it was a quarter of the bytes on the wire and no client
painted it. Turn text is a session-scoped read.
Counts every turn soul across ALL of a session's states (forks included) — an
upper bound of the chain view, but exact for has any turns? and for
latest-activity ordering.
Per-session turn aggregates: `{:turn-count n :latest-turn-at Date}`.
1-arity: the WHOLE store in ONE grouped query, keyed by soul-id string —
`{soul-id-str stats}`. Powers the session picker summaries (`turn_count` +
`modified_at` folded into list-sessions) without an N+1 per-session
`db-list-session-turns` hydration.
2-arity: the SAME aggregate for ONE session, returned unwrapped (nil when
the session has no state rows). `GET /v1/sessions/:id` needs these two facts
as much as the list does — without them a client cannot tell that a session
moved — and a detail poll must not scan every session to learn them.
COUNTS AND CLOCKS ONLY, never turn TEXT. This runs on EVERY list read, and a
second grouped query for each session's opening `user_request` pulled half a
megabyte of prose (1041 rows, 32ms measured) out of the store to decorate a
20-row window — where it was a quarter of the bytes on the wire and no client
painted it. Turn text is a session-scoped read.
Counts every turn soul across ALL of a session's states (forks included) — an
upper bound of the chain view, but exact for `has any turns?` and for
latest-activity ordering.(db-session-usage-stats db-info session-id)ONE session's whole-life USAGE rollup, or nil when the session has no turns:
{:turn-count :iteration-count :duration-ms :input-tokens :input-regular-tokens :input-cache-write-tokens :input-cache-read-tokens :prompt-cache-reusable-tokens :prompt-cache-reused-tokens :prompt-cache-sample-count :prompt-cache-estimated-sample-count :prompt-cache-rebuild-count :prompt-cache-expired-count :output-tokens :output-reasoning-tokens :cost-usd :first-turn-at :last-turn-at :provider :model :tool-call-count :fold-count :health}
:health describes the latest measured request in the same current chain.
Provider input is authoritative; budget and prompt provenance exist only for
calls that recorded them. It never reconstructs old instructions from disk.
Token, cost and cache facts are ONE skinny SQL aggregate over the session's
session_turn_iteration rows — the LLM calls themselves — and never over the
turn rollups. A rollup is written when a turn ENDS, so reading it dropped the
tokens and the cost of every interrupted and every still-running turn out of
the session total for good, and left the two cache percentages measured over
two different populations. Turn COUNT and wall-clock duration stay on the turn
states, the only rows that know them.
TOOL and FOLD counts live inside each iteration's Nippy tool_calls BLOB:
tools are keyed by :vis/tool-name; :vis/fold-count records successful fold
operations independently of stdout. usage-tally thaws any one
row's blob at most ONCE per process and memoises the merged result per
session — a re-read costs two skinny id-only queries, a repeat read of an
unchanged session costs nothing beyond them, and a live session pays only for
the iterations added since the last read. It is still an ON-DEMAND
single-session read and never part of list-sessions.
Only the TOTALS survive the blob pass: per-tool and per-error rankings had no reader left on any channel, so they are not computed.
ONE session's whole-life USAGE rollup, or nil when the session has no turns:
`{:turn-count :iteration-count :duration-ms :input-tokens
:input-regular-tokens :input-cache-write-tokens :input-cache-read-tokens
:prompt-cache-reusable-tokens :prompt-cache-reused-tokens
:prompt-cache-sample-count :prompt-cache-estimated-sample-count
:prompt-cache-rebuild-count :prompt-cache-expired-count :output-tokens
:output-reasoning-tokens :cost-usd :first-turn-at :last-turn-at :provider
:model :tool-call-count :fold-count :health}`
`:health` describes the latest measured request in the same current chain.
Provider input is authoritative; budget and prompt provenance exist only for
calls that recorded them. It never reconstructs old instructions from disk.
Token, cost and cache facts are ONE skinny SQL aggregate over the session's
`session_turn_iteration` rows — the LLM calls themselves — and never over the
turn rollups. A rollup is written when a turn ENDS, so reading it dropped the
tokens and the cost of every interrupted and every still-running turn out of
the session total for good, and left the two cache percentages measured over
two different populations. Turn COUNT and wall-clock duration stay on the turn
states, the only rows that know them.
TOOL and FOLD counts live inside each iteration's Nippy `tool_calls` BLOB:
tools are keyed by `:vis/tool-name`; `:vis/fold-count` records successful fold
operations independently of stdout. `usage-tally` thaws any one
row's blob at most ONCE per process and memoises the merged result per
session — a re-read costs two skinny id-only queries, a repeat read of an
unchanged session costs nothing beyond them, and a live session pays only for
the iterations added since the last read. It is still an ON-DEMAND
single-session read and never part of `list-sessions`.
Only the TOTALS survive the blob pass: per-tool and per-error rankings had no
reader left on any channel, so they are not computed.(db-set-session-favorite! db-info session-id is-favorite)Star (true) or unstar (false) the soul behind session-id. Returns the
rank the session now holds - a long while starred, nil once it is not.
The star is a RANK, not a boolean, because the clients pin starred sessions to the top of the list and that band needs a TOTAL order: two stars sharing a wall-clock millisecond would tie, and a tie is exactly how a deterministic order dies. The rank is allocated MAX+1 INSIDE the write transaction, so two devices starring at the same moment still get two different ranks. Ranks are only ever compared, so the gaps unstarring leaves behind cost nothing.
Starring an ALREADY starred soul keeps its rank: the star is a state the human sets, not an event, so a retried request or a second tap racing the first must not reshuffle a band nobody touched.
Star (`true`) or unstar (`false`) the soul behind `session-id`. Returns the rank the session now holds - a long while starred, nil once it is not. The star is a RANK, not a boolean, because the clients pin starred sessions to the top of the list and that band needs a TOTAL order: two stars sharing a wall-clock millisecond would tie, and a tie is exactly how a deterministic order dies. The rank is allocated MAX+1 INSIDE the write transaction, so two devices starring at the same moment still get two different ranks. Ranks are only ever compared, so the gaps unstarring leaves behind cost nothing. Starring an ALREADY starred soul keeps its rank: the star is a state the human sets, not an event, so a retried request or a second tap racing the first must not reshuffle a band nobody touched.
(db-set-session-model-pref! db-info session-id provider model)Persist (or clear, with both nil/blank) the PROVIDER + MODEL preference for a session (soul id). Lives on the stable soul, so it survives turn forks/retries.
Persist (or clear, with both nil/blank) the PROVIDER + MODEL preference for a session (soul id). Lives on the stable soul, so it survives turn forks/retries.
(db-set-session-project! db-info session-id project-id)Assign the soul behind session-id to project-id; a nil project-id clears
membership (removes it from its project AND resets its now-meaningless
project_position to 0, so no stale ordinal is left behind). Moving INTO a
project the soul does NOT already belong to APPENDS it (its project_position
becomes max+1 within that project) so project sessions stay MOVABLE;
re-assigning a soul ALREADY in the project is idempotent and keeps its current
position. Returns the soul id.
Assign the soul behind `session-id` to `project-id`; a nil `project-id` clears membership (removes it from its project AND resets its now-meaningless `project_position` to 0, so no stale ordinal is left behind). Moving INTO a project the soul does NOT already belong to APPENDS it (its `project_position` becomes max+1 within that project) so project sessions stay MOVABLE; re-assigning a soul ALREADY in the project is idempotent and keeps its current position. Returns the soul id.
(db-set-session-prompt-cache-state! db-info session-state-id state)Overwrite one session state's exact provider-prefix checkpoint. Nil clears it.
Overwrite one session state's exact provider-prefix checkpoint. Nil clears it.
(db-set-turn-attachment-transcription! db-info
session-turn-soul-id
position
transcription)Write the words a recording turned out to hold onto the row that already holds
the recording, addressed by (turn soul, position) on the user rail
(session_turn_iteration_id IS NULL).
The transcript lands LATER than the row it belongs to: staging a recording only STARTS the words, and the turn is written down without waiting for an hour of speech. This is how they still reach the row - so a resumed session, the history re-render and every player have them, instead of a column that stays NULL forever because the pass that made the words ended.
position is the attachment's index in the vector db-store-session-turn! was
handed, which is exactly the position it stored. Blank words are refused rather
than written, because an empty string reads as a recording nobody found speech in.
Returns true when a row took the words.
Write the words a recording turned out to hold onto the row that already holds the recording, addressed by `(turn soul, position)` on the user rail (`session_turn_iteration_id IS NULL`). The transcript lands LATER than the row it belongs to: staging a recording only STARTS the words, and the turn is written down without waiting for an hour of speech. This is how they still reach the row - so a resumed session, the history re-render and every player have them, instead of a column that stays NULL forever because the pass that made the words ended. `position` is the attachment's index in the vector [[db-store-session-turn!]] was handed, which is exactly the `position` it stored. Blank words are refused rather than written, because an empty string reads as a recording nobody found speech in. Returns true when a row took the words.
(db-store-iteration! db-info
{:keys [session-turn-id thinking assistant-prose answer
llm-full-duration-ms error llm-routing
cache-created-tokens llm-returned-empty-code?
tokens cost-usd attachments]
:as opts})Store one iteration row in a single SQLite transaction.
The executed tool-call records travel on the iteration row's
tool_calls BLOB; there is no sandbox-var sidecar persistence.
Cross-turn references go through :session/facts and introspect-iter /
introspect-turn (DB reads against session_turn_iteration.tool_calls).
Returns the iteration UUID.
Store one iteration row in a single SQLite transaction. The executed tool-call records travel on the iteration row's `tool_calls` BLOB; there is no sandbox-var sidecar persistence. Cross-turn references go through `:session/facts` and introspect-iter / introspect-turn (DB reads against `session_turn_iteration.tool_calls`). Returns the iteration UUID.
(db-store-session! db-info
{:keys [channel external-id title system-prompt provider
model workspace-id parent-state-id claimed? owner-id]
:or {claimed? true}})Create session_soul + initial session_state (version 0). Returns the session-soul UUID.
Required opt: :workspace-id — session_state.workspace_id is NOT NULL
after V1, enforcing the 1:1 invariant. The caller
(loop/create-environment) calls workspace/ensure-trunk! to mint a
trunk workspace before invoking this fn.
Session identity and LLM root defaults are first-class columns: session_soul.channel / external_id session_state.system_prompt / llm_root_provider / llm_root_model session_state.title.
Create session_soul + initial session_state (version 0). Returns the session-soul UUID. Required opt: `:workspace-id` — session_state.workspace_id is NOT NULL after V1, enforcing the 1:1 invariant. The caller (loop/create-environment) calls `workspace/ensure-trunk!` to mint a trunk workspace before invoking this fn. Session identity and LLM root defaults are first-class columns: session_soul.channel / external_id session_state.system_prompt / llm_root_provider / llm_root_model session_state.title.
(db-store-session-turn! db-info
{:keys [parent-session-id session-turn-id user-request
status attachments request-kind
council-entry-id]})Create session_turn_soul + initial session_turn_state (version 0).
:attachments - EVERY validated image attached to the turn [{:media-type :base64 :size :filename}], both INLINE (web/API) uploads and terminal-drop
disk images - persist as session_attachment BLOB rows (user rail: NULL
iteration) so resume + history re-render survive a restart even after the source file moves or is
deleted.
:request-kind is explicit provenance (:user by default or :council).
Council turns require :council-entry-id: the immutable entry owns the actual
request content, author and kind. The user-request remains the model instruction.
Returns the session-turn-soul UUID.
Create session_turn_soul + initial session_turn_state (version 0).
`:attachments` - EVERY validated image attached to the turn `[{:media-type
:base64 :size :filename}]`, both INLINE (web/API) uploads and terminal-drop
disk images - persist as `session_attachment` BLOB rows (user rail: NULL
iteration) so resume + history re-render survive a restart even after the source file moves or is
deleted.
`:request-kind` is explicit provenance (`:user` by default or `:council`).
Council turns require `:council-entry-id`: the immutable entry owns the actual
request content, author and kind. The user-request remains the model instruction.
Returns the session-turn-soul UUID.(db-store-stale? store db-spec)True when a persistent SQLite store no longer matches the requested db-spec or the file at the same path was REPLACED under this JVM. When true, the facade closes the old shared pool and opens a new one, so this predicate must never fire on a store that is merely being written to - the close lands on connections other threads are querying.
Paths compare CANONICALIZED on both sides; the file compares by
filesystem identity only (see stable-db-file-key).
True when a persistent SQLite store no longer matches the requested db-spec or the file at the same path was REPLACED under this JVM. When true, the facade closes the old shared pool and opens a new one, so this predicate must never fire on a store that is merely being written to - the close lands on connections other threads are querying. Paths compare CANONICALIZED on both sides; the file compares by filesystem identity only (see `stable-db-file-key`).
(db-turn-history db-info session-id)Per-turn history rows with canonical typed :content.
Per-turn history rows with canonical typed `:content`.
(db-update-project! db-info
project-id
{:keys [name color position archived? workspace-root]
:as opts})Patch a project: any of :name (non-blank), :color, :position,
:archived? (true stamps archived_at=now, false clears it). Returns the
updated project (canonical shape) or nil when nothing to change.
Patch a `project`: any of `:name` (non-blank), `:color`, `:position`, `:archived?` (true stamps `archived_at`=now, false clears it). Returns the updated project (canonical shape) or nil when nothing to change.
(db-update-session-turn! db-info
session-turn-id
{:keys [content iteration-count duration-ms status
tokens cost prior-outcome ctx error]
:as opts})Settle the latest running session_turn_state exactly once.
Terminal writes compare-and-set from running; a late worker cannot replace the
outcome already chosen by a watchdog, cancellation backstop, or another terminal
path. Only keys present in opts are changed, so a sparse forced terminal does
not erase counters or metadata already on the row. Returns true when this call
settled the row, false when the row is absent or already terminal.
When :prior-outcome is provided (one of :complete, :cancelled,
:error), it lands in the dedicated prior_outcome column so the next turn's handover digest can read
it without scanning every iteration. The column is bounded by a
CHECK constraint at the schema level.
Settle the latest running session_turn_state exactly once. Terminal writes compare-and-set from `running`; a late worker cannot replace the outcome already chosen by a watchdog, cancellation backstop, or another terminal path. Only keys present in `opts` are changed, so a sparse forced terminal does not erase counters or metadata already on the row. Returns true when this call settled the row, false when the row is absent or already terminal. When `:prior-outcome` is provided (one of `:complete`, `:cancelled`, `:error`), it lands in the dedicated `prior_outcome` column so the next turn's handover digest can read it without scanning every iteration. The column is bounded by a CHECK constraint at the schema level.
(db-workspace-for-session db-info session-state-id)Return the workspace pinned to session-state-id, or nil. Always
non-nil after step-4 wiring (1:1 invariant); nil only during the
transitional window where session_state may not yet have a workspace.
Return the workspace pinned to `session-state-id`, or nil. Always non-nil after step-4 wiring (1:1 invariant); nil only during the transitional window where session_state may not yet have a workspace.
(db-workspace-insert! db-info
{:keys [id repo-id repo-root root label fork-ms
apply-fork-ms workspace-kind workspace-backend
workspace-mechanism parent-workspace-id state
filesystem-roots]})Insert a workspace row. Returns the inserted record (canonical shape).
Required: :repo-id :repo-root :root Optional: :id (defaults to a new UUID), :label, :fork-ms, :apply-fork-ms, :workspace-kind, :workspace-backend, :workspace-mechanism (how the backend made the clone), :parent-workspace-id, :state (defaults to :active), :filesystem-roots (extra per-root draft clones, persisted as a JSON array)
Insert a workspace row. Returns the inserted record (canonical shape).
Required: :repo-id :repo-root :root
Optional: :id (defaults to a new UUID), :label, :fork-ms, :apply-fork-ms,
:workspace-kind, :workspace-backend, :workspace-mechanism (how the
backend made the clone), :parent-workspace-id, :state
(defaults to :active), :filesystem-roots (extra per-root draft
clones, persisted as a JSON array)(db-workspace-list-by-repo db-info repo-id)(db-workspace-list-by-repo db-info repo-id state-set)List workspaces in repo-id, optionally filtered to a state-set of
keywords (e.g. #{:active :merging}). Newest first.
List workspaces in `repo-id`, optionally filtered to a `state-set` of
keywords (e.g. #{:active :merging}). Newest first.(db-workspace-list-drafts db-info)Every draft workspace across all repos, newest first. Housekeeping reads
this to decide which draft clones on disk have gone stale; it is
deliberately repo-agnostic because the drafts store is a single
~/.vis/drafts tree shared by every trunk.
Every draft workspace across all repos, newest first. Housekeeping reads this to decide which draft clones on disk have gone stale; it is deliberately repo-agnostic because the drafts store is a single `~/.vis/drafts` tree shared by every trunk.
(db-workspace-touch-focus! db-info workspace-id)Stamp last_focused_at_ms to now-ms on the workspace row. Called by
workspace/focus!. Returns the updated record.
Stamp `last_focused_at_ms` to now-ms on the workspace row. Called by `workspace/focus!`. Returns the updated record.
(db-workspace-update-label! db-info workspace-id label)Set the human-friendly :label override. Pass nil to clear the
label and fall back to the heuristic. Returns the updated record.
Set the human-friendly `:label` override. Pass nil to clear the label and fall back to the heuristic. Returns the updated record.
(db-workspace-update-state! db-info workspace-id new-state)Transition workspace-id to new-state (:active | :discarded).
Stamps discarded_at on :discarded. Returns the updated record.
Transition `workspace-id` to `new-state` (`:active` | `:discarded`). Stamps `discarded_at` on :discarded. Returns the updated record.
(query! db-info q)Run a HoneySQL map and return rows with unqualified lower-case keys.
Run a HoneySQL map and return rows with unqualified lower-case keys.
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 |