Session Data

Session Data is what was actually in a conversation, in one shape regardless of which agent produced it. A landed file has the extension .sd. Its first line is a header carrying everything constant for the file, and its last line closes the file with a digest. Every line between them is one source record, converted, with its content broken into parts named for what they are: a message, a thought, a call, its result.

The conversion happens once, while the source is read, and never again. A runtime’s vocabulary stops at its adapter. A reader is handed parts, so there is no runtime shape for it to reach into. A dialect that meets something it cannot describe keeps the bytes verbatim in an unknown part rather than guessing or dropping them.

The format is public. Package pkg/sessiondata defines it, and a third-party adapter produces it. The schema is sd/1.

One record per line

Every record is one line, for two reasons.

A row is a line. A round addresses a record by the file’s sequence and the record’s line in it, two integers rather than a path into nested structure. They stay valid because a landed file is never rewritten. See Addressing a record.

A reader can decode one record at a time and never hold the whole file. A source record is landed whole however large it is (see Append cursors), so a line has no fixed length. A line reader with a fixed limit, such as Go’s bufio.Scanner at its 64 KB default, stops at the first longer line.

{"h":1,"schema":"sd/1","seq":2,"at":"2026-09-01T12:56:03.1818Z","kind":"transcript",
 "adapter":"claude-code-local/0.1.0","dialect":"claude-code/1",
 "src":"-Users-me-github-skywalking/0438c73b-….jsonl","session":"0438c73b-…","stream":"main"}
Field Meaning
h envelope version, 1
schema sd/1
seq the landed sequence number, monotonic per session
at when the file was collected
kind what it was collected from: transcript, agent_meta, journal, workflow_manifest, workflow_script, changes, execution or provider_body. otlp_log and otlp_span are reserved for a push transport, and no adapter writes either.
adapter how the records were acquired, with its contract version
dialect whose schema they were read as. A push receiver and a local reader for one runtime share a dialect and nothing else.
src the source, relative to the adapter’s source root, with forward slashes on every platform
session, stream, batch the session, the execution stream (main or an agent id), and the group of children a workflow run started. A provider_body file names neither a stream nor a batch

A reader refuses a header unless h is 1, schema is sd/1, and kind, session, src and dialect are all present. The writer in pkg/sessiondata fills in h and schema, and refuses to start a file whose header fails the same check. dialect is required because without it nothing says whose vocabulary the parts were read in. A later reader could not tell a shape it should understand from one it should not.

Because the header holds what is constant, no record repeats it, and a file can be read on its own, away from the directory it was landed in. It names its session, its stream or batch, its kind and its sequence.

src is kept because the storage root does not follow the source’s layout. The root files a session under its id and puts every child stream flat under streams/, keyed by agent id (see Storage Root). For Claude Code, that drops two things the source path holds: the project directory a source file sits in, and the workflow directory a child’s transcript sits in. src is the only place both survive. It uses forward slashes on every platform, so a root reads the same on the machine that collected it and on any other. With a record’s off and sha, it lets a reader check the record against its source for as long as the source exists.

File kinds

One .sd file holds one cut of one source. The header’s kind says which, and the records of each kind carry a characteristic set of fields. A provider_body file is the exception: its source is a directory with one file per body, and each record names its own file.

kind Collected from Its records What refers to it
transcript one execution stream: the main stream or a child agent’s own the conversation itself: inputs, calls in fragments, results, injections, resets. stream on the header is main or the agent id. almost every node of a round
agent_meta what the runtime recorded about an agent: about a child agent when it started it, or, from Kiro, about the session one record, from: runtime, with child and label, and a data part. Kiro’s is the session’s header, in the main stream, with label and no child the child’s stream node, for its label; in the main stream, the session node’s title
journal a workflow run’s journal one record per event, from: runtime, with child and batch; a child_result flag on the record that returns a child’s value agent.output nodes and ends_with relations
workflow_manifest a workflow run’s manifest one record with batch and label, and a data part the run’s name
workflow_script the program a workflow ran one record whose part is unknown: the source is a program, not data nothing; kept because it is part of the session
changes the files the asz Claude Code plugin saw a tool call change, one line per call one record per observed call, with id, tool and time lifted from it and the line whole as one data part, a changes/1 record; no from and no flags, so assembly emits no node for it nothing in a round; the view joins each to its step by tool. A transcript’s Edit and Write results carry the same record as a second data part, from the runtime’s own patch. See the Claude Code plugin.
execution what the asz Claude Code plugin’s hook saw of each call to an MCP server, one line per call, in a file of its own beside the stream’s change file one record per observation, with id, tool and time lifted from it and the line whole as one data part, an execution/1 record; no from and no flags, so assembly emits no node for it nothing in a round; the view joins each to its step by tool, and one step can have several. See Execution records
provider_body what a model call was sent and what came back: the bodies Claude Code wrote for its provider, one file per body, or a LangChain run’s inputs and outputs one record per body, with id unique in the session (for Claude Code, the body’s file name without .json; for LangChain, the run id followed by :request or :response), run on a request, call on a response, model, and the parts described in Provider bodies; no from and no flags, so assembly emits no node for it. The header’s src is . and it names no stream nothing in a round; the view joins each to its call. See Claude Code Provider Bodies and LangChain

A source that is one document, not a stream of lines, lands as one record with ord 1 and off 0. Its sha and bytes cover the whole document. The Claude Code adapter’s agent_meta, workflow_manifest and workflow_script files are of this kind, as is the Kiro adapter’s agent_meta, and a new version of the document lands as a new file (see Snapshot cursors).

Files of every kind are bound by the rounds’ input_digest, so a session travels or archives as all of them. A session’s agent_meta, workflow_manifest and workflow_script records carry no time, so those files have no record time range.

Record

{"ord":12,"off":25086,"sha":"4fed1b624096","bytes":1524,
 "id":"c8b85d73-…","parent":"a5f0a42d-…","call":"msg_011Cdm…",
 "from":"agent","time":"2026-08-06T16:57:22.652Z","flags":["finished"],
 "usage":{"in":2,"out":331,"cache_read":20729,"cache_write":19709},
 "parts":[{"k":"call","id":"toolu_015jKz…","name":"Bash","data":{"command":"make check"},
           "state":"available","bytes":181}]}

Two halves. The record level carries provenance and the identifiers structure is built from, in role names. The part level carries what the content is.

Field Meaning
ord, off, sha, bytes where the record was in the source: its line number counting from 1, the byte offset its line starts at, the first twelve hexadecimal characters of the SHA-256 of its bytes, and their size. The source record is not always kept whole. What data holds describes what its parts keep. The digest stays, so provenance is provable and a record that claims a source it did not come from is detectable.
id, parent the record’s own identity and its containment parent
call the provider call this record is a fragment of
run the agent loop it belongs to, one per trigger
continues on a reset boundary only, the record the new context resumes from
tool, child, batch, started_by joins the runtime states outside the content: the call a record refers to (a notification names the call it completes), the child stream a record names, the group it belongs to, the stream that started this one
label a name the runtime gave something. The only naming evidence in the data.
from who produced it: agent, external, runtime. A record’s type is not what a record is; most records that look like a person are a tool answering.
time, trigger, flags when, what started the loop, and what the record is beyond its producer. See Flags
usage what the provider reported: input, output, cache read and cache write tokens. Meaningful only where the call finished.
model the provider model the call ran on, as the runtime named it, on the records of a call. What a token count is reported under. Empty on records landed before it was kept.
parts the content
dropped what the conversion chose to leave out, with its size and the reason

Empty identifiers are common and mean the runtime supplied none. Nothing is inferred to fill them.

asz verify checks ord, off and bytes without the source. A provider body is a whole document at ord 1 and off 0, so for it asz verify rebuilds the body instead, as Provider bodies says. In each stream or run, across its files of one kind, ord must run 1, 2, 3 with no gap, starting at 1. The first record must start at byte 0, and each record after it at the byte after the previous record’s line ends. Where the stream has an append cursor, the records must reach the line and the byte the cursor names (see Append cursors). A skipped line, or a lost file of a source that is a stream of lines, would otherwise show only as a shorter conversation. The Claude Code adapter’s agent_meta, workflow_manifest and workflow_script files are each one document with a snapshot cursor, so a lost one is found only by the round chain (see Retention). A repeated record is not a gap (see Landed files).

Flags

flags names what a record is beyond who produced it. An adapter sets each one by a rule over its source’s fields, never by guessing from text or size, so a reader can act on a flag by rule.

Flag On a record that
external_input carries input from outside the agent, including input that exists only as an attachment
injected carries material the runtime put into model context
system_prompt carries the system prompt the runtime sent, or may carry it
tool_schemas carries the schemas of the tools the runtime advertised, or may carry them
finished carries a provider stop reason, so the call finished and its usage counts are real
synthetic is an assistant-role record the client wrote. No model produced it
context_reset is an explicit model-context reset
reset_summary is the summary that reset carried forward
child_result returns a child’s value. See below
error is a provider or runtime error
launch_ack acknowledges that a child started, which is not its result
turn_duration carries the runtime’s own measurement of a turn
command is a command run in line rather than said to the model
notice is the runtime telling the requester about the session itself
auxiliary names a child stream that is a plain model call the agent made inside its own work, not a child agent

system_prompt and tool_schemas exist so a server that shows a conversation to the people an agent served can withhold what the runtime sent the model, by name rather than by guessing from the text or the size. A record that may carry them is named for them too, such as a line whose fields cannot be read, since a reader withholding them is better kept from it than shown it. The Claude Code mapping says which attachments carry them, and LangChain which records can.

child_result is not only on a journal record. On a transcript record it marks a result that is a child’s synchronous return, which is the parent’s copy of the child’s output. Assembly reads the flag only on a journal record. Nothing removes the parent’s copy, so it stays as the result of the call that started the child.

Reading a record

The format is JSON, and a reader in any language reads it with its own JSON library. The rules below and the rest of this page are the contract. A reader does not have to copy what asz’s code does with input that asz never writes.

Every field of a record, as the table in Record lists them, has one type. ord, off and bytes are integers, and ord and off are never negative. flags is a list of strings. parts and dropped are lists of objects, typed in Parts and Dropped. usage is an object whose in, out, cache_read and cache_write are integers. Every other field is a string. An integer is a whole number, written with no fraction and no exponent, that fits in 64 bits with its sign. A field written as null is the same as a field left out. A reader ignores a field this page does not list, so a later version can add one.

A line with a field of another type is not a record. asz’s reader stops at a line it cannot decode, such as one whose off is a string, as it stops at a line that is not JSON, and reads no record after it in the file.

A reader may count a line that holds more than 256 objects and lists open at once as one that does not decode, and the OAP does. No landed line comes near that. Measured over 423,944 lines of 94 real conversations, the deepest held 16, a model request carrying a tool’s JSON Schema in its data.

A time is written as RFC 3339: a four-digit year, the letter T, the time of day to the second with a fraction of up to nine digits or none, and Z or an offset such as +08:00. Times compare as the instants they name, never as text. As text, 2026-01-01T12:00:00.11Z sorts before 2026-01-01T12:00:00.1Z, and 2026-01-01T12:00:00.5Z before 2026-01-01T12:00:00Z: in both pairs the first is the later time. 2026-01-01T20:00:00+08:00 sorts after 2026-01-01T12:00:01Z as text, though it is a second earlier. A value that is not such a time means the record has no time.

Addressing a record

A round points at a record with {seq, row}, and at one of its parts with {seq, row, block} (see Session Flow). In Session Data terms:

Field Is
seq the seq in the file’s header
row the record’s line after the header, counting from 1
block the part’s position in parts, counting from 0

The index numbers every part, including parts it has no use for, so a number names the same part to the index, to a round and to a reader. A dialect writes one part for each piece of content in the source record, in source order. Content it cannot describe goes in as an unknown part in the same place, never left out. A part the adapter adds beside the content, such as the changes/1 record on an edit result, comes after them.

Provider bodies

A request Claude Code sends repeats most of the request before it: the whole message list of its chain, the system prompt and the tool schemas. A provider_body record keeps only what its session does not hold yet, and says how to put the rest back.

Its parts come in source order. Each is a data part holding a piece of the body, byte for byte: a tool definition, or a JSON string of 1 KiB or more, with its quotes. The last part is a data part that asz builds, the manifest, schema provider_body/1:

{"schema":"provider_body/1","role":"request","src":"fcc261af-….request.json",
 "sha256":"<SHA-256 of the whole body>","bytes":137054,"depth":1,"chain":"3e1f…",
 "model":"claude-opus-5","session":"9a91…","run":"75db…","previous_request":"req_011C…",
 "segments":[{"copy":{"from":"03455f62-….request","sha256":"…","len":26777}},
             {"lit":"…"},{"part":0},{"piece":"<SHA-256 of a piece>"},{"lit":"…"}]}
Key Value
role, src, sha256, bytes request or response, where the body came from relative to the adapter’s source root, as a header’s src is, and the digest and size of the whole body
segments joined in order, they are the body. lit is a JSON string whose UTF-8 bytes are literal bytes of the body. part is a part of this record. piece is the SHA-256 of a piece an earlier record of the session holds. copy is the first len bytes of the earlier body whose record id is from and whose digest is sha256
depth how many copies lie between this body and one with none. It is at most 32; a body whose base is that deep copies nothing
bytes limit a body may claim at most 256 MiB, and a rebuild stops as soon as it passes the size its record claims, so a damaged record cannot make a reader hold more than one body
chain the first sixteen hexadecimal characters of the SHA-256 of the request’s first message, with the cache marker taken out. It chooses which earlier body a copy comes from. It is storage, and a join must not rely on it
model, session, call, request, previous_request the values the body carries that join it, as its adapter read them, in the model’s words: the session it names; on a response, the call it answers and the provider’s id for its request; on a request, the call it was sent to when its adapter knew it, and otherwise the provider request id of the call before it in its chain. Empty when the body carries none. The adapter’s glossary says what the runtime calls each

A reference is a digest or a record id, never a file and a row, because asz repack renumbers files and rows and carries record lines unchanged. Every reference points at a record that landed before this one in the same session, so a session rebuilds on its own. A reference may point into an earlier file: the tool schemas, for instance, land once, in the session’s first provider file. So to rebuild a body in the provider_body file with sequence N, a reader reads the session’s provider_body files with sequence N or lower, in order, and keeps them: a later body needs only the files it has not read yet. Package pkg/providerbody does this.

Keeping each file on its own would repeat the tool schemas, the system prompt and one whole request in every file. On the second capture, cut into three files of ten bodies, that took 43.9% of the bytes Claude Code wrote instead of 21.7%, and a long session with requests of several megabytes would keep almost nothing.

A body can have nothing to refer to: the first body of a session, the first of a chain, one past the depth limit, or one in a session whose earlier provider_body files are gone from the root. It lands with all of its pieces, and rebuilds from its own record. A body that refers to a file the reader does not have does not rebuild. asz verify reports it, and a receiver that keeps a session’s files in several storage stages has to read the earlier files from whichever stage holds them.

The adapter checks each record before it lands: it writes the record with the Session Data writer, reads it back and rebuilds the body. A body that does not come back whole, or is not one JSON object in valid UTF-8, lands whole in one unknown part, whose text says why it was not cut. asz verify rebuilds every landed body and compares it with sha256, and reports a reference to something the session does not hold.

A record whose new pieces are larger than the collector’s max_delta_bytes has its largest pieces taken out until it fits, as Too large to land describes for any record, and never its manifest. This is the first request after a person pasted a screenshot. A piece taken out keeps its place, so every segment still names it, and its sha256 is the piece’s digest, so a later body that holds the same piece names it as any earlier piece. A body made of short strings keeps nearly all of itself in the manifest’s literals, so when the record is still too large with every piece taken out, the body lands whole in one unknown part, and that part is taken out.

A body with a piece taken out cannot be rebuilt from the root: Session.Body returns ErrTooLarge, and Session.BodyFrom rebuilds it from another copy of the body, such as the file the runtime wrote. A reader registers a piece taken out by its sha256, as any piece, so a later body that names it is read, and one that names only other pieces still rebuilds. The adapter’s check before landing rebuilds the record with the pieces it took out. No body copies the front of a body that cannot be rebuilt. asz verify counts these bodies apart from damage, and still holds the record’s bytes and sha to its manifest.

On two captures of Claude Code 2.1.260, the landed provider_body files were 25.5% and 21.6% of the bytes Claude Code wrote, and all 42 bodies rebuilt exactly.

Closing line

{"t":"end","records":118,"digest":"c41e…"}

Every file ends with this line. records is the number of record lines, and digest is the SHA-256, in hexadecimal, of every byte before this line: the header and every record, each with its newline. The writer computes it as it writes, so a file can be checked on its own.

A reader refuses a file with no closing line, or one whose digest or count does not match. A file cut short would otherwise read as a shorter conversation, with nothing saying so. asz verify reads every landed file this way.

Parts

Kind Is Carries
text readable text text
reasoning the model’s own reasoning text when the runtime kept it
call a request to run something id, name, data, and server and server_tool for a call to an MCP server
result what a call returned of, failed, and text, data or both
media an image or a document media, data
data structure that is not prose: a record the runtime keeps for itself, a manifest data
unknown content the dialect could not describe the bytes in data as one JSON string, the bytes themselves or their base64 when encoding is base64, and the reason in text

In a part, k, text, id, name, of, server, server_tool, media, encoding, state and sha256 are strings, bytes is an integer, failed is a boolean, and data is any JSON value.

server and server_tool say which MCP server a call is addressed to and which of its tools. An adapter sets them only where its runtime’s name for the call splits exactly. Claude Code names such a call mcp__<server>__<tool>, and the split is made only when exactly one __ follows mcp__, counting the ones that overlap: a name that holds a second __, or underscores that run together as in foo___bar, could split two ways, so it is left whole. The server is Claude Code’s form of the configured name, so “claude.ai Claude Docs” is claude_ai_Claude_Docs. The name the server was configured with is in the call’s execution record, when there is one.

Every part carries state and bytes. state is one of available, truncated, redacted, omitted, unavailable or too_large, or removed in a skeleton, and bytes is the size of the original even when the part holds less. A reader is always told how much of the original it has.

Too large to land

A landed file travels whole, so the collector’s max_delta_bytes is the most a receiver has to store at once. A collector cuts a file by what its records take once written, not by the bytes they were read from: a record carries an envelope its source line does not, and 4 KiB of short source lines measured 19 KB of records. One record cannot be cut: a pasted screenshot measured 4.6 MB in one transcript line. Such a record lands alone in a file of its own, and its largest payloads are taken out, one at a time, until the file fits.

A part taken out keeps its place, its kind and every identifier: id, name, of, server, server_tool, failed and media. Its data and encoding go first, and then its text, except an unknown part’s, which is the reason its bytes were not described rather than content. A part with text and data, such as a result with the runtime’s structured copy of it, so keeps its readable text while it can: its state is truncated. A part with nothing left says too_large. Its bytes is the size of the original, and its sha256 is the SHA-256 of what was taken out first: an unknown part’s bytes, other data compacted as the writer lands it, or the text of a part with no data. A reader still sees the step, and can match the payload against another copy of it.

A changes/1 record and an execution/1 record are evidence a step joins to by its tool call, so neither is ever taken out whole: a reader that found nothing would read that the call changed no file or ran nowhere. A change record is made smaller in its own format instead. Its file with the largest hunks loses them and says its diff is too_large, as a diff too large to compute says, and its additions and deletions stay. This goes on, file by file, until the record fits. When no file has hunks left, its files, overlaps and gaps go, and it keeps its identity, its tool, its window and changed_files. Its part says truncated, with the original’s bytes and sha256, and the record still joins its step. An execution record keeps sizes and digests only, never what was sent or returned, so it is a few hundred bytes and is left as it is.

Of two payloads of the same size, the one in the earlier part goes first, so the same record always loses the same parts. No other part carries sha256. A record that is still too large with every payload taken out lands as it is: what is left is its identifiers, its label and the list of its parts. asz repack carries records as they landed, and takes nothing out.

What data holds

data is one JSON value. What it holds depends on the part:

  • A piece of the source’s own JSON: a call’s input, a result’s structured form or content that is not text, an attachment with no prose, or a record or document with no message. A journal line, sidecar, manifest, system record or line the asz plugin wrote can land whole this way. The writer inserts that JSON after json.Compact, keeping its bytes apart from whitespace between tokens. This keeps each landed record on one line. Whitespace inside strings is unchanged.
  • Bytes the dialect could not describe: an unknown part holds them inside one JSON string, using base64 when needed, as described below. Part.Raw returns the original bytes whatever escapes that string uses.
  • A value the adapter built: a media part’s base64 is written again as a string, and a changes/1 record derived beside a result uses asz’s own encoding. These are not source slices.

The writer never re-encodes data, so its bytes after compaction do not depend on whether asz uses Go’s JSON v2 engine. Existing escapes stay as the source wrote them. Unknown strings and derived change records still use Go’s json.Marshal, and plugin lines keep the escapes their producer wrote. Strings the writer encodes itself, such as text, label and identifiers, are values rather than source bytes. It writes <, > and & literally in those strings, but escapes U+2028 and U+2029 as \u2028 and \u2029.

Whitespace between tokens is removed without a dropped entry or warning. If a runtime starts writing such whitespace, those bytes will be lost silently. The scenario check compares source bytes only in scenarios; it does not monitor real source files.

Earlier writers, including asz 0.2.0 and development builds before this change, encoded <, > and & as \u003c, \u003e and \u0026, including inside data. They encoded U+2028 and U+2029 there as \u2028 and \u2029. The JSON values are the same, but the bytes can differ. Old files are never rewritten, and asz repack carries their record lines unchanged. Old and new files can share one root and round chain; each digest covers the bytes actually written.

Measured on 2026-09-12 on one machine’s Claude Code sources, the writer kept all 229,341 parts carrying source JSON byte for byte. The earlier encoding kept 144,464 (62.99%). The sample held 6,377 files from 85 sessions, with 444,881 source records, copied over 34 seconds and then read from the frozen copies. No source JSON in that sample needed whitespace removed. Both Go 1.27.1 JSON engines gave the same result through the final writer and reader. Separately, all 177 unknown parts returned their bytes through Part.Raw. The 2,560 derived change parts were not counted as source JSON, and the sample held no plugin change records.

No fixed rule could decide how much of a part to keep or show, because tool output varies too much. The measured corpus is 62 Claude Code sessions: 3,032 files and 1,100.1 MB of source records. One tool result in it was 1.1 MB. Over its 28 sessions larger than 1 MB, the share of a session that is tool output ran from 17.3% to 83.5%, with a median of 35.1%. A rule tuned to the median is wrong by a factor of two at both ends. So every part states its own size and state, and a reader decides. No built-in adapter writes omitted, and a part one writes as truncated is one made smaller to fit the budget, so no part they write is clipped otherwise. Clipping is left to a reader, such as the asz.view document, which clips a node’s text and gives the full size beside it. A part is still not always all the runtime gave. The Claude Code dialect leaves some content out with no dropped entry. Its Session Data mapping lists these as known gaps.

An unknown part’s text is not content. It is the dialect’s reason for not describing the bytes, such as the record is not valid JSON or block type "x" is not one this dialect describes. Its state is available, because every byte is kept, and bytes is their size. A reader that shows text as the message would show the reason in its place.

An unknown part’s data is always one JSON string, and it holds the bytes in one of two forms. When the bytes are valid UTF-8, the string is the bytes themselves, and the part has no encoding. A JSON string cannot hold bytes that are not valid UTF-8: Go’s JSON encoder writes U+FFFD in their place, and the byte is lost. So when any byte is not valid UTF-8, the string is all of the bytes in standard base64, and encoding is base64. encoding has no other value, and no other kind of part carries it. A reader that does not know encoding still reads one string, and shows base64 text where the bytes were not text. Part.Raw in pkg/sessiondata applies the rule.

Every unknown part landed before encoding existed is one string with no encoding, and reads as it always did. Where its bytes were not valid UTF-8, that string holds U+FFFD in their place. Claude Code and the asz plugin write UTF-8, but such bytes can still reach an adapter. A write cut short and joined to the next line is one way, and a workflow script saved in another encoding is another. Measured on 2026-09-11 on one machine’s storage root of 60 sessions, 151 of its 408,439 parts were unknown: 150 workflow scripts and one block of a type the dialect did not describe. None of them held U+FFFD. So none had lost a byte, and all of them keep the form without encoding.

Media is kept inline. A media part’s data is the base64 text the runtime wrote, as one JSON string. media is its media type, and bytes is the length of the base64 text. There is no separate store for media. The measured corpus held 41 image blocks, so inline costs little. A runtime that sends images routinely should be measured again.

A result can carry text and data together: the readable output, and a structured form of the same result that the runtime gave beside it. A reader that reads only one of them loses the other.

failed is absent, not false, when the runtime said nothing. Most results carry no error flag, and reading that as success would report something the runtime never claimed.

The set of kinds is small because the thing described is small. Measured across a corpus of 3,032 Claude Code files, six content-block shapes exist and four cover 99.99% of content blocks. The Claude Code Session Data mapping gives the counts.

Execution records

An execution/1 record says what one observer around a tool call saw it do. The transcript says what the model asked for and what came back. It does not say which server ran the call, how long the call took, or what the server was sent. The one observer today is the asz Claude Code plugin, whose hook runs after every call to an MCP server. Measured on Claude Code 2.1.282:

  • The hook after a call names the server and where its configuration came from, the time the runtime measured around the call, and the input as the server received it. A hook before the call that rewrote the input changed what the server received and what this hook sees, while the transcript kept the model’s own input.
  • An error the server returned, a lost connection and a timeout all reach the failure hook, not the success hook.
  • A call the runtime refused never reaches a hook, so it has no record.
  • A server’s declaration that a tool only reads, or can destroy, reaches neither the model, nor the hooks, nor the transcript.
Field Holds
schema execution/1
id the observation’s own identity: the tool-use id and where it was observed. One call can have several records, each with its own id. The same event handed over twice is one record
observed_by, boundary who observed the call, asz-plugin, and where it stood, client_hook: the runtime’s hook after the call
session, stream the session, and main or the agent id the call ran under
tool, tool_name the call’s tool-use id, the only join to its step, and the name the runtime called it by
cwd the working directory the runtime reported, which the collector’s session filters are judged by
protocol mcp
server name, the server’s configured name, and source, where its configuration came from in the runtime’s own word: for Claude Code user, project, local, or dynamic for a server given on the command line
time when the observation ended
duration_ms the time the runtime measured around the call, which includes any waiting before it started. It is not the server’s own time. Absent when the observer did not say
outcome returned, failed or interrupted
arguments, result only the bytes and the sha256 of the value as compact JSON with sorted keys, and state size_only. Two observations of one value have one digest, so a digest that differs from the model’s input shows the input was changed on the way. result is only on a call that returned

Dropped

"dropped":[{"what":"reasoning signature","bytes":588,"why":"a provider verifies it; a reader cannot read it"}]

dropped exists so a loss is stated rather than silent. It lists only what the dialect understood and chose not to carry. Everything it did not understand travels as an unknown part, so a later version of the dialect can interpret it without collecting again. In an entry, what and why are strings and bytes is an integer.

A source record is not always kept whole, so a .sd file is the only landed copy of what its parts hold, and everything above the adapter treats it as the authority. Content that was converted to text, such as a message or a thought, needs the runtime’s source to correct a conversion error. Claude Code deletes its own transcripts, and a storage root outlives them (see Storage Root). An unknown part can be interpreted again from its original bytes in the store. A part that keeps the source’s JSON can also be interpreted again, subject to the whitespace and older-file limits in What data holds.

Skeletons

When storage.retention removes a landed file, a skeleton takes its place: the same name with the extension .sk, in the same directory, written once and read-only. A skeleton is Session Data compressed with gzip, and it is local: it is never sent. It keeps what a conversation’s structure is built from and none of the content, so the index rebuilt from it is the index rebuilt from the landed file. Retention says when a file is removed.

  • The header, whole, with one more key, removed:

    {"digest":"9f2c…","size":204811,"records":118,"at":"2026-10-13T10:00:00Z","generation":1,
     "receiver":"accepted","owner":"grpc:%2F%2Foap:11800||me@laptop"}
    

    digest, size and records describe the landed file: the SHA-256 of its whole bytes, which a round’s input digest binds, its size in bytes and its number of records. at is when it was removed, and generation which removal of the session removed it, counting from 1. receiver says what the receiver confirmed of it: accepted, rejected when it took the file and rejected some of its records, or unconfirmed when no answer confirmed it. owner is the owner its session is sent under, as push.state records it.

  • Every record, in order, with ord, off, bytes, id, parent, call, run, continues, tool, child, batch, label, started_by, from, time, trigger, flags, usage, model and dropped. A provider body keeps sha too; any other record drops it.

  • Every part, in its place, because a part’s position is its block number. Each keeps k, bytes, failed and sha256, and its state is removed, or too_large for a part that was too large to land, which held nothing to remove. A call keeps id, name, server and server_tool, and a result keeps of. text, data, media and encoding go, with three exceptions, each made only when the data decoded:

    • A changes/1 record, in any data part, keeps schema, id, captured_by, tool, tool_name, time, basis, root, coverage and changed_files, and for each file in changes its path, operation, before, after, attribution, additions and deletions: what the page groups and counts the files by. hunks, windows, policy, window, outcome, gaps and overlaps go, and a diff that was available says unavailable, since its lines are not kept.
    • An execution record keeps schema, id, tool, time and outcome: its own identity, the step it joins to, its place in time and how the call ended.
    • A provider body keeps its manifest without the literal strings, which are text of the body: each segment loses its lit and keeps the rest, in order, and a segment left empty goes. So every part, piece and copy stays, a reader holding the skeleton checks it as it checked the body and accepts it or not as before, and a manifest with no segments keeps none. Each other data part keeps the SHA-256 of its data in sha256, which is the piece’s identity, or the one it had when it was too large to land.
  • Its own closing line, with its own record count and digest.

A reader opens a .sk through gzip and reads it as any other Session Data. While a removal is in progress, a landed file and its skeleton can both be there; read the landed file.

Producing Session Data

There is no Go interface to implement. An adapter is whatever lands Session Data in a storage root. Nothing above it reads a runtime field, so another runtime needs no code on the assembling side (see What travels).

An adapter writes:

  • A header on every file, with adapter, its name and contract version, and dialect, beside the other fields a reader requires. See Header.
  • Records with role-named identifiers and parts, one part for each piece of content, in source order. See Addressing a record.
  • The closing line. The writer in pkg/sessiondata adds it on close.
  • Files in the layout and under the names Storage Root shows. Each is written once and made read-only, numbered one above the highest sequence already in the session, while the session’s lock, <session-id>/.lock, is held.

The built-in adapters are wired into cmd/asz by name, so a third-party adapter is a separate program that writes the storage root. asz parse finds sessions by listing the root, and asz view serves the chains the root holds, so such a root is assembled and shown like any other. The index is not part of the contract. asz parse builds a session’s index from its landed files when the session has none, and never reads past the index it has (see The index). So a program that adds files to a session that already has an index removes its index/ directory. That costs only a rebuild.

A dialect also has a glossary, keyed by the dialect: what its runtime calls each name the model uses. cmd/asz gives the page and asz glossary the Claude Code glossary, whatever dialect a conversation was read in.