Liking cljdoc? Tell your friends :D

com.blockether.vis.internal.foundation.doctor

Foundation's contribution to the host vis-agent doctor aggregator. ONE fn (doctor-fn) returns the full message stream from two logical sections, each stamping its own :check-id so the formatter groups them under the same banner the original four-checks-vec shape produced (plan §1 Q18 / §10):

::agents-md AGENTS.md presence / source / size; one :info line when found, one :warn line when neither AGENTS.md nor CLAUDE.md exists (rules silently absent is worth flagging even though it isn't an error per se).

::provider-env providers whose ${NAME} config references point at environment variables that are not set. Config loads leniently by design, so vis-agent doctor is the moment a user actually LOOKS — the right place to fail fast without failing the gateway.

::image-render one real SVG rasterized through the attachment path, so a build whose imaging cdylib is missing or unloadable SAYS SO instead of silently dropping every diagram.

These section fns are pure data -> message-seq; they don't mutate anything and don't depend on the runtime environment beyond what's needed to read the existing scanners. Activation contract per plan: every registered extension's :ext/doctor-fn runs regardless of :ext/activation-fn, so the section fns must NOT assume :db-info or other env keys are present.

Foundation's contribution to the host `vis-agent doctor` aggregator. ONE fn
(`doctor-fn`) returns the full message stream from two logical
sections, each stamping its own `:check-id` so the formatter
groups them under the same banner the original four-checks-vec
shape produced (plan §1 Q18 / §10):

  ::agents-md         AGENTS.md presence / source / size; one
                       :info line when found, one :warn line
                       when neither AGENTS.md nor CLAUDE.md exists
                       (rules silently absent is worth flagging
                       even though it isn't an error per se).

  ::provider-env      providers whose `${NAME}` config references point at
                       environment variables that are not set. Config loads
                       leniently by design, so `vis-agent doctor` is the moment a
                       user actually LOOKS — the right place to fail fast
                        without failing the gateway.

::image-render      one real SVG rasterized through the attachment path, so
                     a build whose imaging cdylib is missing or unloadable
                     SAYS SO instead of silently dropping every diagram.

These section fns are pure data -> message-seq; they don't mutate
anything and don't depend on the runtime environment beyond
what's needed to read the existing scanners. Activation
contract per plan: every registered extension's `:ext/doctor-fn`
runs regardless of `:ext/activation-fn`, so the section fns must
NOT assume `:db-info` or other env keys are present.
raw docstring

com.blockether.vis.internal.foundation.drafts

Drafts as the model reaches them: the draft_create, draft_status, draft_approve and draft_discard sandbox symbols and the draft_backend toggle. Only the agent manages drafts — no channel offers a slash command or a picker for them. Each symbol is a thin layer over workspace.drafts, the boundary the daemon's HTTP routes use too, so an extension hook on :draft/* sees every surface alike.

Drafts as the model reaches them: the `draft_create`, `draft_status`,
`draft_approve` and `draft_discard` sandbox symbols and the `draft_backend`
toggle. Only the agent manages drafts — no channel offers a slash command
or a picker for them. Each symbol is a thin layer over `workspace.drafts`,
the boundary the daemon's HTTP routes use too, so an extension hook on
`:draft/*` sees every surface alike.
raw docstring

com.blockether.vis.internal.foundation.editing.core

Host-side file reading, search, listing and anchored editing.

cat and grep return line/hash addresses consumed by patch. One patch validates every edit and the resulting syntax before replacing one file. list-directories supplies the Python ls shim. Standard Python owns file creation, copying, moving and deletion.

Paths are confined to the session's allowed roots. Host operations also consult extension-owned :fs/access gates.

Host-side file reading, search, listing and anchored editing.

`cat` and `grep` return line/hash addresses consumed by `patch`. One patch
validates every edit and the resulting syntax before replacing one file.
`list-directories` supplies the Python `ls` shim. Standard Python owns file
creation, copying, moving and deletion.

Paths are confined to the session's allowed roots. Host operations also
consult extension-owned `:fs/access` gates.
raw docstring

com.blockether.vis.internal.foundation.editing.diff

Unified-diff rendering and line accounting for anchored patches. Independent of the tool and activity namespaces.

Unified-diff rendering and line accounting for anchored patches.
Independent of the tool and activity namespaces.
raw docstring

com.blockether.vis.internal.foundation.editing.escapes

Unicode-escape hygiene for model-authored edit TEXT.

Public surface: decode-unicode-escapes — undo the \uXXXX drift a model writes when it means the character itself, and nothing else.

Unicode-escape hygiene for model-authored edit TEXT.

Public surface: `decode-unicode-escapes` — undo the `\uXXXX` drift a model
writes when it means the character itself, and nothing else.
raw docstring

com.blockether.vis.internal.foundation.editing.hashline

Pure hashline primitives: the ANCHOR vocabulary cat mints, grep echoes and patch spends.

An anchor is <1-based line>:<3-hex content hash> (Can Bölük's original hashline shape). The LINE NUMBER locates the line; the CONTENT HASH verifies it. A write requires BOTH coordinates to match exactly: any contradiction is REFUSED (:anchor-mismatch) instead of relocating the edit. Only the non-destructive read path may follow matching content through small line drift.

This namespace is pure — no IO, no tool wiring, no extension envelope. Every surface that addresses a line routes here so the scheme is never recomputed:

split-content-lines / char-offset-at-line blob <-> line/char coordinates line-hash / line-anchor / anchor->line text -> <line>:<hash> render-hashline-block [[ln text]…] -> gutter text anchor-token / parse-anchor rendered line -> bare anchor indices-matching-hash content-only hash lookup resolve-one-anchor / resolve-anchor-range exact write resolution resolve-anchor-range-read tolerant read resolution resolve-anchor-edit-span anchor span -> char span

Pure hashline primitives: the ANCHOR vocabulary `cat` mints, `grep` echoes and
`patch` spends.

An anchor is `<1-based line>:<3-hex content hash>` (Can Bölük's original
hashline shape). The LINE NUMBER locates the line; the CONTENT HASH verifies
it. A write requires BOTH coordinates to match exactly: any contradiction is
REFUSED (`:anchor-mismatch`) instead of relocating the edit. Only the
non-destructive read path may follow matching content through small line drift.

This namespace is pure — no IO, no tool wiring, no extension envelope. Every
surface that addresses a line routes here so the scheme is never recomputed:

  split-content-lines / char-offset-at-line   blob <-> line/char coordinates
  line-hash / line-anchor / anchor->line      text  -> `<line>:<hash>`
  render-hashline-block                       [[ln text]…] -> gutter text
  anchor-token / parse-anchor                 rendered line -> bare anchor
  indices-matching-hash                       content-only hash lookup
  resolve-one-anchor / resolve-anchor-range   exact write resolution
  resolve-anchor-range-read                   tolerant read resolution
  resolve-anchor-edit-span                    anchor span -> char span
raw docstring

com.blockether.vis.internal.foundation.editing.parse

Language detection and PARSE VERDICTS for the anchored patch gate.

Two questions, answered through tree-sitter (com.blockether/tree-sitter-language-pack, which sources Clojure from our own grammar fork):

  1. what language is this file, and is it a language where a parse error means the file is genuinely broken (code-languages)?
  2. where exactly does the new content fail to parse (error-nodes)?

patch spends both: it re-parses what a write would produce and refuses an edit that introduces a syntax error the file did not already have, naming the line and the unpaired delimiter instead of a bare error count.

All native handles (Parser/Tree/Node) are opened and closed inside each call; only plain Clojure data escapes. Requiring this namespace also requires the native resolver, which selects the right per-platform FFI library at runtime.

Language detection and PARSE VERDICTS for the anchored `patch` gate.

Two questions, answered through tree-sitter (com.blockether/tree-sitter-language-pack,
which sources Clojure from our own grammar fork):

  1. what language is this file, and is it a language where a parse error
     means the file is genuinely broken (`code-languages`)?
  2. where exactly does the new content fail to parse (`error-nodes`)?

`patch` spends both: it re-parses what a write would produce and refuses an
edit that introduces a syntax error the file did not already have, naming the
line and the unpaired delimiter instead of a bare error count.

All native handles (Parser/Tree/Node) are opened and closed inside each call;
only plain Clojure data escapes. Requiring this namespace also requires the
native resolver, which selects the right per-platform FFI library at runtime.
raw docstring

com.blockether.vis.internal.foundation.environment.core

vis-foundation — the agent's environment-awareness layer.

Owns the environment facts: cwd, user, platform, shell, plus:

  • git repository facts via the git binary (root, branch, dirty status, submodules, worktree),
  • a bounded language scan over the working tree (top languages by file count, primary language),
  • monorepo / multi-package shape detection (polylith, workspace, submodules) by counting per-ecosystem manifests.

Model-facing VCS/workspace truth lives in session['workspace']; session['env']['project'] supplies project kind and primary language. Detailed scans remain host data for context and language-tool dispatch.

Runtime facts are computed lazily on first access and cached per working-directory. The cache is invalidated automatically when cwd changes between calls, and explicitly by the HOST-ONLY refresh! — which /reload runs and the sandbox cannot call.

vis-foundation — the agent's environment-awareness layer.

Owns the environment facts: cwd, user, platform, shell, plus:

  * git repository facts via the git binary (root, branch, dirty status,
    submodules, worktree),
  * a bounded language scan over the working tree (top languages
    by file count, primary language),
  * monorepo / multi-package shape detection (polylith, workspace,
    submodules) by counting per-ecosystem manifests.

Model-facing VCS/workspace truth lives in `session['workspace']`;
`session['env']['project']` supplies project kind and primary language.
Detailed scans remain host data for context and language-tool dispatch.

Runtime facts are computed lazily on first access and cached per
working-directory. The cache is invalidated automatically when
`cwd` changes between calls, and explicitly by the HOST-ONLY
`refresh!` — which `/reload` runs and the sandbox cannot call.
raw docstring

com.blockether.vis.internal.foundation.environment.git

Git introspection for the environment block, backed by the native git binary (via internal.workspace.git).

Returns a snapshot map for the repository that contains start (typically the JVM working directory). nil when start is not inside any git repository. Never throws — every git call is guarded; on any failure we degrade gracefully to nil or a reduced-shape map.

The expensive call is git status (working-tree walk). When it fails or is suppressed, the snapshot drops the dirty-status fields instead of stalling the system-prompt build.

Git introspection for the environment block, backed by the native `git`
binary (via `internal.workspace.git`).

Returns a snapshot map for the repository that contains `start`
(typically the JVM working directory). nil when `start` is not inside any
git repository. Never throws — every git call is guarded; on any failure we
degrade gracefully to nil or a reduced-shape map.

The expensive call is `git status` (working-tree walk). When it fails or is
suppressed, the snapshot drops the dirty-status fields instead of stalling
the system-prompt build.
raw docstring

com.blockether.vis.internal.foundation.environment.host

Host-side facts read from JDK system properties and process environment variables.

No I/O, no shell-out, no third-party deps. Cheap to compute, safe to call from any thread, never throws.

Host-side facts read from JDK system properties and process
environment variables.

No I/O, no shell-out, no third-party deps. Cheap to compute, safe
to call from any thread, never throws.
raw docstring

com.blockether.vis.internal.foundation.environment.languages

Bounded language scan over a directory tree.

Walks the tree with Files/walkFileTree, skipping common non-source subdirectories (.git, node_modules, target, ...) via FileVisitResult/SKIP_SUBTREE. Counts files and bytes per language using a small extension-to-language map.

The walk has TWO hard guards: a max-file count and a wall-time deadline. Either one stops the walk via TERMINATE. Callers get a possibly-partial result; on a small repo the result is exact.

No third-party deps. Reflection-clean.

Bounded language scan over a directory tree.

Walks the tree with `Files/walkFileTree`, skipping common
non-source subdirectories (`.git`, `node_modules`, `target`, ...)
via `FileVisitResult/SKIP_SUBTREE`. Counts files and bytes per
language using a small extension-to-language map.

The walk has TWO hard guards: a max-file count and a wall-time
deadline. Either one stops the walk via `TERMINATE`. Callers get
a possibly-partial result; on a small repo the result is exact.

No third-party deps. Reflection-clean.
raw docstring

com.blockether.vis.internal.foundation.environment.monorepo

Monorepo / multi-package detection.

Walks the tree once (bounded), counting per-language manifest files at any depth below the root. >=2 manifests of the same kind in distinct subdirectories signals a multi-package workspace; we report the kind, the count, and a best-guess shape label (e.g. "polylith", "workspace", "submodules").

Reflection-clean. Honors the same skip-directory list as the language scanner.

Monorepo / multi-package detection.

Walks the tree once (bounded), counting per-language manifest
files at any depth below the root. >=2 manifests of the same
kind in distinct subdirectories signals a multi-package
workspace; we report the kind, the count, and a best-guess
shape label (e.g. "polylith", "workspace", "submodules").

Reflection-clean. Honors the same skip-directory list as the
language scanner.
raw docstring

com.blockether.vis.internal.foundation.environment.render

Build compact foundation environment data for ctx. No prompt labels.

Build compact foundation environment data for `ctx`. No prompt labels.
raw docstring

com.blockether.vis.internal.foundation.environment.repositories

Bounded discovery of multiple Git repositories below the current project root. This catches multirepo workspaces where the user's cwd is a parent directory or a primary repo that vendors sibling/nested repos outside .gitmodules.

Returns compact per-repo Git summaries for the system prompt. Full status walks are bounded per repo by git/snapshot; the repository scan itself is bounded by max files, max repos, and a wall-clock deadline. Never throws.

Bounded discovery of multiple Git repositories below the current
project root. This catches multirepo workspaces where the user's cwd
is a parent directory or a primary repo that vendors sibling/nested
repos outside `.gitmodules`.

Returns compact per-repo Git summaries for the system prompt. Full
status walks are bounded per repo by `git/snapshot`; the repository
scan itself is bounded by max files, max repos, and a wall-clock
deadline. Never throws.
raw docstring

com.blockether.vis.internal.foundation.gif

Multi-frame GIF (87a/89a) codec, delegating every byte of the GIF format to the com.blockether/imaging native cdylib (Rust image crate): LZW, palette quantization, interlace, disposal and NETSCAPE looping -- all of it lives there now.

This used to be a hand-rolled pure-Clojure codec (its own LZW decode/encode, sub-block framing, disposal compositor, median-cut-free palette path). That could not survive the image crate's decode-limit changes and duplicated work the cdylib already does; the native path composites every frame onto a full-size canvas honouring GIF disposal (the image crate's own GifFrameIterator), so a caller only ever sees plain packed-0xAARRGGBB pixels -- exactly the shape the old decode produced.

Multi-frame GIF (87a/89a) codec, delegating every byte of the GIF format to
the `com.blockether/imaging` native cdylib (Rust `image` crate): LZW, palette
quantization, interlace, disposal and NETSCAPE looping -- all of it lives there
now.

This used to be a hand-rolled pure-Clojure codec (its own LZW decode/encode,
sub-block framing, disposal compositor, median-cut-free palette path). That
could not survive the `image` crate's decode-limit changes and duplicated work
the cdylib already does; the native path composites every frame onto a
full-size canvas honouring GIF disposal (the `image` crate's own
`GifFrameIterator`), so a caller only ever sees plain packed-0xAARRGGBB
pixels -- exactly the shape the old [[decode]] produced.
raw docstring

com.blockether.vis.internal.foundation.harness.core

harness compatibility layer — a BUILT-IN foundation module (ships in the main jar, always present, gated by toggles) that exposes the SKILLS vis' own project dir and other AI coding HARNESSES (Claude Code, pi, opencode, the agents standard, …) leave on disk to the vis model. The sibling of the shell layer's POSIX compat. Vis reads its OWN project-local skills from .vis/skills (highest precedence).

  • SKILLS are DOCUMENTS, never a verb: the prompt lists every skill name — description (cheap — always present) and the WHOLE SKILL.md is one document in the doc/apropos corpus. apropos(pattern) filters skill names and doc(name) prints one whole. Reading a skill has no session effect: there is nothing to activate, nothing to re-read and no activation receipt.

  • The USER's /skill:<name> slash is that same document with a POINTER, never a copy: it expands to one sentence naming the skill (plus the owning project and any bundled resource paths, which the body does not carry) and leaves fetching it to the model, which is the only party that knows whether the text is still in front of it. No injected body means nothing to remember between two /skill:<name>s: every skill surface is stateless.

Skills and commands have no user toggle; the layer is always active.

`harness` compatibility layer — a BUILT-IN foundation module (ships in the
main jar, always present, gated by toggles) that exposes the SKILLS vis'
own project dir and other AI coding HARNESSES (Claude Code, pi, opencode, the agents
standard, …) leave on disk to the vis model. The sibling of the shell
layer's POSIX compat. Vis reads its OWN project-local skills from
`.vis/skills` (highest precedence).

- SKILLS are DOCUMENTS, never a verb: the prompt lists every skill
  `name — description` (cheap — always present) and the WHOLE `SKILL.md` is
  one document in the `doc`/`apropos` corpus. `apropos(pattern)` filters skill
  names and `doc(name)` prints one whole. Reading a skill has no session effect:
  there is nothing to activate, nothing to re-read and no activation receipt.

- The USER's `/skill:<name>` slash is that same document with a POINTER, never a
  copy: it expands to one sentence naming the skill (plus the owning project
  and any bundled resource paths, which the body does not carry) and leaves
  fetching it to the model, which is the only party that knows whether the
  text is still in front of it. No injected body means nothing to remember
  between two `/skill:<name>`s: every skill surface is stateless.

Skills and commands have no user toggle; the layer is always active.
raw docstring

com.blockether.vis.internal.foundation.harness.discovery

Cross-HARNESS discovery of agents + skills — the sibling of the shell layer's POSIX compat, for the agent/skill definitions vis' OWN project dir and OTHER AI coding harnesses (Claude Code, pi, opencode, the agents standard, …) leave on disk.

An AGENT is a markdown file with YAML-ish --- frontmatter (name, description, model, tools) + a body that IS a system prompt. A SKILL is a SKILL.md (same frontmatter, name+description) in its own directory, alongside bundled resource files.

Discovery is PURE except for the directory scan: parse-frontmatter, parse-agent, parse-skill-meta, and dedup-by-name take strings and are unit-tested without the filesystem; the discover-* fns walk the known source roots. Precedence is source ORDER, first-name-wins (vis project-local > other harnesses' project > user > plugin; Vis and Claude before pi/agents/opencode).

Cross-HARNESS discovery of agents + skills — the sibling of the shell
layer's POSIX compat, for the agent/skill definitions vis' OWN project dir
and OTHER AI coding harnesses (Claude Code, pi, opencode, the agents
standard, …) leave on disk.

An AGENT is a markdown file with YAML-ish `---` frontmatter
(`name`, `description`, `model`, `tools`) + a body that IS a system
prompt. A SKILL is a `SKILL.md` (same frontmatter, name+description) in
its own directory, alongside bundled resource files.

Discovery is PURE except for the directory scan: `parse-frontmatter`,
`parse-agent`, `parse-skill-meta`, and `dedup-by-name` take strings and
are unit-tested without the filesystem; the `discover-*` fns walk the
known source roots. Precedence is source ORDER, first-name-wins
(vis project-local > other harnesses' project > user > plugin; Vis and
Claude before pi/agents/opencode).
raw docstring

com.blockether.vis.internal.foundation.housekeeping

Retention for the Vis-owned directories that grow without bound — the one nobody may delete for you, and the eight that delete themselves.

ADVISORY (scan observes, purge! acts, vis-agent doctor renders): the drafts store (~/.vis/drafts). A draft clone is a full copy of a trunk and survives until someone applies or abandons it, so a machine that drafts daily and never abandons accumulates gigabytes of dead clones. It holds recoverable work, so nothing here deletes it on its own: scan is pure observation (no mutation, never throws) and purge! is the explicit operator action behind vis-agent doctor --purge. scan reports the gateway journals the same way, because an operator asking what is reclaimable today should see them.

SELF-DELETING (sweep-stale!, once per process at startup): diagnostic logs, the gateway journals, the display caches, the rewind stores and the embedded Python runtimes of versions this binary no longer pins. Those are DERIVED — a log of a process that exited, the wire replay of a turn the DB already owns, a picture whose bytes are already DB-owned, the pre-image of an edit nobody will rewind a fortnight later, an interpreter the next start refetches from its release — so they carry a window instead of a report. sweep-targets is the one list of them. Journals also self-sweep inside the tailer loop (gateway.bus/sweep!) after a single idle day, but that is a LIVENESS rule and it only runs while a daemon does — journals from crashed or never-restarted daemons used to stay forever, and startup is exactly when no daemon is running.

purge! routes deletions through workspace/abandon! for live draft rows so the DB transition, hooks, and backend root release all use the canonical engine path. Only rows already :discarded, directories with no row at all, and journal files are removed directly — and every direct delete is confined to a path under the drafts store or the events dir.

Retention for the Vis-owned directories that grow without bound — the one
nobody may delete for you, and the eight that delete themselves.

ADVISORY (`scan` observes, `purge!` acts, `vis-agent doctor` renders): the
drafts store (`~/.vis/drafts`). A draft clone is a full copy of a trunk and
survives until someone applies or abandons it, so a machine that drafts daily
and never abandons accumulates gigabytes of dead clones. It holds recoverable
work, so nothing here deletes it on its own: `scan` is pure observation (no
mutation, never throws) and `purge!` is the explicit operator action behind
`vis-agent doctor --purge`. `scan` reports the gateway journals the same way,
because an operator asking what is reclaimable today should see them.

SELF-DELETING (`sweep-stale!`, once per process at startup): diagnostic logs,
the gateway journals, the display caches, the rewind stores and the embedded
Python runtimes of versions this binary no longer pins. Those are DERIVED — a
log of a process that exited, the wire replay of a turn the DB already owns,
a picture whose bytes are already DB-owned, the pre-image of an edit nobody
will rewind a fortnight later, an interpreter the next start refetches from
its release — so they carry a window instead of
a report. `sweep-targets` is the one list of them. Journals also self-sweep
inside the tailer loop (`gateway.bus/sweep!`) after a single idle day, but
that is a LIVENESS rule and it only runs while a daemon does — journals from
crashed or never-restarted daemons used to stay forever, and startup is
exactly when no daemon is running.

`purge!` routes deletions through `workspace/abandon!` for live draft rows so
the DB transition, hooks, and backend root release all use the canonical engine
path. Only rows already `:discarded`, directories with
no row at all, and journal files are removed directly — and every direct
delete is confined to a path under the drafts store or the events dir.
raw docstring

com.blockether.vis.internal.foundation.introspection

Programmatic introspection of the agent's own state from inside :code. The public state surface is deliberately small:

  • (read-session [target]) -> canonical data map, including usage and raw LLM diagnostics
  • (get-session [target]) -> ONE session's descriptor, no transcript
  • (list-sessions [search]) -> metadata-only index, RANKED when search is given

Everything else in this namespace is implementation detail. The agent gets the data once and manipulates it with ordinary Python collection operations when filtering or presentation is needed.

Every function is a pure read off the same DB tables the projection layer reads from (or a classpath read for the doc accessors). Failures return nil/[], never throw, so a misbehaving introspection call cannot break iteration execution.

Gated: the extension registered at the bottom of this namespace binds its symbols and prompt only while the introspection toggle is ON (default OFF).

Programmatic introspection of the agent's own state from inside
`:code`. The public state surface is deliberately small:

- `(read-session [target])` -> canonical data map, including usage and raw LLM diagnostics
- `(get-session [target])` -> ONE session's descriptor, no transcript
- `(list-sessions [search])` -> metadata-only index, RANKED when `search` is given

Everything else in this namespace is implementation detail. The agent
gets the data once and manipulates it with ordinary Python collection
operations when filtering or presentation is needed.

Every function is a pure read off the same DB tables the projection
layer reads from (or a classpath read for the doc accessors).
Failures return nil/[], never throw, so a misbehaving introspection
call cannot break iteration execution.

Gated: the extension registered at the bottom of this namespace binds its
symbols and prompt only while the `introspection` toggle is ON (default OFF).
raw docstring

com.blockether.vis.internal.foundation.language-surface

Language-neutral FORMAT / TEST / REPL_EVAL / REPL-LIFECYCLE dispatch.

Language extensions register handlers under :ext/language-tools; this foundation surface exposes stable bare tool names and dispatches to the active handler for the requested/current language. REPL lifecycle is resource backed: repl_start creates a language-owned session resource, repl_status reports it and repl_stop ends one. Live REPLs also surface in the ctx resources block.

Language-neutral FORMAT / TEST / REPL_EVAL / REPL-LIFECYCLE dispatch.

Language extensions register handlers under `:ext/language-tools`; this
foundation surface exposes stable bare tool names and dispatches to the
active handler for the requested/current language. REPL lifecycle is resource
backed: `repl_start` creates a language-owned session resource, `repl_status`
reports it and `repl_stop` ends one. Live REPLs also surface in the ctx
`resources` block.
raw docstring

com.blockether.vis.internal.foundation.mcp.client

Minimal Model Context Protocol (MCP) client. Speaks JSON-RPC 2.0 over two transports:

:stdio — spawn the server process and frame newline-delimited JSON-RPC on its stdin/stdout (the dominant local-server pattern). A daemon thread drains stderr into the vis log so a chatty server never deadlocks on a full pipe.

:http — Streamable HTTP: POST each JSON-RPC message to one endpoint; the reply is either application/json (one response) or text/event-stream (SSE) — both handled. The Mcp-Session-Id handed back by initialize rides on every later request, a DELETE frees it on shutdown, and an optional GET listen loop reacts to server-pushed notifications/tools/list_changed.

OAuth 2.1 (spec 2025-06-18) is supported for HTTP transports via oauth.clj: pass :bearer-fn (a 0/1-arg fn yielding the current Bearer token, called with the just-rejected token on 401). A 401 triggers a single-flight refresh and one automatic retry.

A conn is a plain map of closures + state; the extension treats it opaquely. Lifecycle: connect (which performs the initialize handshake) → list-tools / call-toolclose.

Minimal Model Context Protocol (MCP) client. Speaks JSON-RPC 2.0 over two
transports:

  :stdio  — spawn the server process and frame newline-delimited JSON-RPC on
            its stdin/stdout (the dominant local-server pattern). A daemon
            thread drains stderr into the vis log so a chatty server never
            deadlocks on a full pipe.

  :http   — Streamable HTTP: POST each JSON-RPC message to one endpoint; the
            reply is either `application/json` (one response) or
            `text/event-stream` (SSE) — both handled. The `Mcp-Session-Id`
            handed back by `initialize` rides on every later request, a
            `DELETE` frees it on shutdown, and an optional GET listen loop
            reacts to server-pushed `notifications/tools/list_changed`.

OAuth 2.1 (spec `2025-06-18`) is supported for HTTP transports via
`oauth.clj`: pass `:bearer-fn` (a 0/1-arg fn yielding the current Bearer
token, called with the just-rejected token on 401). A 401 triggers a
single-flight refresh and one automatic retry.

A `conn` is a plain map of closures + state; the extension treats it
opaquely. Lifecycle: `connect` (which performs the `initialize` handshake) →
`list-tools` / `call-tool` → `close`.
raw docstring

com.blockether.vis.internal.foundation.mcp.core

Built-in Model Context Protocol (MCP) surface. The gateway daemon owns ONE shared pool of MCP connections ({server {:conn spec}}); every session sees the same live tools. Always on: MCP is core infrastructure, not a droppable plug-in and not gated by any toggle. The pool is empty (and costs nothing) until at least one server is declared in config.

Servers are declared natively in ~/.vis/state.yml:

{:mcp {:servers {"filesystem" {:transport :stdio :command "npx" :args ["-y" "@modelcontextprotocol/server-filesystem" "/path"]} "remote" {:transport :streamable-http :url "https://.../mcp" :headers {"Authorization" "Bearer ${MY_TOKEN}"} :timeout_ms 60000} "stale" {:enabled false :url "https://.../mcp"}}}}

Every string in :headers / :env / :args / :url / :command / :cwd supports ${ENV_VAR} interpolation from the host environment. :enabled false skips the server without deleting the entry. HTTP servers with no static bearer transparently negotiate OAuth 2.1 on first 401 (RFC 9728 discovery + RFC 7591 dynamic client registration + PKCE loopback).

ONE model-facing verb under alias mcp (flat sandbox renders alias_name): mcp__call(server, tool, args) - call a tool mcp__call(server) - that server's descriptions + input schemas

There is deliberately NO connect/disconnect verb. The daemon connects every enabled server, health-checks the pool on its own clock, and reaps/respawns a dead one; a tool call self-heals its connection too. Starting or stopping a server is a human admin action on the gateway API (save/enable/kill/start), never something one session does to a resource every other session shares.

Every visible server - its status and the NAMES of the tools it exposes - rides in ctx under env.mcp, keyed by server name so a change diffs per server. That IS the inventory: no listing verb spends a turn re-fetching what the session object already carries.

Built-in Model Context Protocol (MCP) surface. The gateway daemon owns ONE
shared pool of MCP connections (`{server {:conn spec}}`); every session sees
the same live tools. Always on: MCP is core infrastructure, not a droppable
plug-in and not gated by any toggle. The pool is empty (and costs nothing)
until at least one server is declared in config.

Servers are declared natively in `~/.vis/state.yml`:

  {:mcp {:servers {"filesystem" {:transport :stdio :command "npx"
                                 :args ["-y" "@modelcontextprotocol/server-filesystem" "/path"]}
                   "remote"     {:transport :streamable-http :url "https://.../mcp"
                                 :headers {"Authorization" "Bearer ${MY_TOKEN}"}
                                 :timeout_ms 60000}
                   "stale"      {:enabled false :url "https://.../mcp"}}}}

Every string in `:headers` / `:env` / `:args` / `:url` / `:command` / `:cwd`
supports `${ENV_VAR}` interpolation from the host environment. `:enabled
false` skips the server without deleting the entry. HTTP servers with no
static bearer transparently negotiate OAuth 2.1 on first 401 (RFC 9728
discovery + RFC 7591 dynamic client registration + PKCE loopback).

ONE model-facing verb under alias `mcp` (flat sandbox renders `alias_name`):
  mcp__call(server, tool, args) - call a tool
  mcp__call(server)             - that server's descriptions + input schemas

There is deliberately NO connect/disconnect verb. The daemon connects every
enabled server, health-checks the pool on its own clock, and reaps/respawns a
dead one; a tool call self-heals its connection too. Starting or stopping a
server is a human admin action on the gateway API (save/enable/kill/start),
never something one session does to a resource every other session shares.

Every visible server - its status and the NAMES of the tools it exposes -
rides in ctx under `env.mcp`, keyed by server name so a change diffs per
server. That IS the inventory: no listing verb spends a turn re-fetching what
the session object already carries.
raw docstring

com.blockether.vis.internal.foundation.mcp.http

One lazy babashka.http-client instance shared by every MCP HTTP subsystem.

One lazy babashka.http-client instance shared by every MCP HTTP subsystem.
raw docstring

com.blockether.vis.internal.foundation.mcp.oauth

OAuth 2.1 client for Model Context Protocol servers (spec 2025-06-18).

Flow — on HTTP 401 from an MCP server, we:

  1. read WWW-Authenticate: Bearer resource_metadata="..." (RFC 9728), fall back to ${origin}/.well-known/oauth-protected-resource;
  2. GET the resource-metadata JSON → pick an authorization_servers[0];
  3. GET its .well-known/oauth-authorization-server (RFC 8414) or .well-known/openid-configuration for endpoints + capabilities;
  4. dynamic-client-register (RFC 7591) if the AS supports it, or use the caller-supplied client_id;
  5. prepare an allowed callback and register it, then hand the adapter to provider.flow, the SAME lifecycle used by model-provider authentication. The initiating client opens the browser and returns directly to its gateway;
  6. exchange code → access + refresh tokens; persist to ~/.vis/mcp-tokens/<server>.edn;
  7. on later expiry / 401, refresh the token single-flight through com.blockether.vis.internal.provider.oauth/make-file-refresher.

The returned bearer-fn is a 0/1-arg function: 0-arg yields the current bearer token; 1-arg (with the token the server just rejected) forces a refresh. It never opens a browser and never waits — with nothing to refresh it throws :mcp/oauth-required, which callers turn into sign in.

OAuth 2.1 client for Model Context Protocol servers (spec `2025-06-18`).

Flow — on HTTP 401 from an MCP server, we:
  1. read `WWW-Authenticate: Bearer resource_metadata="..."` (RFC 9728),
     fall back to `${origin}/.well-known/oauth-protected-resource`;
  2. GET the resource-metadata JSON → pick an `authorization_servers[0]`;
  3. GET its `.well-known/oauth-authorization-server` (RFC 8414) or
     `.well-known/openid-configuration` for endpoints + capabilities;
  4. dynamic-client-register (RFC 7591) if the AS supports it, or use
     the caller-supplied `client_id`;
  5. prepare an allowed callback and register it, then hand the adapter to
     `provider.flow`, the SAME lifecycle used by model-provider authentication.
     The initiating client opens the browser and returns directly to its gateway;
  6. exchange code → access + refresh tokens; persist to
     `~/.vis/mcp-tokens/<server>.edn`;
  7. on later expiry / 401, refresh the token single-flight through
     `com.blockether.vis.internal.provider.oauth/make-file-refresher`.

The returned `bearer-fn` is a 0/1-arg function: 0-arg yields the current
bearer token; 1-arg (with the token the server just rejected) forces a
refresh. It never opens a browser and never waits — with nothing to refresh
it throws `:mcp/oauth-required`, which callers turn into `sign in`.
raw docstring

com.blockether.vis.internal.foundation.mpl-capture

Per-block collection of explicit attachments and rendered matplotlib figures.

Producers call record-attachment! with bytes they already hold. run-python-block binds *attachment-sink* and drains it into the block's :attachments; the loop passes those records to iteration persistence. Stdout is used for display, not attachment persistence. Ordinary filesystem writes are not collected.

This namespace does not depend on the renderer or tool namespaces.

Per-block collection of explicit attachments and rendered matplotlib figures.

Producers call `record-attachment!` with bytes they already hold.
`run-python-block` binds `*attachment-sink*` and drains it into the block's
`:attachments`; the loop passes those records to iteration persistence.
Stdout is used for display, not attachment persistence. Ordinary filesystem
writes are not collected.

This namespace does not depend on the renderer or tool namespaces.
raw docstring

com.blockether.vis.internal.foundation.pty

The PTY adapter for libvisjail.

Native descriptor ownership, terminal setup, process groups, waiting and signals live in vis-python-runtime; this namespace keeps only the handle map consumed by shell and its passthrough bridge.

The PTY adapter for `libvisjail`.

Native descriptor ownership, terminal setup, process groups, waiting and
signals live in `vis-python-runtime`; this namespace keeps only the handle
map consumed by shell and its passthrough bridge.
raw docstring

com.blockether.vis.internal.foundation.pty-bridge

Passthrough bridge on top of the libvisjail pseudo-terminal adapter.

The problem it solves: a background shell child owns a native PTY whose master descriptor is managed by Vis. That is convenient for the agent (shell send/logs), but a human cannot jump into the live terminal to finish a browser authorization or answer an interactive prompt. tmux gets that from a separate server; libvisjail does not expose such a user-facing attachment endpoint.

This namespace restores that capability WITHOUT tmux: each background PTY optionally exposes a per-shell UNIX-DOMAIN SOCKET. vis is the server (it holds the master fd); vis-agent extension shell attach <id> is a thin client the human runs in their OWN Terminal.app. On connect the server (a) tees live master output to the socket and (b) forwards the socket's bytes to the master (stdin) — a genuine bidirectional passthrough. Multiple humans can attach at once; detaching just drops the socket and leaves the child running (exactly like tmux detach).

Everything here is stdlib: java.nio.channels AF_UNIX sockets (JDK 16+, already in vis's native-image reachability metadata) on the server side, and stty for raw mode on the client side (the human's interactive shell always has it). No JNA, no new dep, native-image clean.

Passthrough bridge on top of the libvisjail pseudo-terminal adapter.

The problem it solves: a background `shell` child owns a native PTY whose master
descriptor is managed by Vis. That is convenient for the agent (shell send/logs),
but a human cannot jump into the live terminal to finish a browser authorization or
answer an interactive prompt. tmux gets that from a separate server; libvisjail does
not expose such a user-facing attachment endpoint.

This namespace restores that capability WITHOUT tmux: each background PTY
optionally exposes a per-shell UNIX-DOMAIN SOCKET. vis is the server (it holds
the master fd); `vis-agent extension shell attach <id>` is a thin client the human runs in
their OWN Terminal.app. On connect the server (a) tees live master output to the
socket and (b) forwards the socket's bytes to the master (stdin) — a genuine
bidirectional passthrough. Multiple humans can attach at once; detaching just
drops the socket and leaves the child running (exactly like `tmux detach`).

Everything here is stdlib: `java.nio.channels` AF_UNIX sockets (JDK 16+,
already in vis's native-image reachability metadata) on the server side, and
`stty` for raw mode on the client side (the human's interactive shell always
has it). No JNA, no new dep, native-image clean.
raw docstring

com.blockether.vis.internal.foundation.rewind

DURABLE file-state rewind: put the working tree back the way it was before a turn, without owning a git/branch/commit lifecycle.

Two independent coverage sources, combined:

  1. SNAPSHOT POOL — an :around op-hook on every mutating tool (patch/fs/format_code) captures each touched path's PRE-mutation state before the op runs. Content lands in a content-addressed pool (objects/aa/<sha256>), so the same bytes are stored once no matter how many turns touch them. The first capture of a path in a turn WINS — later writes in the same turn never overwrite the turn-start pre-image.

  2. GIT BASELINE — at the FIRST hooked op of a turn we record HEAD plus the full dirty set (git status --porcelain -z -uall) and snapshot the pre-image of every DIRTY file. That closes the hole every other agent's rewind leaves open: a sed -i, a formatter, a build step, any shell write. A file that was CLEAN at turn start is recoverable from git show <baseline-head>:<path>; a file that was DIRTY at turn start already has its bytes in the pool. Coverage is therefore COMPLETE for a git workspace and honestly reported as PARTIAL otherwise.

Everything is journalled as NDJSON (journal.ndjson) under ~/.vis/rewind/<session>/, so rewind survives a restart — the history is NOT process-scoped. Entries are append-only and keyed by turn; a truncated/corrupt trailing line (crash mid-append) is skipped, never fatal.

Restore semantics for turn T: every path touched in turns >= T is set back to the EARLIEST recorded pre-image at or after T. A file created inside the rewound region is deleted; a deleted file is recreated; a symlink is recreated as a symlink; a recursively deleted directory is rebuilt and any file created inside it since is pruned.

This layer owns FILES ONLY. Conversation truncation is the channel's job — points exposes the turn ids to truncate to. Because that boundary is invisible to a user typing /rewind, the slash READS the session store for each turn's context size and says out loud, in every branch, that the conversation stays.

DURABLE file-state rewind: put the working tree back the way it was before a
turn, without owning a git/branch/commit lifecycle.

Two independent coverage sources, combined:

  1. SNAPSHOT POOL — an `:around` op-hook on every mutating tool
     (`patch`/`fs`/`format_code`)
     captures each touched path's PRE-mutation state before the op runs.
     Content lands in a content-addressed pool (`objects/aa/<sha256>`), so
     the same bytes are stored once no matter how many turns touch them.
     The first capture of a path in a turn WINS — later writes in the same
     turn never overwrite the turn-start pre-image.

  2. GIT BASELINE — at the FIRST hooked op of a turn we record `HEAD` plus
     the full dirty set (`git status --porcelain -z -uall`) and snapshot the
     pre-image of every DIRTY file. That closes the hole every other agent's
     rewind leaves open: a `sed -i`, a formatter, a build step, any `shell`
     write. A file that was CLEAN at turn start is recoverable from
     `git show <baseline-head>:<path>`; a file that was DIRTY at turn start
     already has its bytes in the pool. Coverage is therefore COMPLETE for a
     git workspace and honestly reported as PARTIAL otherwise.

Everything is journalled as NDJSON (`journal.ndjson`) under
`~/.vis/rewind/<session>/`, so rewind survives a restart — the history is
NOT process-scoped. Entries are append-only and keyed by turn; a
truncated/corrupt trailing line (crash mid-append) is skipped, never fatal.

Restore semantics for `turn` T: every path touched in turns >= T is set back
to the EARLIEST recorded pre-image at or after T. A file created inside the
rewound region is deleted; a deleted file is recreated; a symlink is
recreated as a symlink; a recursively deleted directory is rebuilt and any
file created inside it since is pruned.

This layer owns FILES ONLY. Conversation truncation is the channel's job —
`points` exposes the turn ids to truncate to. Because that boundary is
invisible to a user typing `/rewind`, the slash READS the session store for
each turn's context size and says out loud, in every branch, that the
conversation stays.
raw docstring

com.blockether.vis.internal.foundation.session-slashes

Declarative session-level slash commands shared by every channel.

These are channel-agnostic: the engine dispatches them for every channel through the same slash/dispatch path, and each handler mutates state via the gateway so the change fans out everywhere.

/rename <new title> set this session's title

/rename routes through titling/set-title-with-broadcast! — the single title mutation point.

Declarative session-level slash commands shared by every channel.

These are channel-agnostic: the engine dispatches them for every channel
through the same `slash/dispatch` path, and each
handler mutates state via the gateway so the change fans out everywhere.

  /rename <new title>   set this session's title

`/rename` routes through `titling/set-title-with-broadcast!` — the single
title mutation point.
raw docstring

com.blockether.vis.internal.foundation.shell

Foundation-core's shell implementation. Bound only when the user-owned shell toggle is ON (default ON; flip it OFF in Settings or in vis.yml via toggles: {shell: false} to drop the tools). The OS process jail is the containment layer while active.

ONE model-facing entry point — the shell PYTHON verb, bound BARE in the flat sandbox next to ls / grep. A process is started from Python, and every verb after the spawn is a method on the handle the call returns. EVERY run is a background run: the call spawns under a real pty and returns the HANDLE now, so there is no wait on the request and no number that can select a second mode. ONE call runs ONE command: an ordered batch was a second budget, a second result shape and a second failure mode for what && already says.

  1. sh = await shell("ls")bash -lc in the workspace root, spawned under a REAL pty, its merged output streamed verbatim to a log FILE and registered as a session RESOURCE. sh.wait(30) is what fills exit/out. Output is bounded at READ time to a head+tail budget per stream, so only the MIDDLE of a huge stream is dropped, never its start or end. A non-zero exit is DATA the model reads, not an error.

  2. A server, watcher or long build is the SAME call — you simply do not wait for it, or you wait for less than it takes. A wait that expires is never a lost process: it keeps running under its id and its log keeps filling.

  3. The log OUTLIVES the run. Every shell keeps its log file and its index row by id for as long as the session does, so "what did that build print" is answerable a turn later from the id alone. That retention is the feature, not a leak to reap.

The result IS the HANDLE: every shell answer is a dict-with-methods in the sandbox, so the process is driven on the object the call already returned — sh.logs(-50) reads the last 50 LINES (or a byte OFFSET, or lines=10 for a ten-line window) and returns a page NOW; page[-4000:] slices that page's out text directly, next(page) continues the read and page.pages() walks its ready pages lazily, bounded to ten by default, while lines=-10 walks back up. sh.wait(30) is the bounded poll loop written once in the engine, sh.type("y") types into the pty and sh.stop() kills the tree. There are no id-taking verbs to re-type an id into; re-issuing a LIVE id gives the same handle back.

STATUS is not a stage of its own: EVERY answer of EVERY stage already says what the shell is doing — status/exit, started_at/finished_at/uptime_ms, log_path, and the live cpu_ms/cpu_percent/rss_bytes of its process tree — so "is it done yet" is read off the result already in hand and never costs a second call.

shell-dispatch survives as the INTERNAL grammar the Python-extension entry points use, since those hand-author an options map and genuinely need an op.

EVERY result of EVERY stage — including an argv run, which uses the same runner — is the one [[shell-result-base]] key set: stage names the producer and is the only thing that varies. A key a stage has nothing to say about is nil / false / 0 instead of absent, so model Python indexes any of them without a KeyError, and a run answers with its command and that command's own bytes at the TOP level — there is no entry to unwrap and no second shape to learn. The shell toggle is registered HERE and owned by Vis core. It closes the MODEL's door only: an installed extension keeps its own trusted process boundary (vis.shell, subprocess), which the toggle does not gate.

Foundation-core's shell implementation. Bound only when the user-owned `shell`
toggle is ON (default ON; flip it OFF in Settings or in `vis.yml` via
`toggles: {shell: false}` to drop the tools). The OS process jail is the
containment layer while active.

ONE model-facing entry point — the `shell` PYTHON verb, bound BARE in the flat
sandbox next to `ls` / `grep`. A process is started from Python, and every verb
after the spawn is a method on the handle the call returns. EVERY run is a
background run: the call spawns
under a real pty and returns the HANDLE now, so there is no `wait` on the
request and no number that can select a second mode. ONE call runs ONE command:
an ordered batch was a second budget, a second result shape and a second failure
mode for what `&&` already says.

1. `sh = await shell("ls")` — `bash -lc` in the workspace root, spawned under
   a REAL pty, its merged output streamed verbatim to a log FILE and registered
   as a session RESOURCE. `sh.wait(30)` is what fills `exit`/`out`. Output is
   bounded at READ time to a head+tail budget per stream, so only the MIDDLE of a
   huge stream is dropped, never its start or end. A non-zero exit is DATA the
   model reads, not an error.

2. A server, watcher or long build is the SAME call — you simply do not wait for
   it, or you wait for less than it takes. A wait that expires is never a lost
   process: it keeps running under its id and its log keeps filling.

3. The log OUTLIVES the run. Every shell keeps its log file and its index row by
   id for as long as the session does, so "what did that build print" is
   answerable a turn later from the id alone. That retention is the feature, not
   a leak to reap.

The result IS the HANDLE: every shell answer is a dict-with-methods in the
sandbox, so the process is driven on the object the call already returned —
`sh.logs(-50)` reads the last 50 LINES (or a byte OFFSET, or `lines=10` for a
ten-line window) and returns a page NOW; `page[-4000:]` slices that page's `out`
text directly, `next(page)` continues the read and `page.pages()` walks its ready
pages lazily, bounded to ten by default, while
`lines=-10` walks back up. `sh.wait(30)` is the bounded poll loop written once
in the engine, `sh.type("y")` types into the pty and `sh.stop()` kills the tree.
There are no id-taking verbs to re-type an id into; re-issuing a LIVE id gives
the same handle back.

STATUS is not a stage of its own: EVERY answer of EVERY stage already says what the
shell is doing — `status`/`exit`, `started_at`/`finished_at`/`uptime_ms`, `log_path`,
and the live `cpu_ms`/`cpu_percent`/`rss_bytes` of its process tree — so "is it done
yet" is read off the result already in hand and never costs a second call.

`shell-dispatch` survives as the INTERNAL grammar the Python-extension entry
points use, since those hand-author an options map and genuinely need an `op`.

EVERY result of EVERY stage — including an argv run, which uses the same runner
— is the one [[shell-result-base]] key set: `stage` names the producer and is
the only thing that varies. A key a stage has nothing to say about is nil /
false / 0 instead of absent, so model Python indexes any of them without a
KeyError, and a run answers with its `command` and that command's own bytes at
the TOP level — there is no entry to unwrap and no second shape to learn.
The `shell` toggle is registered HERE and owned by Vis core. It closes the
MODEL's door only: an installed extension keeps its own trusted process boundary
(`vis.shell`, `subprocess`), which the toggle does not gate.
raw docstring

com.blockether.vis.internal.foundation.shell-log

The output of ONE background shell, stored as a FILE and read by BYTE OFFSET.

The file is the STORAGE and the in-memory ring buffer is only a VIEW. A ring is a display convenience: it answers "what is on screen now", and the moment a command prints more than the ring holds, the head is gone before the first poll and no sequence of reads can recover it. That is the whole reported bug, and it is a storage bug, so the fix is storage: every byte the pump reads is appended to ~/.vis/logs/shell/<session>/<id>.log, and a read names the byte it starts at.

A chunk is the paging contract for a growing file, key for key: give an offset, get the bytes and the next-offset to continue from; a NEGATIVE offset names LINES from the end instead (-50 is the last 50). Feeding next-offset back in a loop yields the WHOLE stream with no overlap and no gap, which is why there is no dropped count anywhere in this namespace — nothing is dropped, so nothing has to be reported as lost.

is-eof means "you have read everything WRITTEN so far", never "the command finished": the process's own status belongs to its handle, not to a read of its log. is-truncated is a cap on THIS read alone and never on the file.

The log is PERSISTENT and belongs to the SESSION: it outlives the process's exit, a daemon restart, and the turn that started the command. It dies with the session — delete-session-logs! runs where the session record is deleted, and the DB index row is scoped to the session soul, so the database cascade retires it in the same breath.

Bytes on disk, index in the DB. The log never becomes a row: it is an append-only stream read by offset, and sqlite would turn every pump flush into a blob rewrite and every cursor read into a substring over that blob. What the DB carries is the ROW that makes a log FINDABLE without holding a handle — the command, the path, the start/end and the exit — on the extension_aggregate sidecar rail under index-extension-id.

The output of ONE background shell, stored as a FILE and read by BYTE OFFSET.

The file is the STORAGE and the in-memory ring buffer is only a VIEW. A ring
is a display convenience: it answers "what is on screen now", and the moment a
command prints more than the ring holds, the head is gone before the first
poll and no sequence of reads can recover it. That is the whole reported bug,
and it is a storage bug, so the fix is storage: every byte the pump reads is
appended to `~/.vis/logs/shell/<session>/<id>.log`, and a read names the byte
it starts at.

A chunk is the paging contract for a growing file, key for key: give an
`offset`, get the bytes and the `next-offset` to continue from; a NEGATIVE
`offset` names LINES from the end instead (`-50` is the last 50). Feeding
`next-offset` back in a loop yields the WHOLE stream with no overlap and no
gap, which is why there is no `dropped` count anywhere in this namespace —
nothing is dropped, so nothing has to be reported as lost.

`is-eof` means "you have read everything WRITTEN so far", never "the command
finished": the process's own status belongs to its handle, not to a read of
its log. `is-truncated` is a cap on THIS read alone and never on the file.

The log is PERSISTENT and belongs to the SESSION: it outlives the process's
exit, a daemon restart, and the turn that started the command. It dies with
the session — [[delete-session-logs!]] runs where the session record is
deleted, and the DB index row is scoped to the session soul, so the database
cascade retires it in the same breath.

Bytes on disk, index in the DB. The log never becomes a row: it is an
append-only stream read by offset, and sqlite would turn every pump flush into
a blob rewrite and every cursor read into a substring over that blob. What
the DB carries is the ROW that makes a log FINDABLE without holding a
handle — the command, the path, the start/end and the exit — on the
`extension_aggregate` sidecar rail under [[index-extension-id]].
raw docstring

com.blockether.vis.internal.foundation.shim-attach

Built-in sandbox SHIM: attach — the GENERIC producer twin of the matplotlib capture. A tool running in python_execution writes any artifact (a PNG it rendered, a CSV/JSON/PDF/wav it built, whatever) and hands it to attach(path) (or attach(data, filename) for bytes it never wrote out), getting back the stored artifact's DESCRIPTOR; the engine then OWNS the bytes as a durable session_iteration_attachment row, exactly like a matplotlib figure — surviving a web/TUI restart and (for image media-types) replayable to a vision model cross-turn.

No parsing, no round-trip through the model-facing stdout: we control the whole boundary. The Python side reads the file through the sandbox's OWN confined open (so filesystem-root confinement is enforced for free — a path outside the roots raises the normal sandbox error), sniffs the media-type (magic bytes then extension then utf-8 probe), base64-encodes, and calls the tiny host bridge __vis_record_attachment__, which appends the attachment map to the per-block *image-sink* (mpl-capture/record-attachment!). run-python-block drains that sink into the block outcome's :attachments; the loop stamps each with the producing block's tool-call-id and hands them to db-store-iteration!'s :attachments. The artifact's :id and :version are minted at the sink, so the block that produced it can address it immediately.

Registered unconditionally as a foundation shim (like shim-yaml / shim-matplotlib): its :ext/sandbox-shims entry autoloads attach into every sandbox.

Built-in sandbox SHIM: `attach` — the GENERIC
producer twin of the matplotlib capture. A tool running in `python_execution`
writes any artifact (a PNG it rendered, a CSV/JSON/PDF/wav it built, whatever)
and hands it to `attach(path)` (or `attach(data, filename)` for bytes it
never wrote out), getting back the stored artifact's DESCRIPTOR; the
engine then OWNS the bytes as a durable `session_iteration_attachment` row,
exactly like a matplotlib figure — surviving a web/TUI restart and (for image
media-types) replayable to a vision model cross-turn.

No parsing, no round-trip through the model-facing stdout: we control the whole
boundary. The Python side reads the file through the sandbox's OWN confined
`open` (so filesystem-root confinement is enforced for free — a path outside
the roots raises the normal sandbox error), sniffs the media-type (magic bytes
then extension then utf-8 probe), base64-encodes, and calls the tiny host
bridge `__vis_record_attachment__`, which appends the attachment map to the
per-block `*image-sink*` (`mpl-capture/record-attachment!`). `run-python-block`
drains that sink into the block outcome's `:attachments`; the loop stamps each with
the producing block's tool-call-id and hands them to `db-store-iteration!`'s
`:attachments`. The artifact's `:id` and `:version` are minted at the sink, so
the block that produced it can address it immediately.

Registered unconditionally as a foundation shim (like shim-yaml /
shim-matplotlib): its `:ext/sandbox-shims` entry autoloads `attach` into
every sandbox.
raw docstring

com.blockether.vis.internal.foundation.shim-ls

Built-in sandbox SHIM: ls — the DIRECTORY listing available inside Python.

Mapping a tree is the cheapest question there is and the one a model asks most, so ls(dir) runs inside the python_execution block already in flight. It answers with one compact tree STRING, ready to print. Structured rows would cost the reader a second rendering step and the context every quoted brace; the tree is the shortest form that still says name, kind, size and shape.

The walk itself stays on the HOST: editing/list-directories is fff's ignore-aware listing (.gitignore, .ignore, cache directories, the vis.yml overlay), an order of magnitude faster than a guest os.scandir recursion that would honour none of those rules. Rows cross as JSON so the shim renders native Python dicts, not foreign proxies. Failures use the standard host-tool boundary and the same declarative error hook as the editing tools.

:fs/access is asked by list-directories itself, so an extension that hides a tree hides it from the listing exactly as it hides it from every read.

Built-in sandbox SHIM: `ls` — the DIRECTORY listing available inside Python.

Mapping a tree is the cheapest question there is and the one a model asks
most, so `ls(dir)` runs inside the `python_execution` block already in flight.
It answers with one compact tree STRING, ready to print. Structured rows would
cost the reader a second rendering step and the context every quoted brace; the
tree is the shortest form that still says name, kind, size and shape.

The walk itself stays on the HOST: `editing/list-directories` is fff's
ignore-aware listing (`.gitignore`, `.ignore`, cache directories, the `vis.yml`
overlay), an order of magnitude faster than a guest `os.scandir` recursion that
would honour none of those rules. Rows cross as JSON so the shim renders native
Python dicts, not foreign proxies. Failures use the standard host-tool boundary
and the same declarative error hook as the editing tools.

`:fs/access` is asked by `list-directories` itself, so an extension that hides
a tree hides it from the listing exactly as it hides it from every read.
raw docstring

com.blockether.vis.internal.foundation.transcript

Full session transcript - DATA first, presentation second.

transcript returns one canonical Clojure map with every turn, every iteration, every executed block plus the LLM-side context (system prompt, message envelope, reasoning trace, top-level provider error, per-iteration vars, answer-form pointer, returned-empty-blocks flag) and the per-block forensic detail (code, comment, stdout, error, duration, timeout?, repaired?). Pure data. The agent can pattern-match on it; the CLI renders Markdown on top; a future TUI screen, JSON exporter, or analytics extension consumes the same shape.

Lives in foundation because it's an introspection surface, not host plumbing. The sandbox-visible public surface is (session-state) for data (including compact usage) and (sessions) for metadata lookup; this namespace owns the transcript portion behind that deeper interface.

Public Clojure surface:

(transcript db-info session-id) -> transcript data map (transcript->md data) -> Markdown string (transcript-md db-info session-id) -> DB lookup + Markdown string

Canonical data shape:

{:session {:id :title :channel :model :provider :created-at} :totals {:turns N :iterations N :tokens {:input :output :reasoning :cached} :cost-usd D} :timeline [{:kind :ref :turn-id :iteration-id :content :code :status :duration-ms}] :turns [{:id :user-request :status :prior-outcome :provider :model :iteration-count :failure-count :tokens :cost-usd :content :iterations [{:id :position :status :duration-ms :provider :model :thinking :error :tokens :cost-usd :answer-position :returned-empty-blocks? :vars [{:name :code :value :version}] :attachments [{:id :source :tool-call-id :position :kind :media-type :filename :version :size :stored}] :blocks [{:position :code :comment :stdout :error :duration-ms :timeout? :repaired?}]}]}]}

The Markdown renderer renders thinking, iteration-level errors, vars, per-block forensic previews, and final answer text. Large fields are bounded so reports stay safe to open.

Full session transcript - DATA first, presentation second.

`transcript` returns one canonical Clojure map with every turn,
every iteration, every executed block plus the LLM-side context
(system prompt, message envelope, reasoning trace, top-level
provider error, per-iteration vars, answer-form pointer,
returned-empty-blocks flag) and the per-block forensic detail
(code, comment, stdout, error, duration, timeout?, repaired?).
Pure data. The agent can pattern-match on it; the CLI
renders Markdown on top; a future TUI screen, JSON exporter, or
analytics extension consumes the same shape.

Lives in foundation because it's an introspection surface, not host
plumbing. The sandbox-visible public surface is `(session-state)` for data
(including compact usage) and `(sessions)` for metadata lookup; this namespace
owns the transcript portion behind that deeper interface.

Public Clojure surface:

  `(transcript      db-info session-id)`  -> transcript data map
  `(transcript->md  data)`             -> Markdown string
  `(transcript-md   db-info session-id)`  -> DB lookup + Markdown string

Canonical data shape:

  {:session {:id :title :channel :model :provider :created-at}
   :totals       {:turns N :iterations N
                  :tokens {:input :output :reasoning :cached}
                  :cost-usd D}
   :timeline    [{:kind :ref :turn-id :iteration-id :content :code
                  :status :duration-ms}]
   :turns
    [{:id :user-request :status :prior-outcome :provider :model
      :iteration-count :failure-count
      :tokens :cost-usd :content
      :iterations
       [{:id :position :status :duration-ms
         :provider :model :thinking :error
         :tokens :cost-usd
         :answer-position :returned-empty-blocks?
         :vars
         [{:name :code :value :version}]
         :attachments
         [{:id :source :tool-call-id :position :kind
           :media-type :filename :version :size :stored}]
         :blocks
         [{:position :code :comment :stdout :error
           :duration-ms :timeout? :repaired?}]}]}]}

The Markdown renderer renders thinking, iteration-level errors,
vars, per-block forensic previews, and final answer text. Large
fields are bounded so reports stay safe to open.
raw docstring

com.blockether.vis.internal.foundation.workspace-ctx

Pre-turn "session_workspace" CTX block (STRING-KEYED — crosses the Python boundary as session["workspace"]).

Sessions may work directly in trunk or inside an isolated backend workspace. That distinction is reported on "isolated" (the word sandbox names the Python sandbox, and confinement is jail), NOT as a VCS. "vcs_kind" reports the underlying repository VCS ("git" when the root is inside a git repo, else "none") so it matches the git/ extension surface, which activates on the same predicate. The model reads the workspace block to know the active root and what it has changed since the fork. The block is stamped once per turn at engine start; ctx_renderer serialises it verbatim.

Pre-turn `"session_workspace"` CTX block (STRING-KEYED — crosses the
Python boundary as `session["workspace"]`).

Sessions may work directly in trunk or inside an isolated backend
workspace. That distinction is reported on `"isolated"` (the word
`sandbox` names the Python sandbox, and confinement is `jail`), NOT
as a VCS. `"vcs_kind"` reports the underlying repository VCS (`"git"`
when the root is inside a git repo, else `"none"`) so it matches the
`git/` extension surface, which activates on the same predicate. The
model reads the workspace block to know the active root and what it has
changed since the fork. The block is stamped once per turn at engine
start; ctx_renderer serialises it verbatim.
raw docstring

com.blockether.vis.internal.foundation.workspace-slashes

Declarative filesystem-root slash command.

/cd is session-scoped and available in every channel. What the jail ALLOWS comes from jail.filesystem in merged config; the command only moves the session's primary live root within that grant.

Declarative filesystem-root slash command.

`/cd` is session-scoped and available in every channel. What the jail ALLOWS
comes from `jail.filesystem` in merged config; the command only moves the
session's primary live root within that grant.
raw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

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