Shell output stored on disk and read in bounded windows.
Every byte read from a shell is appended to
~/.vis/logs/YYYY-MM-DD/shell/<session>/<id>.log, using the UTC start date.
The pump keeps only a fixed-size transfer buffer, not a copy of the output.
Live views and later reads use the same file.
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.
Shell output stored on disk and read in bounded windows. Every byte read from a shell is appended to `~/.vis/logs/YYYY-MM-DD/shell/<session>/<id>.log`, using the UTC start date. The pump keeps only a fixed-size transfer buffer, not a copy of the output. Live views and later reads use the same file. 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]].
(close! sink)Flush and close a sink. Idempotent and never throws — a log that cannot be closed must not take the shell's teardown down with it.
Flush and close a sink. Idempotent and never throws — a log that cannot be closed must not take the shell's teardown down with it.
Bytes ONE read returns when the caller named no limit. A window, not the
file: the rest is one more read away at next-offset.
Bytes ONE read returns when the caller named no limit. A window, not the file: the rest is one more read away at `next-offset`.
(delete-session-logs! session)Delete a session's shell logs across all date directories.
Delete a session's shell logs across all date directories.
(drain! in sink)Copy raw bytes from in to sink with a fixed-size transfer buffer.
Each chunk is flushed before the next read so partial lines are visible while the shell runs. No text, lines or output history are retained. Read, write and flush failures propagate to the caller, which owns closing both streams.
Copy raw bytes from `in` to `sink` with a fixed-size transfer buffer. Each chunk is flushed before the next read so partial lines are visible while the shell runs. No text, lines or output history are retained. Read, write and flush failures propagate to the caller, which owns closing both streams.
(index! db-info session id data)Upsert the row that makes ONE log findable by session: the command, the log path, the start, and — once the pump has seen the child die — the end and the exit code. Best effort by construction: the bytes are on disk either way, and an index that throws must never take a running shell with it.
data is engine-shaped (kebab keywords) and reaches the database through
[[wire/->wire]], because a row that outlives the process is JSON on disk and
wears the wire's snake_case string keys — which is exactly what
session-logs reads back.
Upsert the row that makes ONE log findable by session: the command, the log path, the start, and — once the pump has seen the child die — the end and the exit code. Best effort by construction: the bytes are on disk either way, and an index that throws must never take a running shell with it. `data` is engine-shaped (kebab keywords) and reaches the database through [[wire/->wire]], because a row that outlives the process is JSON on disk and wears the wire's snake_case string keys — which is exactly what [[session-logs]] reads back.
Owner of the shell-log sidecar rows.
Owner of the shell-log sidecar rows.
kind of a shell-log sidecar row.
`kind` of a shell-log sidecar row.
(log-file session id)Newest dated log for session/id, or today's path when none exists.
Date discovery preserves lookup across midnight and gateway restarts.
Newest dated log for `session`/`id`, or today's path when none exists. Date discovery preserves lookup across midnight and gateway restarts.
Ceiling on ONE read, so a caller cannot bill a 40 MiB build log to a single context window by asking for it.
Ceiling on ONE read, so a caller cannot bill a 40 MiB build log to a single context window by asking for it.
(open! session id)Create (truncating) the log file for session/id and return the sink the
pump writes through: {:path :out}. Truncating is the point — a new spawn
under an id IS a new shell, and its log starts at offset 0.
Create (truncating) the log file for `session`/`id` and return the sink the
pump writes through: `{:path :out}`. Truncating is the point — a new spawn
under an id IS a new shell, and its log starts at offset 0.(read-chunk id file)(read-chunk id file {:keys [offset limit lines]})Read shell id's log file from :offset and answer a [[::log-chunk]].
With no :offset the read is the TAIL — the last default-chunk-bytes —
because that is what someone watching a live command wants, and the head is
one {:offset 0} away rather than gone. With a POSITIVE offset it reads
FORWARD from exactly that byte, which is what a loop feeding next-offset
does. A NEGATIVE offset counts LINES back from the end — {:offset -50} is
the last 50 lines — and clamps to the start of the window when the log holds
fewer, so more tail than exists is the whole log rather than an error.
:lines N is the LINE window: the answer holds at most N lines, and with no
:offset it is anchored at the last N — so {:lines 10} is the last ten
lines and {:offset next-offset :lines 10} walks the next ten, the two
switches a reader of a long log actually reaches for. A NEGATIVE :lines
walks the OTHER way: {:offset o :lines -10} is the ten lines ENDING at o,
so the window slides back up a log as easily as it slides down, and the
:offset it answers is what feeds the read above it. Bytes stay the outer
cap: :limit still bounds one read.
A missing file is an empty chunk at offset 0, not an error: a shell that has printed nothing yet and a shell whose log was deleted read alike, and neither is worth an exception.
Read shell `id`'s log `file` from `:offset` and answer a [[::log-chunk]].
With no `:offset` the read is the TAIL — the last [[default-chunk-bytes]] —
because that is what someone watching a live command wants, and the head is
one `{:offset 0}` away rather than gone. With a POSITIVE offset it reads
FORWARD from exactly that byte, which is what a loop feeding `next-offset`
does. A NEGATIVE offset counts LINES back from the end — `{:offset -50}` is
the last 50 lines — and clamps to the start of the window when the log holds
fewer, so more tail than exists is the whole log rather than an error.
`:lines` N is the LINE window: the answer holds at most N lines, and with no
`:offset` it is anchored at the last N — so `{:lines 10}` is the last ten
lines and `{:offset next-offset :lines 10}` walks the next ten, the two
switches a reader of a long log actually reaches for. A NEGATIVE `:lines`
walks the OTHER way: `{:offset o :lines -10}` is the ten lines ENDING at `o`,
so the window slides back up a log as easily as it slides down, and the
`:offset` it answers is what feeds the read above it. Bytes stay the outer
cap: `:limit` still bounds one read.
A missing file is an empty chunk at offset 0, not an error: a shell that has
printed nothing yet and a shell whose log was deleted read alike, and neither
is worth an exception.(session-dir session)Directory for new shell logs of one session on the current UTC date.
Directory for new shell logs of one session on the current UTC date.
(session-logs db-info session)Every indexed shell log of session, newest start first. The answer to
"what did that build print" a turn later, with no handle in hand.
Rows are wire-shaped: snake_case string keys, exactly as index! wrote
them.
Every indexed shell log of `session`, newest start first. The answer to "what did that build print" a turn later, with no handle in hand. Rows are wire-shaped: snake_case string keys, exactly as [[index!]] wrote them.
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 |