luv

Workshop wiki

agent-harnesses.org

Field notes on four agent harnesses

Four earlier harnesses, read for what luv can borrow #LRDE65

Four of the author's other repositories are, more or less, LLM agent harnesses, and each solved a different part of the problem well enough to be worth remembering here before luv grows its own agent surface (an agent already lives inside the world terminal, #NMAD2U). This page is a reading of those repositories, not a plan: what is good in each, stated concretely enough that a later design can cite it.

A fifth harness, Autolith (Common Lisp, one self-modifying SBCL image), is read at length on its own page, #HBTNMM; #WU3918 there measures it against the convergence list below (#YY6EYZ).

The three voices to keep apart: what each repository actually does (cited by file), what any harness must therefore do (stated as a lesson), and what luv might choose (left to marks and later pages).

Froth: a tool result is a tree of blocks, not a string #5GU1VI

The strongest idea in froth is Froth.Context.Block (lib/froth/context/block.ex): a keyword list of attributes (headers, like MIME parts, order preserved so :kind stays first), an optional binary body, and child blocks. A tool returns blocks and nothing else -- it "does not measure bodies, does not decide whether content should be blobbed, and does not render anything." Three later stages, each in one place, own those decisions:

  1. Materialization (lib/froth/context/blocks.ex) is the only module that decides inline versus external. Text bodies over 2,400 bytes or 80 lines go to a blob and gain precomputed :blob :size :lines :head :tail :omitted attributes (10 head lines, 5 tail), so renderers never touch the database. Binary bodies, decided purely by :mime, always blob. A text body that is not JSON-safe (invalid UTF-8, embedded NUL, which Postgres refuses inside JSONB) is sniffed and promoted to binary -- so a shell pipeline that prints a PNG surfaces to the model as an image part, and one that dumps a core file just works too.
  2. Rendering (lib/froth/context/block_html.ex) produces the pseudo-XML the model reads with HEEx function components, never string concatenation, so escaping is free. Two views over the same data: live (full body, or head, an omitted element with the hint "N lines omitted -- use pager to read more", tail) and trace (three-line, 160-character preview) used when an old cycle is replayed inside a later prompt: "the trace view is for remembering what happened, not for doing more work with it." Tag names come from the block's :kind through a validated safe_tag, so shell output renders as a shell element. Binary blocks get a deliberately redundant placeholder body ([binary: image/png 10650 bytes -> blob:01K...]) because a self-closing tag reads as "empty output".
  3. Provider serialization (lib/froth/agent/tool_result.ex) walks the tree once for text and once for binaries, emitting real image/document content parts for providers that accept them, while others still see the placeholder and degrade cleanly.

Lesson: separate what the tool found from how much of it the model sees from how it is spelled for a given provider. Each is a policy, and each wants to change independently.

Froth: blobs plus a recursive pager teach the model to stop piping into head #FDMSXG

Froth.Blob (lib/froth/blob.ex) is a content-addressed store kept as bytea in Postgres so blobs stay transactional with everything else, deduplicated on SHA-256, and shown to the model as blob:01K... ULIDs that are globally addressable. The pager tool (lib/froth/tools/pager.ex) offers read | head | tail | grep | stat with bounded integers (80 lines by default, grep with three lines of context and at most 50 hits) and returns blocks, each marked no_fold so a deliberate page is not folded again. A grep result that is itself large becomes a new blob returned by id: the pager is recursively composable.

The run_shell description then unteaches shell habits: do not pipe into head, tail, cut, wc -l or less to avoid "too much output"; do not wrap things in timeout, nohup or &. The harness owns truncation and backgrounding, so the model does not have to guess.

Lesson: when the harness handles size honestly (folded, addressable, pageable), the model can be told to stop defending itself, and the prompt gets simpler and cheaper.

Froth: context is assembled for the cache, and the prompt is a page you can open #WYOFMD

Froth.Telegram.BotContext builds a view model and renders it as a list of parts rather than one string, so PromptCache can place Anthropic one-hour cache breakpoints at the last chapter and a few messages back from the newest (a tail backoff so new messages do not invalidate the breakpoint). RecentWindow replaces a sliding message count with a time-and-mass window (minimum hours, target hours, character budget, bucket minutes) precisely because a count-based window drops the oldest message on every new one and invalidates the whole cached tail (RFC 0019 in rfc/, which contains measured prompt-byte breakdowns and EXPLAIN ANALYZE timings rather than intuition).

Memory is layered -- chronicle volumes, weekly chapters, daily summaries, raw tail -- and the timeline tool lets the agent descend volume, week, day, messages by opaque stable refs (volume:208, week:185) instead of guessing dates. The exact live prompt can be opened in a browser (/froth/bot-context) or printed from the shell (mix froth.context --cycle latest:3).

Lesson: prompt assembly is a rendering problem with a caching problem inside it; make the parts explicit, make the window cache-stable, and make the result inspectable as a page.

Froth: live evaluation as the agent's native interface, with a detachable clock #ENM1B2

lib/froth/tools/elixir_eval.ex describes itself as "a capability browser and an Elixir evaluator" with a DISCOVER, INSPECT, ACT loop: docs with no target returns the module hierarchy (fetching and caching stdlib source for the running version when asked), eval runs against the live BEAM. Bindings persist per session (six-hour TTL). Evaluation runs under a custom group leader implementing the :io_request protocol, so IO.puts inside evaluated code streams into task events live. If evaluation does not finish within three seconds it detaches and returns an eval_running block carrying a task id; the shell tool does the same over a Port with send_input, signals, and an idle timeout.

This is the same shape as luv's ./sly against the durable image, and the lesson is the timeout: a tool call should return something within a few seconds -- a result or a handle to the running work -- never silence. That is this project's "five seconds of silence means broken" rule stated from the harness side.

Froth: parallel tools with a prepare/commit split, and control by button rather than allowlist #IOGTGD

Froth.Agent.Worker is a state machine over :initial | {:thinking, task} | {:working, batch} | {:awaiting_user_input, batch} | :done, running all tool uses of a turn as unlinked supervised tasks with per-tool timers. RFC 0003 admits that this parallelism was once fake -- every task funnelled through one bot GenServer mailbox -- and the fix is visible in execute_tool/3: a short prepare_tool call yields an executable (M,F,A), the work runs in the task, then commit_tool folds side effects back. RFC 0021 makes every cycle its own supervised CycleRuntime, registered by id, subagents as children of the spawning runtime.

There is no permission allowlist. Control is human-in-the-loop over Telegram inline keyboards, each path its own module: Ask parks the cycle until a button or free text arrives; AwaitControl offers detach, continue, cancel, check for background tasks; FailureIntervention intercepts repeated tool errors and offers up to four numbered replies plus carry-on and stop; CreditIntervention sniffs "credit balance is too low" and posts a button labelled "Insert coin." Tool results carry control_outcome, control_data, and yield?, and the worker's batch transition distinguishes yield (finish cycle), unresolved awaits (park), and continue. Required structured description objects on the shell and eval tools (action, up to three goals, assumptions) are validated and become the user-visible narration.

Lesson: the seams that matter are prepare vs. commit (so parallel really is parallel) and park vs. finish (so a human can be asked without ending the turn).

Froth: one span primitive, one firehose, and a patch model over every provider stream #VGXIN9

lib/span.ex is a single primitive for point events and open or closed spans, each writing a row to events and publishing on a firehose topic plus a per-cycle topic. A cycle is consumed as a Stream.resource over that PubSub subscription; payloads over 8,000 bytes are offloaded to an object store with a preview kept inline. FollowLive filters the firehose by event prefix, cycle, or span; ToolLive is a Telegram mini-app view of a running cycle with live thinking, live IO, a steer form and a stop button; a cache report prints per-cycle cache-creation and cache-read tokens.

The LLM layer is provider-neutral in an unusual way: streaming deltas are decoded into %LLM.Edit{op: :open | :set | :append | :merge | :delete | :close, resource, path, value} applied to a store document, so Anthropic, OpenAI Responses and Gemini streams become one patch language, and Edit.project_event/1 maps patches back to caller events such as text_delta. A hand-rolled SSE parser exists because the library one could not handle CRLF line endings, "i.e. every Google API"; tests replay recorded SSE fixtures through the real decoder.

Lesson: normalize streams to edits on a document, and record spans as data first, views second.

Sheaf: six characters address anything, resolved through the graph #9P5YLW

lib/sheaf/id.ex mints six-character handles from a 32-symbol alphabet with ambiguous glyphs removed, and maps handle to IRI by concatenation onto a resource base: #HCFU75 is literally the tail of a URL. Every paragraph, section, document, note, image, conversation and spreadsheet query result is minted the same way -- one flat space, no type prefixes. This is the same move as this wiki's figure IDs (#2RRDHC), taken further.

lib/sheaf/block_refs.ex accepts five spellings -- bare, #-prefixed, bracketed, an existing /b/ link, a code span -- and its bare-id regex requires at least one letter and one digit so six-letter words and pure numbers are not swallowed. linkify_markdown splits fenced and inline code out before rewriting, takes exists? and url_for callbacks so unresolved handles degrade to plain text, and eats the space an LLM leaves before punctuation. Resolution is graph-driven: Corpus.find_documents/1 takes a batch of ids, does one quad-pattern match on rdf:type, and reads the named graph of each hit -- the containing document is whatever graph the block's type statement lives in. A ResourceResolver then dispatches one handle to document, block, note, project, conversation or query result, so one syntax works in prose, URLs, tool arguments, and RDF; hover previews batch by document and compute ancestry breadcrumbs.

Lesson: one short handle space, resolved by asking the store rather than maintaining a link table, lets the model cite by typing.

Sheaf: an append-only quad log with a working set, and doctrine about how to read it #DCH4QD

lib/quadlog.ex replaced a SPARQL server with three SQLite tables: an append-only changes journal (sequence, transaction IRI, polarity, the expanded quad), interned terms, and a quads current-state table with four covering indexes. History is assertions and retractions keyed by transaction; the present is index-friendly. The GenServer keeps a partial in-memory dataset plus a set of loaded patterns, so load_once/1 is an idempotent "pull this neighbourhood into the working set." Every transaction (Sheaf.Repo.transact/3) carries a transaction IRI and OpenTelemetry metadata -- provenance as a condition of writing.

docs/quadlog-access-patterns.md is unusually good internal doctrine: "Quadlog is not a SPARQL server"; use match for broad derived indexes, load_once for request-time neighbourhoods, build a subject-predicate-to-objects index rather than nesting triple scans -- with a measured baseline (479,268 quads, document index about 365 ms warm).

Lesson: a log of signed changes plus a materialized present is enough store for an agent, provided the access doctrine is written down beside it.

Sheaf: typed tool results with two renderings, and reading collapsed by default #GR1GL1

lib/sheaf/assistant/corpus_tools.ex builds the tool list with nearly every dependency injected, so tools are testable without a model, and gates them by tool set so a reading conversation literally cannot mutate documents. Tool callbacks return structs (ToolResults.*); tool_result_text.ex renders them as "compact reading notes for a model, not API payloads", and the same structs drive rich UI cards -- one representation, two audiences. read takes a list of handles and returns sections collapsed with child handles; expand: true walks descendants; repository expansion stops at the tree and never expands file contents; search hits return a bounded excerpt plus a #content IRI to pass back to read.

document_import is one dispatcher over an action enumeration (stage, status, extract, inspect, import, metadata, validate) on a durable RDF-backed run: "one orthogonal action interface the model can compose in different orders, while the server retains authority over paths, credentials, network access, and RDF mutations." Fetching is HTTPS-only, rejects private addresses and credentials, validates redirects, caps bytes, and hands the model file ids, never paths. Writing-attention is four tags (placeholder, needs_evidence, needs_revision, fragment) stored as triples, not TODO comments.

Lesson: the model-facing rendering and the human-facing rendering are two views of one typed value; and "collapsed with handles" is the default that keeps reading cheap.

Sheaf: retrieval variants, persisted context, and one URL in four representations #RLXBWB

Retrieval fuses lexical and vector hits by reciprocal-rank fusion, with a small bonus for a unit found by both channels. Each text unit is embedded twice -- :precise (raw text) and :context (text plus title, authors, page context, stored at a #sheaf-context fragment IRI, down-weighted to 0.85) -- and long source files add overlapping 8 KB segment variants that are not RDF resources and hydrate back to the file's single content block. The evaluation suite uses expected text fragments rather than block IRIs as ground truth, because reimporting a document intentionally remints its blocks.

Conversation context is persisted twice on purpose: raw provider messages as JSON blobs with an append index in a dedicated operational graph (tool schemas stored, callbacks reconstructed from current code), and the semantic trace as ActivityStreams with prov attribution in the workspace graph. write_note is the durable output channel: append-only notes whose mentions are the union of passed handles and refs parsed from the text. The bare /:id path content-negotiates to LiveView HTML, Markdown, JSON or N-Quads with Vary and Link rel=alternate headers; reading a conversation as Markdown is built from the persisted context "so reading a conversation never starts or changes it." A stateless MCP endpoint and a zero-dependency CLI sit on the same six operations, so the CLI, MCP clients, and the in-app assistant share one corpus behaviour.

Lesson: store the operational transcript and the semantic trace separately; expose the same handle at the same URL in every representation a client could want.

Nxt: one coroutine runtime under I/O, tools, and the terminal #3J1QNQ

Nxt is three C++23 namespaces -- nxtrt (runtime), nxtui (raster, layout, terminal), nxtai (Responses client and tool execution) -- over one lazy task type. An SSE stream, a TLS socket, a subprocess, a cgroup sampler and a terminal HUD are all tasks on the same deck, so an agent turn and its UI are siblings in one structured scope rather than two event loops bolted together. The deck is a one-round pump: it swaps the ready queue into a local round and drains exactly that; it throws on reentrant pumping; and sync_wait throws a runtime dump when a task is suspended with nothing ready -- deadlock reported with state, not endured.

firm / deed / catching_deed are the structured-concurrency layer: fork children, join, stop together, read results afterward; when_all, wait_any and with_timeout are compositions over firms, not schedulers. wish / wand / urge / need split intent from platform (io_uring, kqueue, epoll behind a four-verb vtable), and the wand flushes once after each round, so a round's staged operations submit as one batch -- io_uring batching falls out of the scheduling discipline. The whole model is also executably specified in nxtrt/runtime.rkt, an ontology, relational model and temporal spec at once, and AGENTS.md makes updating it policy. The deliberately odd four-letter vocabulary is defended as a technique for keeping concepts soft and renameable.

Lesson: when tools, streams and UI share one scheduler, cancellation, timeouts and error aggregation for a batch of tool calls are free.

Nxt: hope, or the value or the work to get it #X966Z4

The single best efficiency idea is hope<T>: a variant of T or task<T> -- "the value, or the work to get it." await_ready is "holds the value", so a hit returns inline with no coroutine frame born; a miss splices the task onto the awaiting handle. The buffer layer is a port of Zig's post-0.15 reader/writer: the reader owns its buffer, take and peek are inline and concrete, and the cold stream_more verb returns a hope touched only on refill. Socket, TLS, HTTP body and SSE therefore compose with zero suspensions while every layer is buffered. docs/rt-holding.md names the endgame: give the wish an honest await_ready and the buffered feed can be a wand, so deck, wand, feed and hope are one shape at four sizes -- "a scheduler is a buffer whose drain got clever." Around it: hierarchical bitmask free-lists for bounded slot pools, and RFCs pushing firm bookkeeping off the heap into value-shaped storage.

Lesson worth carrying into luv's own systems thinking: make the hot path a concrete inline read and let suspension be the exception the type admits.

Nxt: a hung agent is a question you can ask the process #BWXFJK

src/nxtrt/debug.hpp keeps process-global registries of firm snapshots (id, parent, children, stopping) and wait snapshots (task, token, parked since, wish), and install_signal_dump on SIGUSR1 prints the ready tasks, the firm tree, and every parked wish with how long it has been parked. AGENTS.md instructs: on a freeze, do not rerun the suite -- inspect the parked tasks and wishes. This is the runtime-side answer to this project's silence rule (#NMAD2U's neighbour, the whole of AGENTS.md).

Around it: an OpenTelemetry-shaped trace_record (span begin, end, event; parent; attributes) with observers and a children query, whose current context rides in an ambient environment -- env.hpp is an immutable type-keyed snapshot shared by children and cloned on replace, dynamic binding for coroutines, also used for a breadcrumb path so the SSE transcript printer knows its nesting without threaded state. Every tool subprocess runs under a transient systemd-run scope and is sampled every 50 ms for memory, PIDs, CPU and PSI pressure; the observation rides along inside the tool_result so the UI can show a memory chip next to the call. NXT_STDOUT_TRACE logs every stdout write with source location and a backtrace -- the tool for when a HUD and a subprocess fight over the terminal -- and ANSI has a debug mode that prints escapes visibly and is the non-TTY default.

Lesson: the parked-wish dump, ambient trace context, and per-call resource telemetry are the three instruments a harness should be born with.

Nxt: traces as Arrow, rendered as cassettes through XSLT #7FFNVT

Runs are written as Arrow IPC files (seq, elapsed_ms, unix_ms, phase, event_type, data, payload_json, run_id), compacted to zstd Parquet (about 31 runs, 170 MB to 3 MB), and queried in place with DuckDB: cassette/trace-to-xml.sql derives turns with a window function over response.created, joins call and result rows for per-call duration, and dispatches content extraction per tool. Then Parquet becomes XML becomes XSLT (baltic-birch-v1.xsl) becomes markup becomes ANSI. The conceit is that a tool call is not a table row but a cassette -- case, spine label, J-card, window onto the tape -- and that its visual language should live in one stylesheet rather than in hundreds of lines of C++ interleaving string splitting and colour tables; a template matching result[@kind'matches']= is the declarative twin of a visitor case. Every stage is a flat file, so every stage is inspectable, and scripts/nxtllm-shot runs the binary under a PTY and renders a contact-sheet PNG explicitly so an agent can look at what the tool cycle drew. nxtui.org states the principle behind it: separate operational control from historical memory, with append-only Arrow to Parquet to DuckDB to Org chronicle to compacted summaries.

Lesson: an agent's history is columnar data first; every rendering of it is a query plus a stylesheet.

Nxt: a HUD pinned under scrolling output, drawn from typed diffs #2HR80Z

nxtui is fastidious about geometry: ch, ln, percent unit tags and strong row, column, width and height types, with vertical regions half-open and DECSTBM scroll regions inclusive "because that is what the terminal configures" -- "a value that names a row should stay a row until the last possible moment." Layout is values satisfying a concept (width hint, height hint, render), type-erased into immutable shared pointers and combined declaratively (column, row, each, when, either, range_progress_bar). Slot is the reactive seam: an atomic cell copied by value into the tree each frame while a coroutine elsewhere publishes new content and signals damage -- no observer graph.

The raster is struct-of-arrays mdspan planes (glyphs, fg, bg, emphasis) with interned glyphs, and the frame delta is a ranges pipeline -- chunk_by same-run, filter changed, transform to runs carrying only the SGR deltas. The compositor is double-buffered and can confine scrolling above a bottom fixed HUD with DECSTBM, so subprocess output keeps scrolling while the agent HUD stays pinned, and it takes an output mutex so scrollback writes and HUD diffs go out as one presentation step -- the interleaving tear fixed at the right level. trace_tui turns the runtime's spans into a waterfall in the same layout vocabulary.

Lesson for luv's world terminal (#NMAD2U): typed rows and columns, a struct-of-arrays raster, and diff-runs are the same instincts the Slug terminal path already has; the pinned-HUD partition is the piece worth copying when an agent status line lives in the same cells as its output.

Nxt: tools as concepts, failures as values, streams parsed as grammars #KZ019W

Tools are structs checked by a function_tool concept: name, description, strictness, a parameters struct, a raw JSON schema literal, a parser, and run returning a task -- a missing schema is a static_assert. Any exception becomes tool_result{failed, "tool execution failed: ..."}, an agent-visible message rather than a crash; a batch of calls is one firm of forked children joined together. Subprocess capture is a two-child firm -- an 8 MiB-capped stdout/stderr streamer with terminate-and-wait on exit, and the cgroup sampler -- and truncation is never silent: the call is marked failed and its output rewritten as "exceeded capture limit (N bytes); captured prefix follows". rg_search bakes in --max-count 50 --max-columns 200: context discipline enforced at the tool, not by prompt.

The Responses SSE stream is read as a recursive-descent parse over a feed with peek_one / take_one (response.created, in_progress, output_item.added, content_part.added, deltas, done ...) throwing a typed unexpected-event on protocol drift; JSON is tokenized by coroutine rather than materialized, and request bodies are written with raw() for pre-serialized schemas so nothing round-trips through an object model. The tool UI is data (turn_view, call_view with name, arguments, output, latest memory, state, elapsed), sanitizes untrusted subprocess text of escape sequences before drawing it, and caps bash output windows at eight wrapped lines.

Lesson: bound at the tool, tell the model why something is short, and treat the provider stream as a grammar with a name for every violation.

Swash: a shell command is a session with a host, and the host outlives you #KZG2ZF

Swash (README.md, CLAUDE.md) makes every command a session with a six-character id (three letters, three digits, GenID in internal/host/types.go). swash run echo hello asks systemd --user over D-Bus to start swash-host-KXO284.service; that host owns the bus name sh.swa.Swash.KXO284, starts swash-task-KXO284.service for the actual command, and both live in swash-KXO284.slice. The host, not the client, holds the pipes: it reads stdout and stderr through a ProtocolReader (internal/protocol/protocol.go, line-oriented by default, or SSE events split on blank lines) and writes each unit to the journal tagged SWASH_SESSION, FD=1|2. The Controller interface -- SendInput, Kill, Restart, Gist -- is implemented once by the host and once by a D-Bus proxy, so "the thing that started it" is never a distinguished party: any later client, or a different agent, can send input, kill, or restart. Restart keeps the id and command; a session is an identity, not a PID.

The portable posix backend (internal/backend/posix/) is the same two-process shape with a Unix socket in place of D-Bus and a swash minijournald daemon writing native systemd journal files through the pure-Go pkg/journalfile (lookup3 and SipHash ported, format documented beside the code) so journalctl --file reads them on a Mac. Backend choice is auto-detected by probing the bus for org.freedesktop.systemd1. VISION.md says why: current agent tool calls are blocking, fragile, opaque, and tied to one session; a task should be a persistent object with stable identity, provenance, observable state and structured output, and completion "a state, not an event" -- finished work sits in an inbox until someone receives it, which is the not-yet-built half.

Lesson: put the process under a supervisor that owns its output and its control surface, and let clients be transient. Detach and hand-off then cost nothing, because there was never a client to lose.

Swash: run returns in three seconds or a screenful, and says how to follow #I0U0LI

swash run (cmd/swash/main.go) follows the new session and returns either when it exits or when a detach threshold trips: --detach-after defaults to three seconds and --detach-after-output to 80×24 = 1,920 bytes. On detach it prints to stderr "still running after 3s, detaching" or "output exceeded 1920 bytes, detaching", followed by the exact command to resume: swash follow KXO284. swash start is the same with the threshold at zero; swash poll reads what has accumulated since a journal cursor; swash follow streams to exit. Structured lifecycle events use WriteSync (internal/journal/combined.go): the write carries a SWASH_WRITE_NONCE and the call polls until that nonce is readable, so a "started" or "exited" event is read-after-write consistent while ordinary output stays fire-and-forget.

This is the same detach-with-a-handle rule as froth's three-second eval (#ENM1B2), stated for shell commands and made the default rather than an option -- and the "screenful" bound is a nice complement to a time bound: a chatty command detaches early with a handle instead of flooding the caller.

Lesson: default tool calls to "a result, or a handle plus the follow command, within seconds"; measure the bound in both time and bytes.

Swash: TTY sessions with a screen you can read as text #0PK1OV

--tty sessions run under a PTY fed into libvterm compiled to WASM (vterm/, run under wazero, embedded zstd-compressed) so the host keeps a real terminal model without cgo. TTYHost (internal/host/tty.go) exposes GetScreenText, GetScreenANSI, GetRowText, GetCursor, GetTitle, GetScrollback, GetMode (alternate screen or not) and Resize over D-Bus, and logs output lines to the journal only when not in the alternate screen, so a full-screen program does not spray redraw noise into history. On exit the final screen is persisted as one SWASH_EVENT=screen entry with ROWS and COLS. Attach hands the client two file descriptors plus a screen snapshot taken under the same lock as the stream is opened, "so no bytes are lost between them," and several clients may be attached at once (the master size is negotiated). The webui (cmd/swash/webui.go, templ templates) shows sessions, output, and a WebSocket terminal on the same host methods.

Lesson: an agent driving an interactive program wants screen (what is visible now) as a first-class tool result, distinct from the scroll of output; luv's ./sly screenshot is the pixel version of the same instinct.

Swash: contexts as journal facts, and the log projected as RDF #VBQ79J

A context (CONTEXT.md, cmd/swash/context.go) is a directory under $XDG_STATE_HOME/swash/contexts/<ID>/ plus one journal event; a session belongs to a context through a second event (SWASH_EVENT=session-context) emitted when SWASH_CONTEXT is in the environment. Relations are first-class facts, not tree structure -- "to find all output from a context, query in two steps." swash context shell ID opens bash with a generated rcfile whose PROMPT_COMMAND calls swash prompt, which prints [swash context XYZ789; 2 running] above the prompt, so the count of live background sessions is ambient.

The same lifecycle events are also read into an in-memory oxigraph store (Rust compiled to WASI, Go bindings designed in oxigraph-wasi-ffi/DESIGN.md with iter.Seq result iteration): EventToQuads in internal/journal/types.go maps started/exited/context events onto a tiny vocabulary (swash:Session, swash:startedAt, swash:exitCode, swash:context ...) and swash graph query runs SPARQL over it. claude.sh is the throwaway proof of SHELL.md's thesis: a conversation kept in the journal (CLAUDE_SESSION, CLAUDE_ROLE fields via logger --journald), rebuilt into a messages array by journalctl -o json | jq, no files anywhere. SHELL.md argues the harness should be the shell itself -- claude "fix the build" as an ordinary command, scrollback untouched, with a status bar of live tasks showing cgroup-accurate CPU and memory -- and ends on "an LLM API call is just a long-running command that happens to talk to a remote server."

Lesson: one append-only structured log can be the process log, the conversation store, and the source of a graph; group work by emitting a relation, not by nesting directories.

What the four agree on #YY6EYZ

Read together, the four repositories converge on a few points that any future luv agent surface should probably accept as given:

DONE A luv agent surface would inherit these seams #QQRFZ1

Closed by An agent in the little world, whose NEXT mark #YDAQNO is the concrete first slice and whose figures cite the ones below as premises.

Intent. Not a plan yet: an agent already talks to luv through the world terminal (#NMAD2U) and through ./sly; if luv grows its own harness -- Lisp-native tools over the durable image, results drawn into world cells, history in the wiki's own store -- the seams above are the ones to keep.

Evidence. This page. In particular #5GU1VI for the result shape, #9P5YLW for handles (already halfway present as figure IDs), #BWXFJK for the dump the ./sly image should be able to give when a form goes quiet, and #2HR80Z for a HUD in the terminal wall.

Done when. Either a NEXT mark on a concrete first slice replaces this IDEA, or a later page cites the figures above as its premises.