git:20261016.d6f9ac6 to git:20261016.497dcfd

1 added, 1 removed. Audit A to A.

# session_view
## Operator diagnostic metadata
Protocol 055 adds optional `tool_availability.extension_refusals` to credited
snapshot metadata. `snapshot_view.ToolAvailability` retains at most 32
strings of at most 2048 UTF-8 bytes each. An omitted field decodes to an
empty list for old-daemon compatibility; wrong types, excess count or excess
bytes refuse the entire capture before a host adopts it. The strings are
server-owned startup observations, with no mutation or execution authority.
## Purpose
The part of a Loom client that no host owns: the session lane that speaks
the v2 conversation protocol, the decoders for what the daemon sends, the
projection of a completed capture into a view, and the line builders that
turn that view into transcript lines. It was extracted from `packages/tui`
in phase 4 of issue #530 (ADR-013) so that a second host, the daemon's
web view, can drive the same code the terminal does instead of a
copy of it.
"No host owns it" is literal. The package imports `core`, `machine` and
the standard library and nothing else, and lint R6 holds it there
(`packages/lint/src/lint/portable.gleam` says why it is on the list). A
socket, a recording file, a terminal, a clock and a mailbox all belong to
the host. Where the lane has to name one of them, it takes it as a type
parameter: `session_channel.Channel(socket, recorder)` carries the host's
handles to the outputs that act on them and never uses them itself.
The package holds no process and performs no effect. A host drains its own
inbox, hands each `connection_event.Message` to the lane with the time it
read, takes the lane's outputs and performs them.
Since S4 of `docs/design-notes/step-extraction.md` it also holds the shared
step: the session state every host of a session keeps (`model.Shared`), and
the reducers that take and return it alone, which the S3 slices cut out of
the terminal's step. The terminal runs them over its own binding of the
record's handles and applies what each call records for its surfaces; the
web view runs them through `step.update`, one call per message, and drops
the facts it has no surface for. The terminal keeps its own loop: which
event it is, which command a key means, when to drain the inbox, and the
loop over a drain's updates, one update per call (question 11 of the note).
The web view has no such loop of its own, since `step.update` is that loop
for a host with no surfaces.
## Key Types
- `strand_card.status_line(line)`, `status_title(line)`, `glyph(status)`,
`model_name(model)`, `task_words(title)` (a task, or nothing for the
roster's placeholders and the advisor's feed prompt; a brief is cut to its
first sentence and `task_limit` characters), `needing(lines)` and `context_words(tokens)`: the one
status line under a strand's name on its card (`Needs approval` for a
strand that waits on a decision, whatever the request was; the state word
and what the strand is doing after ` · ` for a working one, where the
engine's phase `assistant` reads `thinking`, a tool is named without its
command and a state word the activity already says is not said twice;
`Finished` and how long it ran), the whole activity text for a tooltip
(`status_title`, cut to `title_limit`), the state's one-character glyph,
a model's last path segment, how many strands wait on a decision (the count
on the web view's Strands tab), and the words for a strand's context size
in its own view. Both hosts can read them so the terminal can word a card
the same way.
- `command.Command`: a parsed draft, `Surface(command.Surface)` for a
command the host carries out with its own machinery (a panel, the model
selector, daemon control, a change of strand, the host's exit) or
`Session(command.Session)` for one the session carries out (a prompt, a
steer or a follow-up, the goal, schedule, approval and configuration
commands, `/clear`, and the parse errors). A host routes on the outer
variant, and the shared dispatch (`commands.submit`) is exhaustive over
`Session`.
- `session_channel.Channel(socket, recorder)` (opaque): one credited
conversation lane. `start`, `start_recorded`, `start_resumed` and
`replay` create one; `receive(channel, message, now:)`,
`tick(channel, now:)`, `submit`, `close` and `retire` transition it and
return `Update`s; `take_outputs` hands over the queued `Out`s oldest
first. `socket` returns the host's handle for liveness checks.
`next_due` names the earliest reading at which `tick` would act (the
in-flight deadline, or the idle refresh of a ready lane with a cut), and
both hosts arm one wake-up for it instead of ticking on a cadence. The
idle refresh is `polling_refresh_ms` (250) until the lane receives its
first pushed frame and `pushing_refresh_ms` (5000) after, and the daemon
pushes the roster at subscribe (protocol-change/054); the private
`Delivery` field (`Polling`, `Pushing`) records which.
- `session_channel.Out(socket, recorder)`: `Transmit(socket, frame)`,
`Shut(socket)`, `Note(recorder, attempt.Event)`. The terminal performs
these in `tui/terminal_lane.perform`.
- `session_channel.Update`: what a transition tells the host. `Captured(cut,
view, trigger)` is the only update that replaces the visible projection;
the others are submissions, history pages, lookups, refusals, pushed
stream fragments and tool tails, commit notices, acknowledgements, lost
replies and failure.
- `inbox.Inbox(source, a)` (opaque): what a host received from one
source and has not reduced, oldest first, with the host's name for the
source. `push` files behind the held messages and `take` returns the
oldest; reading a mailbox stays with the host (`tui/buffered` over an
Erlang subject). An adoption replaces the whole value, so a replaced
source's held messages cannot reach a reducer.
- `connection_event.Message`: `Connected`, `Incoming(text)`,
`Closed(reason)`, `NetworkFault(reason)`. The host's transport maps its
own events into these.
- `attempt.Event`, `attempt.Request`, `attempt.Id` and
`attempt.Trace(recorder)`: the recording vocabulary of one attachment
attempt, and the recorder and identity a lane records under.
- `snapshot.Captured`, `snapshot.Window`, `snapshot.Item`,
`snapshot.Attachment`, `snapshot.Expected`: one validated cut and the
bounded window of records it holds. `snapshot.Transfer` (opaque) is a
cut still being received.
- `snapshot_view.View`: the presentation decoded from a cut's metadata
(`snapshot_view.decode`), and `snapshot_view.Branch`, one strand's
records.
- `history_view.State`: the bounded, pageable window of a strand's
ancestry; `capture` folds a cut in and `branch` gives its records.
`older`, `range`, `sent`, `accept`, `cancel`, `freeze` and `resume` page
it through the lane's `history` read, for the terminal and the web view
alike. `retain_from(state, seq)` drops the records older than `seq` from
a live window with no read owed, for a host that draws only the newest
rows (the web view). A `capture` into a window that joins it keeps the
lower `before_seq` an earlier `accept` reached, so a read that found none
of the strand's records is not asked again. The state also holds a `Scan`
(`Unscanned | Scanning | Abandoned`), a transient read of one stretch of the
strand's ancestry beside the window, for a host that draws summaries and
looks at records without keeping them (protocol-change/071). `scan(state,
leaf, before_seq, known, view)` starts it at the record `leaf`, seeded with
the part of `known` and the window that is the leaf's ancestry below
`before_seq`; `scanned` gives what it holds as a `Branch`. The scan reads the
strand's own parent links (protocol-change/072): `scan_older` owes a read
that starts at the parent the scan lacks (`Lineage`: `Idle | Owed(from) |
Out(from)`), `lineage` names the entry to read when the window has no demand
of its own to serve first, `sent_lineage` marks it out, and `accept_lineage`
takes the page that answers it, ignoring a reply to a read the scan did not
ask for. A page that adds no record the scan did not hold sets
`Floor` to `Bottomed`, so the same entry is never asked for twice.
`scan_readable(state, missing)` says whether it can go on (the last read added
records, it has neither reached its bound nor been cut, and the parent it is
missing, `missing`, is not a record over the presentation limit that it
already holds as a descriptor: no read below such a record proves it), and
`scan_end` drops it. It is bounded at 4,096 records and 32 MiB, private to the
module, and a page that would take it past the bound is cut at the oldest end,
so it still ends at its leaf, and the scan is unreadable afterwards. A
`cancel` abandons it. The window is the same value after a scan as before it,
and its own interval read (`range`, `sent`, `accept`) is the terminal's.
- `protocol.Event`, `protocol.EntryRecord` and the board types, and
`session_wire.Reply`: total decoders for the daemon's frames.
- `transcript_line.Line(speaker, text)` and `Speaker`, with the live
observations that become lines: `Stream`, `ToolTail`, `CacheNotice`,
`Submission`. Agent traffic has three speakers, `SentMessage`,
`StrandMessage` and `PeerMessage`, chosen from the `agent_send` call or
the stored origin and never from the text; the text is a heading, a
newline and the body. A peer heading ends in
`transcript_lines.origin_checked`, and every heading in the local clock
time when `Presentation.clock` (from `Shared.clock_offset`) knows the
zone. `agent_messages.Item.ts` keeps the send's time for the workspace.
- `transcript_lines.joined` joins every tool result in a compact window to
its call, for responses whose calls are drawn as narrative: the call is
drawn with the rows a tool group draws for it (`call_rows`), settled,
failed with its reason, a send with its admission, a program with its
value, an image result with its image's row, `absorbed` says which result entries draw nothing,
`reads_joined` names the responses that bypass the entry cache, and
`joined_entry_lines`/`joined_block_lines` draw a response with the
results joined. A compact `code_mode` call is a
`ProgramRunning`, `ProgramSettled` or `ProgramFailure` block, whose text
is a title, a foot and a body: the program's opening lines under their
numbers (`program_body`, shared by running and settled), or the error,
with a compiler diagnostic cut to its heading (`· line N`) and the
source it quotes. A settled block's title counts the calls
(`✓ code_mode · completed · 5 calls`), its foot says how long the program
ran, and it ends in the `CALLS · …` section a failure shows and a
`RESULT · …` preview from `value_preview`: an object's first three keys
with a type or size hint, a list's length and first element, a scalar as
it is. A result with no record has no calls section or foot, and one with
no `details` keeps the generic rows. All three block speakers open and
close bare, like a call.
- `call_tree.{read, summary, CallLog, Call, Status}` (protocol 060): the
total decoder for the `calls` key of a `code_mode` result's `details` and
the one-line summary (`7 calls · 1 failed`). An absent key and a
malformed one both read as `None`, and `transcript_lines` then renders the
result exactly as it did before the record existed. A `code_mode` failure
with a readable record shows the summary and rows under the failure text.
The golden JSON in `call_tree_test` is the same literal `tools` asserts
its encoder writes.
- `program_card.{read, ProgramCard, State, Source, Chip, CallTable, CallRow,
Bar, Failure, Diagnostic, Value, Opening, opening, bar, axis, chip_words,
budget_words}`: a foreground `code_mode` call and the result joined to it as
typed fields, for a host that draws more than text. The terminal does not
use it and its rows are unchanged. `read` answers `None` for a launch, a
call with no program and a result with no readable details, and otherwise a
card in one of six states (`Running`, `Completed`, `CompileFailed`,
`Rejected`, `RunFailed`, `ProgramFailed`). The outcome sentence and chips
come from a closed capability-to-family table (`fs.read` is a file read,
`fs.write` a write, `proc.run` and `job.start` commands, `net.*` requests,
`git.add` and `git.commit` repository changes, `lsp.*` code lookups, anything
else an other call, never a guess), and nothing in the card names a path or
host the call record does not hold. Each call row carries a `Bar`, whole
percentages of the program's own span with `offset` in 0 to 99 and `width`
in 1 to `100 - offset` for any input; an unsettled call's bar runs to the
end. A compiler's diagnostic is read into its place, quoted source rows and
caret row, and any text that is not one stays as it came. `Source` says how
much text the host keeps (`OpeningOnly`, or `Whole(cut)`, which `turns` builds
from its `Expansion`), and `opening` says which sections a view draws open
(a failed card opens on its calls or its error, a clean settled one is a
line). `turns.Step.program` carries the card.
- `ImageRow` is an image's placeholder row under the call or turn that
carries it, worded by `image_header.describe` (`image 1 · image/png ·
1200×700 · 84 KB`); `image_header.dimensions` reads the pixel size from a
PNG, JPEG or GIF header totally, and answers `None` for anything else.
`ImageRow` carries `image_header.picture`, which is `None` when the header
cannot be read: a fingerprint (byte count and three 32-byte samples of the
base64 text, cheap on a large image), the media type, the pixel size and
the byte count. A line is a cache key, so it never holds the data; a host
that draws the image finds the data again from its entry by fingerprint.
- `transcript_lines.Presentation`: everything the line builders read of a
client's state. A host fills it; the terminal does so in
`tui_model.presentation`.
- `transcript_lines.assistant_terminal_lines` draws an aborted response as
`Stopped`, with its diagnostic beneath it as dim detail unless it says only
that the provider's request was cancelled, which is what a Stop or a steer
does and which the records do not tell apart (F153, protocol-change/071).
- `child_stop` joins captured `child-run/{operation}` facts to exact aborted
results and assistant entry IDs. `transcript_lines` and `agent_view` show
the recorded parent completion or child budget expiry beside the provider's
cancellation diagnostic. Missing, malformed or foreign-strand metadata adds
no reason. The gateway captures this metadata for each strand's latest
terminal operation; older history without a captured result stays generic.
- `transcript_lines.collapse_repeats` folds a run of identical consecutive
items into the newest of them with `×N` on its first row, in compact
history only. Two predicates say what may fold: `repeated_call` (a call
that settled successfully on one row, such as an `agent_wait` poll) inside
a tool group, and `repeated_failure` (an entry that draws only a provider
error) between items. The terminal's anchor fold in `tui/projection`
applies the same fold over the same items, so rows and anchors stay
paired; a folded run anchors to its newest call. A failure row opens bare,
so it gets a gap under a call's bare last row.
- `transcript_lines.response_awaited(records, operations, stream)` says
whether a live response is still owed to a host that draws only captures:
its request's identity names an entry that `records` do not hold and the
capture still shows its operation running. The web view keeps drawing the
stream it last saw while this holds (`web_view/component.streamed`), since
a pushed entry clears the record's streams before the capture that holds
the row. `transcript_lines.line_count(text)` is the `2 lines` a live
reasoning row says, for a host that draws the elapsed time itself.
- `transcript.project(cut, view, strand)`: one strand's lines from one
capture, for a host that keeps no presentation state between cuts.
`transcript.project_rows` gives the same lines as `Row(key, line)`, each
keyed by the durable sequence it was drawn from
(`transcript_lines.keyed_record_lines`), so a view that keys its list
drops the rows a moving window loses instead of rewriting the rest.
`transcript.branch_blocks(branch, cut, view, strand, notices)` draws the
same blocks from a branch a host keeps across captures, and
`transcript_lines.block_seq(block)` reads a block's sequence back from
its key.
- `operator`: what an operator's input becomes on the wire, shared by the
terminal and the web view. `submit(lane, id, strand, text, Delivery,
now)` sends a `Prompt` or a `Steer`; `decision` and `decide` encode an
answer (`Choice`: `AllowOnce`, `AllowForSession`, `Deny`) that echoes
the drawn escalation exactly; `drawn` finds a pending record only by its
identity and the sequence it was drawn at; `drain(state, budget, take,
handle)` is the bounded, oldest-first loop both hosts feed the lane
with.
- `text_hygiene.multiline` and `single_line`: the terminal-safety pass
every untrusted string goes through. `unchanged_prefix` is how many
leading bytes the pass leaves as they are, with the guarantee that the
pass over the whole is that prefix followed by the pass over the rest;
the terminal's live tail uses it to sanitize a growing answer once
rather than on every frame.
- `code_tokens.line(language, text) -> List(CodePart)`: the token classes of
one line of a code fence (`CodeKeyword`, `CodeType`, `CodeString`,
`CodeNumber`, `CodeComment`, `CodePunctuation`, and a diff's `CodeAdded`,
`CodeRemoved`, `CodeDiffMeta`). A `gleam` tag is scanned, a `diff` tag
classifies the whole line, any other tag is one plain run. Total, per line
(no state across a line break), and the tokens joined are the line
unchanged. The terminal maps each kind to a style in `tui/markdown`, the
web view to a `tok-` class in `web_view/code_view`; it holds no
`@external`, so R6 stays clean.
- `markdown.parse(text) -> List(Block)`: an answer's Markdown as a closed
tree (`Block`, `Inline`, `Cell`, `Level`, `Align`, `AlertKind`,
`TaskState`) whose every string is text to show. Both hosts draw it: the
terminal's `tui/markdown` as etui spans and the web view's
`markdown_view` as elements. HTML in the source is text and a link keeps
its destination as a string. Besides CommonMark's core it recognises
GitHub alerts, task boxes, pipe tables, footnotes, reference links and
bare `http`/`https`/`www.` links; emoji shortcodes, entities and inline
footnotes stay text. It is Loom's own parser because mork, which the
terminal used before, backtracks exponentially on a run of `[`. This one
is linear in the input and caps nesting at `max_depth` containers and
`max_emphasis` open delimiters. Reference and footnote definitions are
collected in a first pass over the lines, into a map, before any block
is parsed.
- `turns.pieces(blocks, strands, latest)`: one strand's lane as turns for a
host that draws more than rows (the web view): `Plain` blocks, `Prompt` for a
person's message (the sender is a field, not a `name:` line of the text;
`turns.authors` reads each principal's role from the presence rows and
`turns.attributed` sets it on that principal's messages, never the reader's), one
`Work` divider per turn (`Folded`, or `Open` while the strand runs or waits
on an approval; a host that lets the reader open a fold sets `Unfolded(hidden)`
through `fold_budget.draw`, and `Work.id` is the sequence of the first record
the work folds, the number the host names the fold by; its `Worked` figures come from the records, failed calls included, which
`turns.divider` prints as `· 1 failed`, and `Worked.ending` is `Interrupted` when a response of the turn was aborted (its row leads with `transcript_lines.stopped_words`) or when a command it ran says the broker stopped it on request (`cancelled` without `timed_out` in a tool result's details, which `bash` records), which the divider prints as `· interrupted`; a result whose call lies outside the
window counts as one step, so a turn cut inside a long run of calls shows a
figure that grows as older rows load). A fold's steps read by `turn_ledger.steps`
never hold a result without its call: a read that stopped between a batch's
calls and its results leaves those results out, and the count of steps not shown
is the divider's less the steps drawn. A stop's diagnostic is worded for the
reader (`transcript_lines.assistant_terminal_lines`): nothing beneath `Stopped`
for a stop the provider confirmed, one plain sentence for a stop it could not
confirm, and never the harness's note of where it observed the stop. A response that failed says why on a
`Plain` row beside the divider, never inside the fold. The fold's
items are `Narrated` blocks (each with `took`, the response's time, which a
reasoning row reads), `Step`s (`words` from `step_words.of_call`) and
`Memory`, the memory context the daemon recorded ahead of a prompt: it is no
input, `split` and `grouped` hold it for the next input, and it is the first
item of that turn's fold. `Spawned` and
`Returned` rows for sub-agents, `Nudged` for a delivered advisor frame,
`Peer` for another session's message, `Sibling` for a message a strand of
the same session sent (stored origin `StrandOrigin`, framing removed by
`strand_framing.strip`, a brief's result-contract trailer kept apart; `Peer`
and `Sibling` keep the whole text and the host shortens it when it draws),
`Missed` for a cache notice and `Commentary` for the advisor's board (reviews that
stand next to each other are one piece with a `reviews` count). A feed or goal
feed sent to the advisor is an input, since the advisor's strand has no other:
with none its whole history would be one turn with no start, and a host that
completes a turn by reading back to its input would read all of it. It reads
`transcript_lines.keyed_record_blocks` (`transcript.blocks`), which tags
each block with its `Source`. `turns.grouped(blocks, strands)` splits
the same blocks at their inputs, the lead before the first input and
then each turn, so a host that holds only the newest turns cuts between
them. `turns.hue` is a strand's colour from its position, never its
name. `turns.pieces` takes an `Expansion`: `Skip`, or `Expand(cap)`, where
the host's `cap` cuts what a reader who expands a row sees, the rows the
terminal's `Ctrl+g` draws (`transcript_lines.expanded_call_lines`, and
`assistant_block_lines` at expanded extent for a reasoning row). A `Step`
carries the cut rows as `full`, and a `Plain` or `Narrated` piece carries
`thoughts` by row key; both are empty when the expansion equals the compact
rows, so no piece holds uncapped text. `grouped` skips them. The terminal
does not call `turns`.
- `step_words`: how one step of a turn reads, shared by every host that
draws a step. `of_call(call)` turns a tool's name, arguments and (for an
edit) its diff into `Words(verb, subject, change)`: `Read calc.py`,
`Edit calc.py +3 −1`, `Ran python3 -m unittest`, `Spawned scan`. A subject
is tagged `Mono` (a path or command), `Prose` (a name or purpose), `Figure`
(a count or a time) or `Unnamed`, which only says which face a host uses;
every subject is session text and is drawn as a text node. A tool the table
does not list keeps its own name. `failure_sentence(words, engine)` is the
one plain sentence a failed step opens on (`The edit was rejected: "from" is
required for this hunk op.`) and `spoken_breaks(report)` reads the two
characters `\n` of a double-escaped report as line breaks. `memory`, `reasoning`, `worked`,
`returned` and `duration` word the rows that are not tool calls, `text`
flattens any `Words` to one line, and `first_call(program)` names the first
capability a `code_mode` program calls (`fs.read calc.py`) by reading its
text. That reader stands in for the trace view's fold of a program's calls.
- `diff_view`: a unified diff read into lines a host can colour. `parse(diff)`
splits on newlines (a CRLF's `\r` is dropped), keeps at most `max_lines`
(400) and returns `Diff(lines, cut)`; `of_lines` reads lines a host already
bounded. Each `Line(kind, old, new, text)` has a closed `Kind` (`FileHeader`
before the first hunk only, `Hunk`, `Added`, `Removed`, `Context`,
`NoNewline`) and, inside a hunk, the line numbers the header's counters give.
An added, removed or context line's text has its marker removed. The text is
session text; the web view draws it as a text node.
- `transcript_image`: the images a lane row carries, which the rows draw as
`[image <type>]` text. `Image(mime_type, data)` holds the entry's own base64
text (nothing is copied). `of_entry`, `of_message`, `of_outcome` and
`of_block` read a user message's `UserImage` blocks or a tool result's
`ToolResultImage` blocks, and `ref(key)` names a row as one path segment (a
step key's `/` becomes `-`). `turns.Step` carries `since`, the record time
of a call still running (from `tool_activity.Call.asked`), so a host can show
how long it has run, and `turns.abridged(block)` and `turns.abridges(pieces,
key)` find a user message whose row shows less than it said, with its whole
text read from the block's entry, so a host opens only a row the lane draws.
`turns.Step` carries `images`, `turns.pictured`
lists the rows that carry any by name and `turns.picture(pieces, ref,
position)` finds one, so a host that serves an image serves only one the lane
draws. `pasted_image.media_type` (PNG, JPEG, GIF or WebP by magic number, the
terminal's own sniff, which `tui/image_drop` delegates to) and
`pasted_image.is_raster` are the allowlist both hosts share.
- `agent_view.Row`, `agent_activity` and `reviewer_status`: each strand's
status, task, activity and approvals from one capture, shared by the
terminal's agent rail and strip and the web view's chips.
`agent_roster.{Roster, Line, Chips}` is which strands a strip lists, in
what order, with elapsed time and context size (`lines`, `chips`,
`running_ms`, `context`). A strand that is idle and has no operation has
never run (a fresh fork waiting for its first prompt), and it is listed
among the live cards rather than settled; one that ran and is idle again
has an operation and is settled. `describe` gives any row the same line
whether or not a strip would list it; the terminal's workspace list uses it.
`agent_view.catalogue_name(view, strand)` is the catalogue entry a strand's
configuration names (`ModelIdentity.provider`), the owner's own name for the
model, where a row's `model` is the upstream identifier; the web header and
its cards word the model with it.
Its internal `listed_count` uses the same membership predicate without
constructing display lines, for hosts measuring geometry. The roster test
compares that count with `lines` across every status and active-strand choice.
- `notice_words` (`sent`, `outcome`, `done`): the closed table that words a
command's outcome for the footer, `Goal pinned`, `Denied`, `Queued for the
next turn`, so no wire name is a notice. `reviewer_status.lines` words the
advisor row `watching <strand>` and cuts a sub-agent's brief to its first
sentence; `without_idle_advisor` is the page's filter.
- `decisions` (`from_ledger`, `strands`, `words`): the approval decisions the
approval ledger holds, with the author, the verdict and the strand the
request was raised on (from the pending cell the capture held, or the
record's own scope when the page read `escalations_decided`, whose answer
arrives as `LookedUp(records, [])`). `turns.with_decisions` places each as a
`turns.Decided` piece by the register sequence that committed it, which
storage numbers from the same counter as transcript entries.
- `approval.wants(tool)`: the fixed words for what a request asks to do
(`run a command`), shared by the cards.
- `cache_miss` (a miss reconstructed from two usage rows, and the TTL
outlook the rows prove) and `cache_watch.Ledger` (which rows may be
compared: `admit`, `settle`, `capture`, `observe`, `forget`, and `shown`,
which suppresses the outlook while a strand runs): the prompt-cache rules
both hosts draw.
`shared_set`: one setter per `Shared` field that three or more sites set. A
reducer that changes one field pipes the record through the setter instead of
writing `Shared(..shared, f: x)`: `Shared` has about ninety fields, and each
record update cost erlc roughly 10 ms (`tui/CLAUDE.md` has the measurements).
The shared step. Every function here takes and returns `model.Shared` alone
and reads no host state; a host stores each result and applies what it
recorded (the terminal through `tui_model.hold_shared`, `run_shared` and
`inbound.run_settled`).
- `model.Shared(socket, recorder, source, replay_source)`: the session
state, what a host needs to show a session and act on it (what the daemon
said, what was sent and not yet committed, the reads in flight, the
presentation revisions), and four host handles typed by its parameters:
`channel` is `Option(session_channel.Channel(socket, recorder))`,
`replay_state` is `attempt_replay.State(socket, recorder)`, `inbox` and
`replay_inbox` are `inbox.Inbox` values keyed by `source` and
`replay_source`, and `recorder` is `Option(recorder)`. The inboxes have
separate source parameters because the terminal reads them from
differently typed subjects. The terminal binds the four in
`tui/model.TerminalShared`. Its types are `Peer`, `Interrupt`,
`SubmissionSource`, `UnconfirmedSubmission`, `ConnectionBacklog`,
`GoalReport`, `ReturnedDraft`, `GoalObservation` and `SurfaceFact`.
`Shared.outbox` holds the `step_effect.Effect` values a call decided,
newest first, and is empty between calls. Four fields record what a call
means for a host's editors and surfaces, so that the call need not write
them: `drafts_sent` counts the composer drafts the lane has sent,
`queue_notices` holds `queue_request.Notice` values (`Refused(reason)`,
`Dropped(message)`, `Received(owner, namespace, document)`, `Saved`,
`Unknown`), `goal_observations` holds `GoalObserved(board)` and
`GoalUnavailable(reason)`, and `surface_facts` holds the `SurfaceFact`
values the folds and the commands recorded (`WorkspaceSwitched`,
`SessionSynchronized`, `ModelsListed`, `OutlookCleared`, `NotesArrived`,
`JobsReplaced`, `LookupAnswered`, `ApprovalSettled`, `ApprovalsPresented`,
`QueueRowsCaptured`, `HistoryReleased`, `AgentMessagesCaptured`,
`GoalReleased`, `ConnectionLost`, `ReplayAdopted`, `InterruptRequested`,
`ReviewAnswered`, `DraftTaken`, `TranscriptCleared`, `LookupRequested`).
The three lists are empty between calls. `Shared.answer` is the words of
the daemon's latest reply to a command the lane sent (an acknowledgement,
a refusal, a lost reply), which `lane_fold.apply_channel_update` writes
beside `notice`; `notice` is replaced by any event and `answer` only by
another reply, and a refusal of a read the host issued itself (`session_channel.is_read`,
`history`, `escalations_get`) is not one. The terminal reads `notice`; the web page reads `answer`.
`build_notice` is the
build-mismatch lines the host computed when it adopted a daemon, which a
cut splices in; the builds themselves are the host's, because
`host/build_identity` reads the environment. The writers are
`append_system`, `append_error`, `append_notice`, `invalidate_transcript`,
`invalidate_frame`, `mark_activity` (bumps `activity_revision` only),
`hold_channel` (stores the lane, queues its outputs as `Lane(..)`),
`record_arrival` (queues `Recorded(..)`) and `record_surface`; the
readers are `queue_owner`, `queue_namespace`, `active_strand_live`,
`active_strand_phase`, `active_interrupt`, `is_known_strand`,
`strand_running` and `presentation`, which builds the
`transcript_lines.Presentation` the line builders read.
- `step_effect.Effect(socket, recorder)`: the two effects the session
decides, `Lane(output)`, an output of the adopted lane, and
`Recorded(recorder, message)`, the recording line for a message that
arrived with no lane. It is a module of its own rather than part of
`step` because the record holds a queue of these and `step` imports the
record.
- `msg.Stamp(now_ms, transport_ms)`: the instant an input is applied at,
stored as `Shared.stamp`. `msg.Command` is what an operator does to a
session from any host: `Submit(draft, command, delivery)`,
`Control(command)` (a session command chosen by a button, which has no
draft), `Interrupt`, `Stop(strand)`, `Decide(review, choice)`,
`SelectModel(name)`, `Quit`.
`msg.Msg(source)` is what a host with no surfaces hands `step.update`:
`Arrived(List(Arrival))`, traffic to file (`Arrival` is `Frame(source,
message)` or `Replayed(event)`), or `Input(at: Stamp, event: Event)`,
one event to reduce (`Event` is `Ticked` or `Acted(Command)`).
- `admission.file_frame` and `file_replayed`: a frame is filed into the
adopted inbox when its source is that inbox's and refused otherwise, and
a replayed event is always filed. What a refused frame is, the host
decides; the terminal offers it to its waiting attempt.
- `outbound`: `send_frame`, `send_via`, `apply_submission`,
`discard_own_turn`, `waiting_notice`, `mutation_refusal` and
`mutating_submission`. A sent `ComposerSubmission` empties `attachments`
and bumps `drafts_sent`; a frame the lane did not send appends
`queue_request.Refused(reason)`.
- `surfaces`: the `service_*` reads (todo seed, notes, queue, worktree,
jobs, advisor nudges, block summaries, goal, context), the receivers
(`receive_jobs`, `receive_goal`, `receive_advisor_nudges`,
`retire_delivered_nudges` for a pushed entry,
`retire_nudges_delivered_since_board` for a captured cut, `refuse_goal`),
the `sync_*` edges with
`context_refresh_due`, `context_usage_due` (a provider usage row landing on the
live active strand, at most once per `usage_refresh_interval_ms` of the
selection's `context_view.State.marked_ms`), `advisor_nudges_action` and `goal_action`, which
compare two records, and the goal commands `submit_goal_action` and
`confirming`. A dropped queue read appends `queue_request.Dropped`, and a
goal board or failed goal read appends a `GoalObservation`. The functions
that open a surface or read its target are the host's
(`tui/side_surfaces`).
- `event_fold.apply_event`: one pushed event (streams, tool tails, entries,
phases, usage and the cache watch, the side-surface replies, schedules,
skills, models, a returned draft), with the functions it reaches:
`select_workspace` (the session half of a switch, with `leave_session`),
`select_model`, `settle_pending_cache`, `send_prompt_to`,
`expect_own_turn`, `settle_own_turn` and `abandon_interjections`.
- `lane_fold`: `apply_channel_update(shared, update, around)` folds one
`session_channel.Update` (`reconcile_cut`, `render_cut`,
`receive_history`, the approval lookup, `apply_request_refused`, the
acknowledgements, a lost lane); `receive_unlaned` applies a channelless
message; `apply_replay_change` applies one change of a replayed attempt
event. `tick`, `receive` and `cancel_unsent` hold the lane and return its
updates, and `take_replayed` returns a replay event's changes, so the host
keeps the loop. `Surroundings(worktree, notes, reviewing, wanted)` is what
the host shows that a decision inside an update reads; `nothing_shown()`
is a host that shows none of it. Also `request_decisions`, `apply_cut`,
`service_history`, `request_visible_worktree`, `refresh_worktree`,
`approval_lines` and `retains_history`.
- `commands.act(shared, msg.Command)`: `Submit` runs `submit`, the refusal
before encoding, the `ComposerSubmission` marker, the image prompt and
the dispatch exhaustive over `command.Session`, then
`release_submission`; `Control` runs `control`, the same refusal and the
same dispatch with no marker and no `DraftTaken`, so a lane that sends
its frame moves no `drafts_sent` and a host has no editor to empty, which
is what a button that fires while the operator is typing needs; the others run
`interrupt_active`, `stop_strand`,
`decide_review`, `select_model` and `quit`. `decide` (by ID) is the
`/approve` and `/deny` arm, and `focus` and `load_strand` are the second
and third units of a strand switch, after the lane's `cancel_unsent`.
- `generation_rate.State` owns one request clock and latest measured rate per
strand in `Shared.generation_rates`. Live phase events and coherent captures
observe every strand, including background agents. A first capture or fragment
of an already running request opens a partial window, which can show elapsed
observation time but cannot divide the full output count. Settlement consumes
that strand's clock; late fragments and repeated captures cannot restart it.
Stream identity survives active-only transcript projection, so a lagging
capture cannot upgrade a partial window. Fresh captures observe phases after
retiring recorded streams. Focus only selects the reading through
`model.generation_started_ms` and `model.output_rate_tps`. Roster replacement
prunes removed strands, and session replacement discards the ledger.
- `step.settle(before, after)`: the shared step's own settle after every
event, `sync_context`, `sync_advisor_nudges` and `sync_goal` in that
order. Beside it are a tick's two session units besides the lane's:
`service_reads`, the eight side-surface reads in the order they share the
lane's command slot, and `advance_activity_clocks`, which moves the
activity and generation clocks to the stamp. The terminal imports the
module as `session_step`.
- `step.update(shared, msg.Msg(source))`: the whole-event entry for a host
with no surfaces of its own, which is the web view (S5, question 12 of the
design note). `msg.Arrived([Frame(source, message) | Replayed(event)])`
files through `admission` and reduces nothing. `msg.Input(stamp, Ticked)`
runs the terminal's tick less its surfaces, in its order: the activity and
roster clocks, the drain of every held frame (each lane update applied on
its own with `nothing_shown()`), `service_reads`, and the lane's tick with
its history read; it then settles against the record it started from and
drops the facts such a host has no surface for (`forget_surfaces`: the
surface facts, queue notices and goal observations; a returned prompt is
the prompt's last copy, so it stays in `returned_drafts` for the host to
take and empty, as the web view does for the composer's element). The
terminal's block-summary read is left out, because the daemon may run a
summarizer for a label. `Input(stamp, Acted(command))` runs `commands.act`
and settles, and leaves the command's facts for the host to read
(`DraftTaken`) before it calls `forget_surfaces`. A tick with no lane
drains nothing. The result is the record and the effects, oldest first.
The terminal does not call it; `step_test` holds it to the terminal's
order. `step.new(strand, session, stamp, inbox, replay_inbox)` builds the
record such a host starts from, with no lane and `peer: Disconnected`;
the host adopts a lane with `hold_channel` and sets `peer` to `Attached`.
`step.focus(shared, strand, stamp)` is the same host's change of strand:
the terminal's `switch_active_strand` less its surfaces (`cancel_unsent`
with each update applied, `commands.focus`, `commands.load_strand` with
`nothing_shown()`), settled against the record it started from and with
the surface facts dropped, returning the effects. The caller checks that
the strand is listed and not already active.
`Shared.ended` is why the adopted lane failed, set by the lane fold; the
terminal never reads it and clears it when it adopts a lane. The step's
functions are `@internal`, so they are not in `gleam export
package-interface`; the hosts in this repository call them, and R6's check
of the imports is what covers them.
- `queue_request.State` (the queue editor's lane correlation),
`agent_messages` (provenance-checked inter-agent sends),
`attempt_replay.State(socket, recorder)` (the two-slot replay of recorded
lane updates) and `completion_summary.State` (an operation's completion
evidence) are the pieces the record holds that came with it from the
terminal.
- `trace_view.fold(records)` folds a strand's records, as a branch holds
them newest first, into the session's `code_mode` programs, oldest first:
each with a closed `State` read from the result's `status` word (`Running`
while it has no result), a label (the `program_path`, else the text of the
program's leading `//` comment after any imports, else `Program N`), a result
excerpt, the `within_ms` the call named, a closed `Vetting`, `detail` (the
result's own `detail`, the compiler's diagnostics or a run's reason, kept
apart from the excerpt, which for a failed build is the sentence written for
the model; the web Trace tab draws `detail` and `budget_words`, the terminal
keeps `excerpt` and `budget_line`), and `calls`, the
rows of the protocol-change/060 call record the result carries (`CALLS · …`
and one row per call, from `transcript_lines.call_section`; nothing for a
running call or a result with no readable record). `newest(records, strand)`
is the terminal's form: one strand's newest program from a window that holds
several strands, in entry order whatever order the records arrive in, with
`source`, the program's opening twelve lines numbered. `state_title` words a
state as the transcript's failure block does (`compile error`, `refused by
vetting`), and `state_word` is the web's. `first_call` is a program's first
call row, else its label. Bounded: `max_programs` 12 (older ones counted in
`omitted`) and `max_characters` 160 per label and excerpt, both single-line
and free of control characters. Both hosts draw it: the web view's Trace pane
from `fold`, the terminal's Trace tab from `newest`. Portable, no externals.
- `changes_view.fold(records)` folds a strand's records, as a branch holds
them newest first, into the board of the session's own edits: the files
the successful `fs_edit` results named, each with the diff the result
reported as rows of a closed `Kind` (`Hunk`, `Added`, `Removed`,
`Context`) and the `+` and `-` totals, and the successful `fs_write` calls:
a write reports no diff, so its file is one hunk whose every line is added,
from the call's `content` argument, with `origin: Written` and the words
`written · 23 lines` (`counts_words`) in place of counts. A file that was
also edited is `Edited` and counts both. It reads no worktree, so it is what
the agent wrote in the window and not the state of the tree, and it says so
(`label`). It is bounded (`max_files` 24, `max_file_rows` 200, `max_rows`
600, `max_row_characters` 240) and every cut is counted. The web page's
Changes section draws it; the terminal's `/diff` still reads the worktree.
- `session_summary.jobs(board, strand)` and `session_summary.viewers(captured)`
are the Session summary's two rows that a read or the presence roster
supplies. Jobs are the `live_jobs` board for the strand asked about, its
lines cut to `max_job_rows` with the rest counted, or `Unread` when there is
no board or it names another strand (never a count of zero). Viewers are the
cut's presence rows grouped by principal, at most `max_viewer_rows`, each
with its role words, how many pages (attachments) it holds and whether one is
the host's own; `total` still counts attachments. Whether a host shows the
viewers is the host's choice: the web page shows them on an operator's page
only.
`turn_ledger` keeps a closed turn as what a page draws of it instead of its
records (protocol-change/071). `seal_all(groups, after, records, strands)`
closes the turns a host holds whole into `Sealed`: the pieces with the fold
closed, the `fold_budget.Weight` (the fold's rows capped at `fold_rows`), the
first record's sequence and parent, the last record (`end`, an `Anchor` of
identity and sequence, which is the next turn's first record's parent, or the
newest record when no open turn follows, so `after` names the turns that stay
open) and the `changes_view`, `trace_view` and newest tool result the turn's
records carried, and `bytes`, about how many bytes of text the summary holds
(`fold_budget.sealed_bytes`, 16 MiB, is the budget a host holds closed turns to
beside its rows). A lead's divider is keyed `work:<first seq>.0`, not by the
window's start. A turn is sealed once nothing more will be added, which the
records cannot always say (an operation can lag the record that opens its turn,
and a turn can go on with no new input); records that arrive after a sealed turn
with no input of their own are read as the end of a turn whose start the host
does not hold and the whole turn is sealed again in place of the partial one
(module doc, "When a turn is sealed"). `older`, `completed` and `steps` take what a scan has read so
far and a `Source` (`Readable | Exhausted`) and say whether it is enough:
`older` for ten whole turns below the host's oldest, `completed` for the turn a
window began inside (`Whole(sealed)`, or `Partial` when the scan ended before the
turn's input, which closes nothing), `steps` for the newest `fold_rows` rows of one turn's
steps (`Steps(items, unread)`; `unread` is the cut's and, when the read stopped
before the turn's input, the divider's count less the steps shown). `lead_end`
is where the blocks before a window's first input end, which a read of the rest
of that turn starts from. `changes_view.append` and `trace_view.append` join the
boards of two stretches so a host can fold each turn once. `turns.Reading` is a
fold the reader opened whose steps are not here yet.
`fold_budget` decides how many rows a host that retains what it draws spends on
turns: `weigh` costs one turn's blocks (its closed rows, and the rows its fold
would add), `fit` takes the weights newest first with the open folds and the
limit and says how many turns fit and which open fold must draw fewer steps, and
`draw` empties a closed fold's items and cuts an open one's to the newest that
fit. It is pure and portable, and the web view's `component.held` is its one
caller (protocol-change/070); `newest` and `item_rows` are public so a host that
reads a fold's steps keeps no more than a page draws.
`turn_labels` draws the summarizer's labels on the finished pieces
(`apply(pieces, labels)`): each reasoning row of a `Plain` block or a `Work`
item whose content block has a label (`transcript_lines.labels_for`: the stored
label, or the live label carried over to the entry's first long reasoning block)
becomes a `SummarizedReasoning` row, with the row's key and the block's full
text (`thoughts`) unchanged. It is applied when the lane is drawn and never
stored in a piece, so a label that arrives after a turn was sealed
(`turn_ledger`) or after a fold's steps were read shows with nothing re-sealed.
`keys(pieces)` lists the long reasoning blocks of the same pieces for
`block_summary.want`. `step.update`'s tick runs `surfaces.service_block_summaries`
for a host that has marked blocks wanted.
The remaining modules are the pieces those decode or fold through:
`approval` (exact escalation decisions), `advisor_history` and
`advisor_pending`, `block_summary` (summarizer labels), `command` and
`skills`, `composer` (the rules for showing pasted text, and the composer's
attachment list; it also recognises the memory context the daemon attaches to
a run, `memory_context_lines`, and folds it in `transcript_text` to `memory
context (n lines)` beside the `[loom]` injection collapse, and owns the
attribution lead and fence `client/memory` builds its text from), `context_view`, `file_read_view`, `goal_view`,
`live_jobs`, `notes_view`, `pasted_image`, `queued_input`, `remembered` (what
the session remembers for its operator, protocol-change/073: the board's total
decoder, the words both hosts use for a row and `Forget`, the request a forget
becomes; `Shared.remembered` holds the last board and `remembered_refresh` is
the page's owed read, serviced by `surfaces.service_remembered_read` and
unused by the terminal),
`stream_identity`, `text_hygiene`, `todo_board` and `tool_activity`, and
`worktree_view`.
## Relationships
- **Depends on**: `core` (json, codec, entries, messages, ids, todo_list),
`machine` (`codec`, `operation`, `strand` for decoding register cells),
`gleam_stdlib`. Nothing else, by R6.
- **Depended on by**: `tui`, the terminal host, and `web_view`, the second
host: the Lustre server components the daemon serves with `loomd --ui`,
which run the shared step (`step.update`, `commands`, `model.Shared`) and
draw from its record with `transcript.branch_blocks`, `turns` and
`agent_roster` (ADR-014 and its addendum). `client` depends on it at
runtime too, for the
`connection_event.Message` its web-view relay sends and the
`snapshot.Expected` its page socket builds, and its tests drive both
hosts' lanes.
- **No module here imports `tui`**, and none may: the package is below the
terminal in the dependency graph. A terminal type the shared step would
need is cut at a type parameter (the four handles) or left in the
terminal, with the step receiving it as data (`Shared.build_notice`) or
from the host at the call (`lane_fold.Surroundings`).
## Traffic
None of its own. The package defines no actor, message or register. It
decodes the ClientGateway protocol (spec Part 1.6, `docs/client-protocol.md`)
frames a host hands it, and the frames it asks the host to write are
`subscribe`, `snapshot_next`, `catch_up`, `history`, `escalations_get`,
`escalations_decided` (`session_channel.decided`, sent only to an idle lane) and,
through `submit`, the session's mutations. A read-only host never calls
`submit`; the web view's operator page reaches it through `commands.act`,
with any session command a draft names except adding a directory, and its
observer's page holds no command.
## Invariants
- **No host in the package.** No `@external`, no `gleam_erlang`, no
`gleam_otp`, no transport, file or terminal. R6 gates it at error level.
- **A shared call writes no host state.** Where a fold or a command used
to write a host's editor, overlay or footer, it records a fact
(`surface_facts`, `queue_notices`, `goal_observations`, `drafts_sent`,
`returned_drafts`), and the host applies the facts after the call, in the
order recorded, before the next update. A host that holds a result
without applying its facts leaves them for a later call to apply against
the wrong earlier state; the terminal's `tui_test/stepping.step` checks
that every list and `Shared.outbox` are empty after each step.
- **Every output names the handle it acts on.** A frame decided against
one socket is written to that socket even if the host has replaced the
lane since; a note goes to the recorder it was decided under (ADR-013,
phase 1 and phase 2 S3).
- **The lane reads no clock.** Every transition that sets or checks a
deadline takes `now` from its caller.
- **One request owns the wire.** A mutation is issued at most once and a
lost reply becomes `UnknownOutcome`, never a retry (the protocol model in
`protocol/models/terminal-attachment/` checks this).
- **`step.update` runs the terminal's units in the terminal's order.** The
terminal does not call it, so nothing in the terminal's tests would fail
if the two compositions drifted; `test/step_test.gleam` spells the
terminal's tick over the shared record (the activity and roster clocks,
the drain with each update applied on its own, `service_reads`, the lane's
tick with its history read) and holds `update` to the same record and the
same effects, and pins the drain before the reads. A change to the
terminal's tick order (`tui/tick`) is a change to `update` and its test.
Its tick leaves out the block-summary read, and a tick with no adopted
lane drains nothing and keeps the frames, because the terminal's tick
would read them as the preview peer's traffic.
- **`next_due` is exact.** A `tick` before the reading it names changes
nothing and a `tick` at it acts; a host that sleeps until it can never
miss a refresh or a deadline. The property test in
`packages/tui/test/session_channel_property_test.gleam` checks it after
every generated step (N1).
- **`Captured` is the only update that replaces the projection.** A host
that draws anything else from a partial transfer is drawing a cut that
was never validated.
- **The line builders read a `Presentation`, never a host's model.** A
new input to a line builder is a new field on `Presentation`, filled by
every host.
- **`markdown.parse` is total, linear and shallow.** Every input yields a
tree, no character is scanned more than a fixed number of times, and
the tree's depth is bounded, because both hosts parse every answer the
agent writes and the terminal parses a live answer again on every delta.
A new lookahead scan must say why the text it reads is text no other
scan reads, as the label, reference and bare-link scans do in their
comments. `markdown_test` holds hostile inputs sized so that a quadratic
parser would miss EUnit's time limit.
- **Agent prose renders as Markdown in both hosts.** An answer, reasoning,
a `ToolDetail` row, a sub-agent's report, an advisor's body and a peer's
message are Markdown wherever their body is shown; prompts, tool calls,
results and patches are not. The line builders put agent prose in
`ToolDetail` rows (a peer message, an expanded `agent_wait` report) so
the terminal's rule is one speaker set; the web view's result, nudge
and peer cards draw their bodies as Markdown too.
- **`strand_framing` owns the Agency's framing strings, and `strip` runs only
for a stored `StrandOrigin`.** `client/agency` builds `frame_message`,
`frame_brief` and `child_notice` from them, because `client` depends on
`session_view`. `strip` compares exact strings built from the origin's own
strand, anchors a brief's trailer on the last foot plus opening line and the
closing marker at the end, and returns any text that is not exactly the
framing whole. Text never selects the strand rendering: a framed text under
origin `None` or in a tool result stays ordinary input.
- **`turns` keys a turn's work by its input.** `Work.key` is
`work:<input key>`, or `work:window-start` for the turn the window opens
inside, never the key of the work's first item. The window drops its
oldest records as new ones arrive, and a key naming the first item would
change on each such capture, which makes a keyed host replace and redraw
the whole turn (`web_view`'s `lane_memo_test`).
`protocol.GoalChanged` invalidates the goal's auxiliary observation
(protocol-change/056). `session_channel` retains one owed goal read separately
from transcript capture debt and queued operator intent. It clears the debt
when it issues `goal_get`, so a change received during an older read survives
that reply and causes a subsequent read. Both hosts use this lane, and the
result is a correlated `Auxiliary(GoalSnapshot)`; it requires no host timer.
An automatic read also emits `Submission(Sent)` after the older reply's
updates. The shared surface records its request ID so a correlated refusal
clears the retained goal and marks an open inspector's board as stale, while
the automatic read stays silent in the transcript.
`snapshot_view.queue_halted(view, strand)` derives a known idle strand with
pending rows from one authoritative cut. Ordinary input drains before the
idle cut is exposed (protocol 033), so retained rows wait for an explicit
submission. The helper says nothing about whether a composer may submit.
`GoalReport.ConfirmGoal` carries the mutation's issued request ID, absent
while the command waits behind a read. `outbound.record_sent` binds it
only on the mutation's `Sent` disposition. A board or refusal from an older
background or explicit goal read can update the observation and retire that
read's slot, but cannot consume the queued mutation's report. Success and
refusal settle that report only on its own correlated command reply.
`model.active_queue_halted` derives held input for the active composer while
excluding a prompt already submitting or a running strand. The terminal
wrapper calls this shared decision.
## Goal and held-input source navigation
The scoped modules' `## Flow` comments name the local entrypoints and helper
order. `surfaces` declares `NudgeAction` and `GoalAction` before the reads and
reducers that return them. `session_channel` places `flush_queued` before
`read_changed_goal`, matching their priority in `send_queued`, and documents
the actual private `Phase` constructors and guards beside that type.
Keep the field owners explicit: `Channel.goal_refresh` is `Idle | Due`
invalidation debt; `Shared.goal_refresh` is `worktree_view.Requested | Settled`
surface scheduling. `Shared.goal_request` names an issued command, while
`ConfirmGoal.request` names the mutation that may settle the pending report.
The [delivery reading guide](../../docs/architecture/delivery.md#reading-the-held-input-and-goal-paths-in-gleam)
explains the relevant Gleam idioms and traces an older reply, a later notice,
a queued mutation and a correlated automatic read refusal in both hosts.
## Deep Docs
- `docs/adr/013-tui-effects-as-values.md`: why the lane's effects are
values; its phase 3 addendum, "What phase 4 extracts", set this
package's scope.
- `docs/adr/014-second-runtime.md`: one engine, two views, and why
session logic lives here rather than in a host; its addendum on the step
says how the four blockers to moving it were closed.
- `docs/architecture/client.md`, "The client engine and its hosts": the
layering this package sits in, and where each host performs effects.
- `docs/architecture/terminal.md`: the terminal host that drives it.
- `docs/architecture/web-view.md`: the web host that drives it.
- `docs/design-notes/step-extraction.md`: how the step moved here, the
field split, the facts, and the rulings on the host's loop.
- `docs/architecture/delivery.md`: how both hosts deliver traffic to the
lane and wake for `next_due`, traced from the socket to the screen.
- `docs/client-protocol.md`: the protocol the lane speaks.
- `protocol/models/terminal-attachment/`: the P model of the lane, the
attachment worker and the gateway.
## Exact-action watcher approval
`approval.rememberable` admits a singleton public wall-seconds-zero grant for
session consent, in addition to the existing filesystem/full-network forms.
Other limits and mixed requests remain unavailable. `remembered_authority`
states that this consent permits only the captured action on its strand; the
wire still echoes the exact action, grant subset, and displayed sequence.
The client gateway owns persistence and matching; this pure UI grants nothing.
## Usage projection (protocol 081)
`protocol.UsageChanged` separates aggregate usage from `last_usage`. The
aggregate contributes to cumulative consumption; cache watches, output rate,
and context observations use the final attempt. A present malformed final
observation is refused, while a historical omitted field describes its
single usage observation. Captures own network cumulative totals; a pushed
observation is never another adder.
`usage_display.estimate` supplies the same words to conversation bars and
goal inspectors. Unavailable pricing shows `est —`, partial estimates name
their coverage, and ChatGPT plan rates show `API ref`. The goal board carries
`cost_evidence` beside `cost_used`, preserving uncertainty through restart
and replay without introducing a ChatGPT allowance counter.
## Immutable result identities
`turns.Item.Step.result_source` retains the result entry identity independently
of its host-capped expansion. Narrative joins keep the identity alongside the
exact joined outcome; compact groups already carry it in `tool_activity.Call`.
`turns.block_result` reads an orphan result identity from its existing source.
These are portable, pure projection facts and contain no I/O or result reader.
## Profile commands (protocol 082)
`command.ProfileShow` and `command.ProfileSelect` are session commands for
`/profile` and its alias `/model-profile`. `commands.dispatch` sends
`protocol.profile_get` and `protocol.profile_set` (an absent `profile` is the
default roles). The lane classifies `profile_get` as a read and `profile_set` as a
mutation, and both are answered by `protocol.ProfileSnapshot`
(`session_channel.matching_presentation`). `event_fold.append_profile` writes the
current profile and the names, with `default` first, or the switch confirmation
that says the session is restarting. `attempt` accepts both names in a recording.
- ## Held-input withdrawal (protocol 087)
+ ## Held-input withdrawal (protocol 088)
`protocol.remove_queued_input` names one strand, opaque input identity and
expected revision. `session_wire` accepts the `removed` mutation outcome.
`outbound` correlates its request through `queue_request`, and `lane_fold`
reports acknowledgement, refusal or uncertain delivery to the host. The queue
editor consumes these notices before `step.forget_surfaces` clears them; the
web host now uses the same full-document read and replacement correlation as
the terminal. No process, I/O or foreign binding is added here.