asz.view
asz.view is one conversation, rebuilt from its Session Flow and its Session Data, as one document:
everything a viewer renders, and nothing a viewer must compute. It is the final form of a
conversation, the one a page draws and the one a person reads. Every talk is a tree of runs and
steps with the text the referenced records carry, and the streams, the segments, the relations,
the rounds and the files sit beside them, each verified. The evidence is inside it once, so a
viewer never opens a .sd file; the ref on a node is a citation, not a pointer to fetch.
It is never a file that the project writes. A stored copy would have to be kept in step with the
rounds and the landed files, and it would be the copy that falls out of date. Built from them each
time, it always agrees with them. Package pkg/sessionview defines and owns the shape, and three
things produce it from the same code:
| How | What you get |
|---|---|
asz conversation -json ID |
the document on standard output, indented, whole. view.hide applies to the page only, since whoever runs the command reads the storage root itself |
asz conversation -yaml ID |
the same document rendered as YAML, with the same keys in the same order |
asz view, at /api/c/{id}/view |
the document as the page’s own response, built once per fold for each set of names it withholds, less what hide withholds. See Withholding |
A server that holds the same .sd and .sf files, such as the SkyWalking OAP, builds the same
document and answers a conversation query with it. Every reader shares the shape, and a change to
it is a change to the version: a 1.x adds keys and never removes or renames one; a 2.0 may do
either.
The document is JSON. Keys are snake_case, as in the two source formats, and are written in the
order this page lists them. Times are unix milliseconds, rounded down, read from the .sd record a
node references; a view is read and never digested. The one exception is a record the document lists
as its own format writes it, a workspace change or a tool execution, whose time stays the RFC 3339
string the record holds. A time a
round’s header carries is null when the header has none, and 0 when its value is not a time as
Reading a record defines one. The same head
round over the same files gives the same document, so one built by asz view and one built by
another server compare equal as documents, when both withhold the same names.
A complete example. asz-view-example.yaml is the whole document for
the fixture session of the format pages, three talks across a main stream and a child agent, a
tool, a synthetic error and a context reset, exactly as asz conversation -yaml prints it. It is
generated from the scenario tests/scenarios/fixture.yaml by make asz-view-example, and a test
fails when the committed file no longer matches what the code produces. Its workspace_changes
holds the one file the build changed; tests/scenarios/workspace-changes.yaml covers every
producer of them.
Top level
{"format":"asz.view","version":"1.0",
"conversation":"0438c73b-…","sessions":["0438c73b-…"],
"head":{"round":4,"digest":"2e77…"},"parser":"v1","policy":"v1+idle=10m0s",
"summary":{"title":"Check otel-rules support for meter mal","state":"verified","problems":[],
"talks":357,"steps":16121,"streams":132,"segments":50,"rounds":4,"unresolved":0,
"from":1786033245531,"to":1786502191749,
"kinds":{"llm.call":4656,"tool":5606,…},"relation_types":{"in_segment":346,…},"quality":{"exact_unique":338,…}},
"rounds":[{"round":1,"digest":"6d2d…","previous":null,"from_seq":1,"through_seq":5,"input_digest":"…",
"from_time":1786033245531,"through_time":1786322168715,"verified":true}, …],
"files":[{"file":"0438c73b-…/streams/main/transcript-…-000002.sd","format":"sd","kind":"transcript",
"seq":2,"round":null,"stream":"main","run":null,"lines":1017,"bytes":2097000,"digest":"9a39…",
"from_time":1786033290556,"through_time":1786036000000}, …,
{"file":"_conversations/0438c73b-…/rounds/r000001-6d2d93b21f79.sf","format":"sf","kind":"round",
"seq":null,"round":1,"stream":null,"run":null,"lines":3575,"bytes":1026788,"digest":"f3fe…",
"from_time":1786033245531,"through_time":1786322168715}],
"streams":[{"id":"stream/main","name":"main","role":"main","label":"","parent":"","records":10728,
"steps":7310,"talk":"talk/main/95a1…","named_by":"","opened_by":[]}, …],
"segments":[{"id":"segment/at_2_7","talks":24,"from":1786033290556,"to":1786039329174}, …],
"talks":[{"id":"talk/main/95a1…","kind":"talk","parent":"epoch/main/0","stream":"main","at":1786033290556,
"ref":{"seq":2,"row":7},
"label":"for meter mal, …","reply":"## Short answer…","runs":1,"steps":132,"tools":51,
"from":1786033290556,"to":1786033717089,"segment":"segment/at_2_7",
"children":[…],"edges":[{"type":"in_segment","other":"segment/at_2_7","dir":"out","quality":"exact_unique","via":"activity window"}]}, …],
"relations":[{"id":"rel/starts/…","type":"starts","from":"tool/toolu_…","to":"stream/a1…","quality":"exact_unique",
"via":"parent tool result","evidence":[{"seq":1,"row":12}]}, …],
"unresolved":[]}
| Key | Value |
|---|---|
format, version |
asz.view, 1.0. A reader that does not know the version stops here. |
conversation, sessions |
the conversation id, and the sessions that contributed to it, from the fold’s session nodes; one session, equal to the conversation id, for the Claude Code adapter |
head |
round and digest of the newest round the document was folded to |
parser, policy |
from the head round’s header |
summary |
title; state, one of verified, incomplete when a round or a file is missing, mismatch when a digest failed; problems, one line each, empty when verified; the counts talks, steps, streams, segments, rounds, unresolved, changes, provider_bodies, the landed provider bodies, and captured_prompts, the calls whose request is captured; from and to, when the session began and its last activity, from the session node; and kinds, relation_types and quality, the fold sized by node kind, by relation type and by how well each relation is known. withheld is what the document withholds by name, {} when nothing was asked to be withheld. See Withholding |
rounds |
one per round, in order: round, digest, previous (null on round 1), from_seq, through_seq, input_digest, from_time, through_time (the record time range of the files the round consumed, null when none carries a time), verified |
files |
one per .sd file, then one per round: file (its path on the wire), format (sd or sf), kind, seq or round, stream or run, lines, bytes, digest, from_time, through_time, and state, which is removed for a file retention removed from the local root and absent otherwise; lines, bytes and digest still describe the file as it landed. Absent values are null. Together with rounds, this is exactly what a rebuild needs. |
streams |
one per execution stream: id, name, role (main or child), label, parent, records, steps, talk, named_by, and opened_by, every step the assembler could tie to the start of the stream as {step, stream, talk, quality}, in the order those steps happened; several means it did not choose, and neither does a view |
segments |
one per activity window: id, talks, from, to. A window says where the work happened, not whether it has finished; a reader with a clock compares to with the time |
talks |
one tree per talk, in time order. See the node below |
loose |
the runs and steps no talk contains, as trees from their highest such ancestor: a child’s output the fold parented to the session because the child’s stream opened no talk, for instance. Empty for most conversations. With talks, it holds every run and step of the fold within twelve levels of its root, so the document covers the whole session |
relations |
one per relation of the fold: id, type, from, to, quality, via, evidence; in the order they happened, each at the earliest record its evidence names |
unresolved |
one per reference the assembler could not resolve, open or since resolved: id, kind, ref, reason, state |
workspace_changes |
one per workspace change record the session’s files carry, joined to its step. See below. |
tool_executions |
one per tool execution record the session’s files carry, joined to its step. See below. |
Verification is content, not an error. A gap in the chain or a failed digest is written into
summary.state and summary.problems, each round says whether it verified, and the rest of the
document holds whatever could still be folded: the fold stops before a missing or broken round,
head names the last round it reached, and the rounds after the gap are listed and not verified.
A viewer shows the problem; it never gets an error instead of a document. Only a chain with no
usable round at all is an error, because there is nothing to show.
A stream’s opened_by, the relations list and a node’s edges are in the order things happened,
never in the order of an id. Inside one stream or one workflow run, a record’s position orders it:
its file’s seq, then its row, then its block. A position means nothing across streams, since a
child’s file can land before its parent’s, so streams and runs are merged by time: each next item is
the earliest of the next items of every stream, a timed item before an untimed one. Times compare as
the instants they name, as Reading a record says. An item several
records support, such as a relation, happened at the earliest of them by the same rule. Each list is
ordered on its own. An id decides only between items that one record supports, such as two relations
with the same evidence, and ids compare in code point order, as Session Flow
defines it. Talks, workspace changes and tool executions are in time order, as their own sections
say.
A node in talks
{"id":"tool/toolu_01KH…","kind":"tool","parent":"call/msg_011C…","stream":"main","at":1786033295667,
"ref":{"seq":2,"row":13,"block":0},"refs":[{"seq":2,"row":13,"block":0},{"seq":2,"row":14,"block":0}],
"attrs":{"result":"available","result_join":"exact_unique","timing":"unavailable"},
"text":"{\"command\":\"ls …\"}","state":"available","bytes":145,"flags":["finished"],
"name":"Bash","result":"…","result_state":"available","result_bytes":2048,
"request_to_result_ms":1312,"request_to_result_join":"exact_unique",
"edges":[{"type":"starts","other":"stream/a1…","dir":"out","quality":"exact_unique","via":"parent tool result"}],
"children":[…]}
| Key | Value |
|---|---|
id, kind, parent, stream, attrs |
the node as the fold holds it; kind is one of the node kinds of Session Flow; attrs is as the round wrote it, numbers included, without provider_bodies, which a call gives under a key of its own |
at |
when its record happened, from the record; 0 when nothing observed it |
ref, refs |
the record it stands on and every record it covers, as {seq, row, block}, kept so a viewer can show the evidence |
text, state, bytes |
the part the node stands on: its readable text, clipped to the longest prefix of whole characters within 2,000 bytes, the part’s state, available or why not, such as too_large for a payload too large to land, and its full size. For a data part the text is the record’s readable text when it has one: its text parts, or the prompt inside a queued_command envelope, the form a message typed while the agent works arrives in. Otherwise it is the data exactly as the record holds it, which is compact JSON. Earlier writers put \u003c, \u003e and \u0026 there for <, > and &, so their step text can differ (see What data holds). A reader wanting the whole record reads it by address. A node that stands on a whole record of several parts, such as a person’s message with a screenshot beside it, has no part of its own: its text is the record’s readable text, and its state is too_large when a part of the record was taken out, and absent otherwise. A node that stands on a record retention removed has no text, and its state is removed. |
usage, flags, dropped |
what else the referenced record says, copied once: on an llm.call, the token counts in, out, cache_read, cache_write from the one record usage_at names, never a sum over fragments, a count of zero left out; the record’s flags, which on a model call are only those a reader may withhold; and its dropped list, each entry its what and bytes, and why when there is one, so a viewer can say what was left out and why. A field the Session Data page does not list is not copied |
| a talk adds | label and reply, clipped the same way and described below, then runs, steps, tools, from, to, child, segment |
| a tool or agent call adds | name, failed, result, result_state, result_bytes, request_to_result_ms and request_to_result_join, the time from the request record to the result record where the assembler joined them exactly; changes and executions, the records joined to it |
a call to an MCP server has in attrs |
mcp_server and mcp_tool, the server and the tool the runtime’s name for the call addresses, where the name splits exactly (see Parts). They are the runtime’s names; which server ran the call, by its configured name, is in the call’s execution record |
a turn.duration step adds |
duration_ms, duration_measured_by |
children |
containment, in record order: by landed position, and on one record a node on the whole record before the nodes on its parts, which follow their block, then by id: a talk holds runs, a run holds steps, a call holds what it produced. A tree is written down to twelve levels below its root, a talk or an entry of loose, and a node deeper than that is left out. No talk measured has more than three |
edges |
every relation touching the node, in both directions, as {type, other, dir, quality, via}, in the order the relations happened, each at the earliest record its evidence names, so a viewer draws cross-stream flow without searching relations; a workflow launch lists the streams it started in the order its run names them |
Keys a node has no value for are absent, not null. Nothing in a document is inferred beyond what
the fold and the records say. Where the fold says unavailable, the document says it too.
Where the document trims white space from a text, or turns a run of it into one space, white space
is what Unicode’s White_Space property lists. A no-break space is white space.
A call’s result. A tool or agent call’s first reference is its request, and its other
references name what came back. They are read in order until one gives the call a result. From
each, the part read is the one its block names, or else the record’s first result part; a
record with neither gives nothing. The result is that part’s text, or its data when it has no
text. Each part read sets result_state and result_bytes, even when it has neither. failed
comes from the first part that has it, the request’s included.
A talk’s label and reply. A talk’s label is the text of the first message.external step in
it. A talk can have none. A talk opened by a command typed locally is one example, because the
runtime records no origin for such a command. The label is then the first text that is not empty
among the talk’s first three context.injection steps, in record order. One that carries a flag a
reader may withhold is passed over. A text that starts with
{"type":"deferred_tools_delta" is skipped. It is Claude Code’s record of a change in the tools
available, and it says nothing about the work. So a label is not always something a person typed. A
talk’s reply is the last message.assistant or agent.output step in it, in record order. The
earlier messages are what the agent said between tool calls, as the Unified Conversation
Model explains. Either key is
absent when no step gives it a text.
Request to result is not tool time. request_to_result_ms is the time between two records the
runtime wrote, the request and its result, tied together by the tool-use id: the difference of
their times, in whole milliseconds, with the rest dropped. It is not how long the
tool ran. It can include waiting and other work between the two records. So timing in attrs
stays unavailable beside it, for the reason the
Claude Code adapter gives. Both keys are absent when the
join is not exact_unique, when either record has no time, or when the result is earlier than the
request. A gap under one millisecond keeps request_to_result_join and leaves out
request_to_result_ms, because its value is zero.
Workspace changes
workspace_changes lists which files each tool call changed, one entry per change record, in
time order, and at one time the runtime’s record first, then by where each was read. An entry carries where the record was read from and the step it joins to, then the
record’s own fields as changes/1 lists them:
| Key | Value |
|---|---|
step |
the target: the id of the tool step this change belongs to, the one whose tool-use id the record names. Empty when no step carries the id, which is also how an unattributed change appears |
ref |
the source: the landed record the entry was built from, in the shape every node’s ref has. seq names a file under files, row the record in it counting from 1, block the part. A patch the runtime recorded sits on the Edit or Write result record, beside the raw result; a record the plugin wrote sits in a changes file, one record per line |
schema, id, captured_by, session, stream, tool, tool_name, time, basis |
the record’s identity. id is the tool-use id the record belongs to, unique per call, or gap/<root>/<step> for a change no tool window covers. captured_by is claude-code for a patch the runtime recorded on its own editing tool, asz-plugin for one the asz Claude Code plugin observed. basis is runtime_reported, tool_window, unattributed or skipped_read_only |
root, policy, window, outcome, coverage, gaps, overlaps |
where it was observed, under which rules, between which scans, what the call reported, whether the whole scope was seen, and which other windows were open on the root at the same time |
state |
truncated when the record was made smaller to fit its landed file, its hunks or its file list taken out (Too large to land), removed when retention removed it, keeping its identity, its time and each file’s path, operation, sides and lines, with no hunks (Skeletons), and absent when it landed whole. A record whose file list was taken out keeps changed_files, so an empty changes with a positive count means files changed, not none |
changed_files, changes |
the count, null when unknown, and one entry per file: path, operation, before and after as present, bytes, sha256, no_newline_at_end; diff, one of available, binary, too_large, unavailable; attribution and windows, which windows could have made the change; additions, deletions and hunks, each hunk old_start, old_lines, new_start, new_lines and lines prefixed with -, + or a space |
A tool step lists the ids of its records under changes, in the order workspace_changes lists
them, and summary.changes counts them. A step may have an entry captured by claude-code and
one captured by asz-plugin for the same call, with the same id; the runtime’s is listed first,
and a viewer showing one prefers it. A change
several windows could have made appears in each of their records, marked shared and naming the
others, and is counted once. A record with basis: skipped_read_only carries no changes and
means the call was not observed, never that it changed nothing.
Tool executions
tool_executions lists what an observer around a tool call saw it do, one entry per execution
record, in time order, and at one time by where each was read. An entry carries where the record was read from and the step it joins to,
then the record’s own fields as execution/1 lists them:
| Key | Value |
|---|---|
step |
the id of the tool step whose tool-use id the record names. Empty when no step of the document carries the id; the record is kept, not dropped |
ref |
the landed record the entry was built from, in the shape every node’s ref has. seq names an execution file under files |
state |
removed when retention removed the record, which then keeps only schema, id, tool, time and outcome, and absent otherwise |
schema, id, observed_by, boundary, session, stream, tool, tool_name, cwd, protocol |
the record’s identity: id is the observation’s own, so one call can have several entries |
server, time, duration_ms, outcome, arguments, result |
which server ran the call and where its configuration came from, when the observation ended, the time the runtime measured around the call, how it ended, and the size and digest of what was sent and what came back |
A tool step lists the ids of its records under executions, in the order tool_executions lists
them. The records are not counted in summary, and they add no node.
Provider bodies
A call step lists the provider bodies joined to it under provider_bodies: its request, then its
response, each only when the join is exact. An entry names where the body is landed. It does not
carry the body.
{"id":"call/msg_011C…","kind":"llm.call", …,
"provider_bodies":[{"role":"request","ref":{"seq":4,"row":5}},
{"role":"response","ref":{"seq":4,"row":6}}]}
| Key | Value |
|---|---|
role |
request or response |
ref |
the landed provider_body record: seq names a file under files, row the record in it |
A reader loads a body when it wants it. A body refers to pieces and bodies that landed before it in
the same session, sometimes in an earlier file, so a reader loads the session’s provider_body files
with a sequence up to the one ref names, in order, keeps them, and rebuilds the body as
Session Data describes. Every such file is listed under files.
A later body needs only the files not loaded yet. The document stays the size of the structure,
however large the bodies.
The join is not made when the document is rendered. It is made when a round is parsed, and the round carries it, so every reader of a conversation — asz’s own page, and a server that mirrors the format — shows the same bodies on the same calls without opening one. See Session Flow. A conversation whose rounds were parsed before that carries no bodies on its calls; parsing its chain again from the landed files brings them in.
A response joins to the call whose message id it carries, and so does a request that names its
call, when exactly one request names it. A request that names no call, only its prompt and the
request id of the call before it in its chain, joins to the call of a stream whose previous call’s
response carries that request id and whose prompt is the one the request names. The first call of
a stream matches a request that names no previous request. Such a request joins only when exactly
one request and exactly one call carry those two ids. A synthetic call, which the runtime wrote
without calling the provider, takes part in no join. No request of that kind joins in a stream
whose landed transcript lines have a gap, since a call may be missing between two that look
consecutive; responses, and requests that name their call, still join. A body that joins to no call, such as
the request that names the session, a compaction request, or a retried request, is listed on no
step; its file is still under files. Nothing is joined by position or by time.
summary.provider_bodies counts the session’s landed bodies as of the folded chain, joined or not, and
summary.captured_prompts the calls that list a request.
Withholding
An operator who serves a conversation to the people an agent served may need to keep from them what
the runtime sent the model: the system prompt and the tool schemas. The record that carries each is
named by its adapter with a flag, system_prompt or tool_schemas, as Session
Data lists, and a server withholds by those names, never by the text or the
size of a part. asz knows nothing about who is reading. asz view withholds what view.hide in
its configuration says, and by default that is the whole answer: a
request that carries a hide parameter is refused with status 400. With view.hide_override on, a
hide parameter on /api/c/{id}/view, /api/c/{id}/record/{seq}/{row} and /api/c/{id}/files
replaces view.hide for that request: ?hide=system_prompt,tool_schemas, or the parameter given
more than once, and ?hide= with no name withholds nothing. A request that sets no hide is served
what view.hide says. Spaces around a name and empty entries are ignored, and a repeated name counts
once. A name that is not a flag a reader may withhold is refused with status 400. So is any other key
whose letters alone spell hide, in any case, such as Hide, hide[] or hide[0], and a query
that does not parse, since either would otherwise read as no hide at all. A host that serves the API
through its own route, on an instance with view.hide_override on, decides per reader, and sets the
parameter on every request it serves: the document, every record and every file. asz’s own page
passes a hide in its address on to all three, so the Evidence tab never shows what the document
withholds, and shows the refusal of an instance that does not take the parameter. Its links between
the list and the conversations keep it too. That is not access control, because a reader can edit
the address. Two audiences are two instances over the same root, each behind the deployment’s own
authentication, or one such host.
Which records carry the flags depends on the adapter. The Claude Code adapter names two
attachments: the prompt snapshot that Claude Code and a runtime built on the Agent SDK write, and
the deferred tools record, which lists the tools offered on demand with their schemas. See
Claude Code. The LangChain adapter lands the system
message in the provider bodies, which a reader that withholds anything is never served. The tools
arrive in the call’s extra, which is not kept, so they land nowhere. It names the other records
that could hold them: the envelope of a call that arrived before it finished and a root model
call’s first input by what they hold, and a decorated function’s values and a graph’s first input
by their shape. See LangChain.
Only these records are named. A tool’s result is the tool’s own and carries no name, even where it repeats part of a schema, as a validation error can. Other context the runtime injects, such as a reminder or an instruction file, carries none either.
A server that withholds, asz’s own or one that mirrors the format, follows these rules, so a person sees the same conversation on every page. asz builds a withheld document from records withheld as they are read, not by clearing a whole document afterwards. Every field read from a record then follows the rules: a step’s text, a tool’s result, and a talk’s label and reply.
- A model call shows no content of its own, so it carries only the flags a reader may withhold from its record. A call that arrived before it finished can land the request it was sent, and such a call is withheld, and its record counted, like any other.
- A withheld step keeps its node:
id,kind,parent,ref,at,flagsandbytesstay. Itstextis absent and itsstateisomitted. It is never deleted, so every count, the round chain and the verification state still hold. A step that names no single part of a record with several has nobytesof its own. Once withheld, it takes the size of all the record’s parts, so it never says it withheld nothing. - A tool’s result read from a record of its own follows that record’s flags. A withheld result has
no
result, and itsresult_stateisomitted. The step’sflagsandstatestay its own record’s, so a step whoseflagshold a withheld name is one whose owntextis withheld. - An injection that carries a flag a reader may withhold names no talk, in any document, withheld or
not. It is what the runtime sent the model, not what the work is. So a talk with no input of its
own never takes its text as
label. A run journal’s record that carries such a flag names no stream either; a stream’s own label, which the assembler reads from the runtime’s description of the work, is not read from such a record. A person’s input names its talk whatever it carries, and a reader who withholds what it carries sees no label. summary.withheldcounts what was withheld by name: the records in the session’s landed files carrying each withheld flag, as the view last loaded the conversation, which it does again at each new round, and underprovider_bodiesthe session’s landed bodies. A record is counted whether or not a step is drawn for it, since the record endpoint withholds it either way. It is counted once by its id, so a record the runtime wrote again, such as one it replays before a reset, counts once, as the conversation holds it once. A record with no id counts by its position. Every name asked for is listed, a zero included, so a filter that matched nothing still shows that it ran. That is the case on a root landed before the adapter set these flags: its records carry none, and it has to be collected again into a new root to be withheld.withheldis{}only when nothing was asked to be withheld.- No call lists a
provider_bodiesentry,summary.captured_promptsis0, and the files endpoint answers 403. A request carries the system prompt and the tool schemas again, and a body rebuilds to its digest or not at all, so a body is served whole or not at all. The response goes with the request, because a landed file holds both. - The record endpoint returns a withheld record with its envelope and its
flags. Every part keeps its kind and itsbytes, losestext,dataandencoding, and hasstateomitted. It answers 403 for a record of aprovider_bodyfile, as the files endpoint does for the file. A body record carries no flag, and a request’s record holds pieces of the system prompt and the tool schemas. - The files endpoint serves
provider_bodyfiles only, whether or not anything is withheld, and answers 400 for any other. A transcript served whole would carry what the document withholds. A file whose header does not read is refused with 400 too, since its kind is not known.
The scenario prompt-snapshot-withheld checks the document’s rules in both formats. A view test
checks the parameter, the record endpoint and the files endpoint.
Rendering the whole conversation
The document is complete: a viewer draws every view of a conversation from it and fetches
nothing else, however many rounds the session was parsed in. A session landed and parsed in three
stages has three rounds and its landed files cut at each stage, and the document built from the
head holds every round, every file, and every talk, run and step, exactly as one built from a
single parse would; the scenario three-rounds checks that at each stage, and every scenario
checks the property view_covers_the_session at its end. This is how each view reads it.
| View | Read |
|---|---|
| Transcript | talks, in order. Each talk’s label is usually the person’s input and its reply the last thing the agent said in it, as A node in talks defines them; its children are the runs, a run’s children the steps, and a call’s children what it produced: thinking, messages, tools. text is what to show for a step, name and result for a tool, usage on a call. loose holds whatever no talk contains, and is usually empty. |
| Flow timeline | every node of every tree by at, with kind and stream; a node with at of 0 was never observed at a time and is placed by its position. |
| Cross-stream flow | edges on a node, and relations as the whole list: starts from an agent call to the child’s stream, reports from the notification that resumed the parent, ends_with from a stream to the child’s output, result_of from a started stream’s final step back to the call it returned to, follows between epochs across a reset, summarizes from a summary to its boundary, in_segment from a talk to its window. Containment never crosses a stream; a child’s work is under the child. |
| Streams and segments | streams with role, label, parent and opened_by, the step that started each; segments with the span of the talks placed in them. |
| Evidence | a node’s ref and refs, {seq, row, block}, name the record and the part it stands on. The text is already on the node, clipped to 2,000 bytes with the full size in bytes; only a reader that wants the whole of a longer part goes to the record, by that address, in files. |
| Verification | summary.state and summary.problems, every round’s verified, and every file’s digest, lines and bytes. A gap or a failed digest is content here, never an error in place of the document. |
| Counts and time | summary: the counts a list shows, the session’s from and to, and the fold sized by kinds, relation_types and quality. |
Nothing in a document is inferred beyond what the fold and the records say. Where the fold says
unavailable, the document says it too, and a viewer shows that word rather than a guess.
The YAML rendering
-yaml is a rendering of the JSON, not a second format. It is produced from the JSON, so the keys
are the same and in the same order; mappings are blocks; scalars are plain, and quoted only where
YAML would otherwise misread them, so version is "1.0" and a title with a colon is quoted; an
empty map is {}; a text with line breaks is a block scalar. Reading the YAML back gives the same
values as the JSON.
Reading it
asz view serves the document at /api/c/{id}/view and builds it once per fold for each set of
names it withholds, so a second reader of the same names pays nothing until a new round arrives.
asz conversation -json ID prints the whole document, withholding nothing, to standard output, and
-yaml prints it as YAML, for a terminal or a diff. The page draws the same document with Horizon’s
conversation renderer, which asz embeds from a pinned Horizon commit, so a conversation looks the
same in asz view and in the SkyWalking UI. The largest conversation measured, 357 talks and 16,121
steps, is 19 MB as one document and was built in 0.7 s.
A running asz view notices new rounds without being told. On every read it lists the
conversation’s rounds directory and compares the newest round there with the round it folded. When
they differ, it folds again and builds a new document. So an asz view beside a separate
asz collect stays current. The comparison is with the round the fold reached, not the newest one
listed, so a round that was still being written at one read is picked up by the next.
The page writes nothing derived to disk, such as a read index. It folds the chain when asked, because folding was measured to be fast enough. The sample was the largest session of a corpus of 62 Claude Code sessions, 1,100.1 MB of landed records. It holds 53,106 nodes and 922 talks.
| On that session | Took |
|---|---|
| fold the whole chain | 302 ms |
| build the ordered talk list | 1 ms |
| one talk’s subtree, at the 99th percentile | 13 microseconds |
| walk every talk’s subtree | 11 ms |
A read index is added only when reading one talk takes more than 200 ms at the 99th percentile.