Changes in 0.3.0
Reliability
- A round is published only after its temporary file is complete, read-only and synced. An
interrupted write no longer leaves a truncated
.sffile that stops every later parse. A storage root on exFAT on macOS, which has neither an exclusive rename nor hard links, still publishes rounds and receipts. An empty file a crash leaves there under a round’s or a receipt’s name is not counted, and is replaced once it is a minute old. The conversation list skips a conversation whose only round file is the temporary file of an interrupted write. asz repackrefuses a populated destination before writing. Its session reservation and file publication also refuse existing data, so repeating a repack cannot invalidate a chain.asz showlocates the current.sdfiles through the shared filename parser.- Named adapters inherit their own defaults, while explicit
falseand empty exclusions stay as written. Omittingenabledno longer disables a local adapter by mistake. An adapter entry withoutexcludenow inherits/private/tmp/**, so it no longer collects those sessions. - Token derivation follows calls across any number of landed files and leaves unfinished calls eligible for later completion. Deferred sessions are retried while other sessions keep growing. Immutable receipts keep requests and their accounting together across an interrupted progress save, even when new fragments arrive before retry. The retry applies those receipts before it derives a file that waited for its grace, and a receipt only moves a series forward, so no two windows of a series overlap. A session the first pass did not finish, because a file waited for its grace or an error stopped it, retains that pass’s look-back.
- The Claude Code plugin applies roots, exclusions and content limits to subagent edits and their
manifest updates. Its
readonly-v2policy scans executable wrappers and writing command modes that the earlier policy skipped, so concurrent writes retain their actual tool windows. A variable assignment before a command scans too, unless it only sets locale or display output. - Local tar packaging disables AppleDouble metadata. Source and binary archive checks reject
._*,.DS_Storeand__MACOSXentries, including the entries macOS tar hides by default.
Collection
- A landed record keeps the provider model a call ran on, as
model, the name the runtime wrote on the record. It is what a token count is reported under, so a metric per model can be produced from the landed data alone. Records landed before this carry none, and the glossary says where the runtime writes it. - A parse takes the conversation’s lock before it reads the session’s index. A parser that read the index first could hold an old one while a scenario removal took the session away, and then publish a round over evidence that is gone. A parse of a session with no landed files and no chain creates nothing.
- An
unknownpart thatclaude-code-localorclaude-code-changeslands keeps every byte the dialect could not describe, as the Session Data page always said. A JSON string cannot hold bytes that are not valid UTF-8. Go’s JSON encoder wrote U+FFFD in their place, so such a byte was lost while the part still saidavailable. When any byte is not valid UTF-8, the part now keeps all of its bytes in standard base64, and carries a new key,encoding, set tobase64. Valid UTF-8 is written as before, as one JSON string with noencoding, so every file landed before reads as it did. The schema stayssd/1, because a key was only added.Part.Rawinpkg/sessiondatareads both forms, andPart.SetRawwrites them. Both adapters now build the part withPart.SetRaw. The plugin’s adapter,claude-code-changes, passed the line to the JSON encoder as a string, and lost such bytes the same way. Claude Code and the 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, none of its 151unknownparts had lost a byte. That root held no change records of the plugin, so the measurement coversclaude-code-localonly. - Source JSON in a part’s
datanow keeps its bytes, apart from whitespace between tokens. The writer inserts it afterjson.Compactinstead of re-encoding it. Earlier writers changed<,>and&to\u003c,\u003eand\u0026, and U+2028 and U+2029 to\u2028and\u2029. Turning HTML escaping off alone still changes the last two under Go 1.27’s JSON v2 engine. Inserting the compact JSON keepsdatathe same under both Go JSON engines. Unknown strings, derived change records and plugin lines keep their own encodings. Whitespace between tokens is still removed without a warning. What data holds describes the exact boundary. On a 2026-09-12 sample of 6,377 source files from 85 sessions, all 229,341 source JSON parts landed byte for byte, compared with 144,464 (62.99%) before. Both Go JSON engines gave the same result. The schema stayssd/1; old files and their digests stay intact. A data-only step can therefore show escapes from an old file and literal characters from a new one. The exampleasz-view-example.yamlis regenerated with the new writer. OAP’s copies need a paired follow-up: regenerate both fixture scenarios and their example documents from the same asz commit, then replace their landed files and expected documents together. - The new
parts_keep_source_bytesscenario property compares source JSON and unknown bytes with landed parts and checks source digests. It runs onclaude-codescenarios only, including the newsource-bytes.yaml; it does not monitor real sources. Scenario writers now preserve literal<,>,&, U+2028 and U+2029. Workflow names produce valid run ids even when they start with punctuation, and a build refuses names that produce the same run id, ignoring letter case. - A source Claude Code pruned now has its cursor set to
source_gone, as the documentation said. A pass visits only the files discovery lists. So before, only a file that went while a pass read it was marked, and a pruned file’s cursor saidactiveforever. Measured on 2026-09-11 on one machine’s storage root of 60 sessions, not one of 6,080 cursors saidsource_gone, while discovery no longer found 8 of the sessions, with 415 cursors between them, and 1 file of another. Now, after a session’s sources, a pass setssource_goneon each other cursor of the session whose file does not exist. A session discovery no longer finds is checked the same way. Either way the pass holds the session’s lock, and it leaves alone a session the adapter’sincludeandexcludekeep out. A collector checks a session discovery no longer finds until one check completes, and again only after discovery has found it in between. Claude Code prunes by age, so without that a pass would read more cursors every week. Discovery reports a project directory it cannot read, and a pass that met one does not check the sessions discovery did not find, because an unreadable directory hides files the way pruning does. Discovery does not report a directory it cannot read inside a session, such assubagents/, so such a directory does not stop the check. A cursor moves only when its file does not exist, so the cursor of a file in such a directory stays as it was. Only anactivecursor is set tosource_gone, and aconflictstays. A cursor that names a path this adapter does not produce, as the cursors of a scenariosdbuild do, is left alone. When discovery lists a file whose cursor sayssource_gone, restored from a backup for instance, the pass sets the cursor back toactivebefore it reads the file. So an older copy of a transcript or a journal, shorter than what was read, becomes aconflictin that same pass. A copy that still holds every byte that was read is collected from where reading stopped. A file in a session thatincludeandexcludekeep out is not read, so its cursor stayssource_gone. The landed files stay in every case. The plugin’s change records are not Claude Code’s files, and their cursors are still marked only when a file goes while a pass reads it. Pruned sources has the details. - A new scenario property,
pruned_sources_gone, checks this in every scenario in theclaude-codeformat. On a copy of the finished root it deletes the session’s main transcript, then every other file of the session that the Claude Code adapter finds, and then puts them all back with the bytes they had. After each step, one pass ofclaude-code-localandclaude-code-changesruns, as a newly started process would. After a deletion, each deleted file’s active cursor must saysource_gone, and every other cursor must say what it said before, with no cursor added or removed. A cursor that already saidconflictstays that way. After the files come back, every cursor must say again what it said before. No pass may land anything, meet a new conflict or an error, or change a landed file. A parse must write no round, andasz verifymust count as many problems as it counted before. The scenario guide describes the property under Check.
Commands
asz collectis the whole pipeline. One pass lands what is new, parses every session that moved, and sends whatexport.otlpasks for, in that order, so a pass ships the rounds it has just written. It reads every enabled local adapter in the same pass, where before it ran them one after another and a watching source kept the others from ever running.asz serveris new: the pipeline and the page in one process, which is what a person runs to watch their own conversations locally.asz viewonly reads now. It serves a storage root that already holds conversations and never collects, parses or sends, which is what a root copied from another machine, or filled by the receiver, needs. Useasz serverto collect and serve together.asz pushmakes one pass and exits. A root that keeps growing is sent byasz collect. This command is for a root that is already there.export.otlp.intervalis gone with the loop it drove, and the collector’sintervalis the one period.- The
claude-code-otlpreceiver listens underasz collectandasz server, the two commands that write for as long as they run. It no longer listens underasz view. asz verifyholds the first record of each stream to line 1 and byte 0. Where the stream has an append cursor, its records must also reach the line and the byte the cursor says the collector read to. Before, the check started from whatever the first record said and never read the cursor. So a lost first or last landed file passed unless a round had consumed it, and in a root never parsed, or one whose rounds were removed, nothing reported it. Now, in a stream with an append cursor, such a loss shows whether or not a round consumed the file. A lost first file shows as anord gapand abyte gap. Records that stop short of the cursor show on a newend gapline, with the cursor’s position and theirs. A stream with an append cursor whose every landed file is lost is checked from its cursor too. A snapshot cursor, or no cursor, is not compared. There are two exceptions, and in both only a round that consumed the file shows the loss. A lost file leaves no gap when another landed file holds the same records, as after an interrupted pass. A cursor never travels, so in a root rebuilt from a push the check stops at the last landed record, and a lost last file leaves no gap there. Which streams have an append cursor depends on what landed them.claude-code-localgives one to a transcript and a workflow journal, andclaude-code-changesto the plugin’s change records.claude-code-localreads a child agent’s sidecar, a workflow manifest and a workflow script whole, with a snapshot cursor. So in a root it collected, a lost file of one of them is found only by the round chain, and a root with no rounds does not show it. A scenariosdbuild gives each of them an append cursor that counts records, so there such a loss is an end gap too. A cursor behind the records is what an interrupted pass leaves, and is not a problem. The cursor is read before the files are listed, so a collector running beside the check cannot make it look ahead. Measured on 2026-09-11 on one machine’s storage root of 60 sessions, all 6,080 streams began at line 1 and byte 0, and none of the 2,995 append cursors was ahead of its records. So on that root, neither rule reported a problem that was not there. The newtests/scenarios/lost-ends.yamlloses the first and then the last landed file of a stream.tests/scenarios/lost-file.expect.yamlnow counts two problems where it counted one. The second is the end gap of the helper stream whose file it loses.asz scenario buildkeeps building with--every D: one whole session, then that long on the wall clock, then the next. It takes more than one scenario file, and a directory contributes every.yamlin it, with--pick cycleor--pick randomand--seedto repeat an order. Each session is stamped so its last record lands when it was written, and carries an id no earlier session has. It is a mock client to run besideasz serverorasz collect.asz scenario build --removesays when a pipeline may remove each session aclaude-codebuild writes:immediately, the default, or a duration such as24hor7dafter the session’s last record. The build writes a marker for each session after every other file of it, naming the policy and every file it wrote for the session with its size and SHA-256.asz collectandasz serverremove a marked session at the end of a pass, once every landed file, round and metrics request of it is recorded as sent to the receiver they send to and none was rejected. They remove its source files, its landed files, its chain, its spool files, its lines in the state files, and last the marker. The session directory and its chain directory are each renamed into_removed/in one call before they are deleted, so a crash never leaves half a session for a parse to read. Only a pipeline whose storage root is the build’s--outremoves. It holds_scenario/.lockthere, and a second one waits. When the configuration keeps every marked session, thescenario :line at the start says why. Removal is a policy of the scenario, never of the product. The configuration has no removal setting, and a session no build marked, which is every real Claude Code session, is never removed.- A workflow in a scenario starts its children as one batch, and they run at the same time, as the
runtime’s do. Before, each child started only after the one before it had ended, so a review by
four agents was drawn as four reviews in a row. The parent’s next step waits for the last child.
The
push_follows_the_wirecheck now compares record times as times rather than as text, which the overlapping children showed it needed.
Read
- The conversation list shows what a conversation did, not how it is built: talks, model calls, subagents, Bash runs, and changes with the lines those records added and removed. Steps, streams and segments leave the table. This follows Horizon’s own conversation list, so the two read the same. The list folds each conversation to draw its row, and keeps the fold until the head round on disk moves. Talks and the time span are read from the fold. Model calls, subagents, Bash runs and changes are the counts in the head round’s header, and a count the head round does not carry shows a dash: a round cut before that count existed does not know the answer, and a zero would be a claim it never made. Measured on 2026-09-11 on one machine’s storage root of 60 conversations, the first list took 2.75 seconds, and a later one with no new round took 43 milliseconds.
- The conversation page is drawn by Horizon’s conversation renderer,
@skywalking-horizon-ui/conversation-view, embedded from a pinned Horizon commit with Horizon’s themes and fonts, soasz viewand the SkyWalking UI draw a conversation identically and the page needs nothing from the network. The pin now names Horizon22e2f869, which draws a tool call’s input and result as fields rather than one escaped string, reads an edit as a line diff, puts a changes mark on a step that changed files with the files and their diffs behind it, adds a Changes tab to the inspector carrying where each record came from, and pops the inspector out over the page. The hand-written viewer is gone with the API routes only it read. The page reads theasz.viewdocument, the glossary, and the landed record behind a step.tools/conversation-view.shrebuilds the copy from the pin, and CI fails when it differs.
Release
tools/release.shruns the Apache release in three commands, each started by hand.preparetags the candidate, pushes the tag and opens the pull request. Its tag message, its commit and its pull request no longer call the candidate a release, and it checks for the tools it uses first. CI on the tag push creates the GitHub prerelease with the binaries, and the release manager waits for it by hand.candidatedownloads the verified CI binaries from that prerelease, archives the tagged source locally, signs every package, verifies them against KEYS, uploads them to the dev area of dist.apache.org, attaches the source package and every signature to the prerelease, and writes the vote mail. After the vote,publish, run by a PMC member, verifies the signatures, moves the voted packages to the release directory, promotes the same GitHub prerelease, and writes the announcement, the website entries and the install manifests. The release manager counts the votes and writes the result mail by hand. How to Release walks through each command and the svn commands it runs.candidaterefuses a signing key that is not RSA of at least 2048 bits, as the ASF requires, or that has no apache.org user ID, and a source package that would hold a font file, since fonts are Category B works the ASF keeps out of source releases. The vote mail names the signing key and tells voters what the committed conversation renderer is.--no-uploadwrites its mail tovote-preview.txtand refuses once a candidate is uploaded.candidatealso refuses a signing key that has expired or is revoked in KEYS, the primary key or the subkey that signs. gpg reads a key’s expiry from KEYS, not from the signer’s machine, so a key extended there but not in KEYS passed before, and every voter’s gpg would warn about it. Every signature, of the scratch file before the build and of each package after it, must now be a good signature by the checked key that gpg does not report as expired or revoked. gpg exits 0 and writesVALIDSIGfor such a key too, so neither was enough. A revoked key is now named as revoked, where before it was refused as having no apache.org user ID. An expired user ID no longer counts as that address. Reading the KEYS listing no longer stopscandidatewhen more than a pipe buffer of it follows the signing key. The voters’ check list and Install say what gpg must not print.--dry-runis described as it behaves.candidateandpublishread the tag to make their plan, so a dry run fetchesv$VERSIONfrom origin when the local repository does not have it, as a real run does. They fetch that one tag and no other, and say so. Acandidatedry run signs its scratch file and reads KEYS in a temporary directory, which it removes.tools/package-smoke.shchecks one binary package the way a person uses it. It verifies the package’s sha512, unpacks it, checks that every file is there, and then runsaszand the plugin from the unpacked files: four scenarios throughasz scenario check, the list page and one conversation page fromasz view, and one shell command through the plugin’s hooks. A new CI job,packages, runs it on all six packages, each on a GitHub runner of its own platform. Thebinariesjob cross-compiles every platform on one Linux runner, and a binary that was never started proves nothing about its platform. The voters’ check list asks each voter to run it on the package for their own platform.candidateruns the package for the release manager’s machine with the tag’stools/package-smoke.sh, after it verifies the candidate and before the upload. When the check fails, nothing is uploaded and no vote mail is written. On a machine no package is built for, it runs none and says so.tools/package-smoke.shreads the plugin’s own record,output/<session-id>/main.jsonl, and needs it to name the file the shell command created. Before, it searched the plugin’s whole data directory, where files of the plugin’s scan name that file before the record is written, so a plugin that wrote no record passed. It compares the package’s sha512 with the one in its.sha512itself. That file must hold exactly one line that is not blank, and that line must be a sha512 followed by this package’s name.sha512sum -cchecks whichever file the line names, and the BSDsha512sumon macOS passed an empty file. On Windows it hands the two paths toExpand-Archivein the environment, so an apostrophe in a path no longer stops the unpack. Each page request now has a time limit, andasz viewhas 20 seconds to answer.- CI builds and tests the source package as a voter does. A new required job,
source-package, makes it withgit archive, unpacks it outside the checkout, where there is no.gitand no font, and runsgo buildandgo testthere. The unit tests of thebuildjob now also run the tests of the command and of the plugin, the plugin’s hook from end to end included, on Linux, macOS and Windows. No CI job ran those tests before. - The release guide shows the result mail, which lists every vote, +0 and non-binding -1 included, and links the vote thread. Only binding votes decide.
publishalso holds each local signature to the voted one, dates the website entries by the day of the move, and refuses--remove-oldin the run that moves: older versions leave the release directory in a later run, once the website links them from the archive. The announcement is titled[ANNOUNCE], and the website entries link the packages through closer.lua.- The notes of the GitHub release name the Apache release, the downloads page, the signatures and KEYS. They no longer point at a git checkout or the container image.
- The changelog has the layout of Apache SkyWalking and SkyWalking SWCK. On
main,docs/en/changes/changes.mdis the changelog of the version in development, and Current Version in the menu always points at it. In the commit it tags,prepareonly removes the in-development note, so the tag and the source package hold the finished changelog where the menu and the welcome page of the tag link it. The website publishes the docs of each version from its tag. In the next commit,preparemoves the page tochanges-VERSION.md, lists the version under Changelog, and writes a newchanges.mdfor the next version. The vote mail, the announcement, the GitHub release and the winget manifest link the tag’schanges.md.publishbuilds the text of the GitHub release from that page when it runs, in place of therelease-notes-VERSION.mdfileprepareused to store. How to Release describes the layout, which the rootCHANGES.mdused to describe. - On the push of a release tag, CI creates the GitHub prerelease and attaches the six binary
archives and their checksums, only after all checks pass. It reuses an empty prerelease an
earlier attempt of the run created, and refuses any other. The local download verifies the tag,
CI run, release identity and asset checksums. The readiness marker carries a fingerprint of the
assets CI verified.
candidateaccepts only the run of the tag push, and only binaries thatgithub-actions[bot]uploaded during it. A replacement candidate requires explicitly removing the rejected prerelease and its tag first. candidateattaches the source package, its checksum and every signature to the prerelease after the SVN upload, and never replaces a file there. If that stops, runningcandidateagain from the same checkout finishes it without signing or uploading again.publishchecks the GitHub release before anything moves, verifies every signature against KEYS, and after the move checks each file of the release against the release directory and promotes it. A KEYS entry that gpg cannot import does not stop it, as it never stoppedcandidate; each package still needs a valid signature from an imported key. Existing GitHub binaries and checksums must match and are never uploaded again. It attaches only missing source files and signatures, verifies the full asset set, and promotes the prerelease. A run after the move resumes there without the original CI artifacts or a localdistdirectory.- Windows on ARM 64 joins the platforms, so a version ships six binary packages beside the source package.
tools/install-manifests.shwrites a Homebrew formula, a Scoop manifest and the winget manifests from the voted binary packages, andpublishruns it. They are conveniences, submitted after the release and after the PMC agrees to each channel. The Homebrew formula installs the voted binary package for macOS or Linux, on ARM 64 or x86-64. It builds nothing, so it needs no Go, and theaszit installs serves the page with the renderer’s fonts. It downloads from the GitHub release, with archive.apache.org as its mirror, and goes to a tap, because homebrew-core does not take a formula that installs a binary built for each platform. The winget manifests download from the GitHub release too, because the archive slows down and bans heavy use. Each package must match the voted.sha512beside it. Install lists every way to get asz.make releasedelegates tocandidate --no-upload, preparing signed files locally from the verified GitHub binaries and tagged source.GPG_USERpicks the signing key. Local binaries frommake binariesare for diagnostics; candidate staging never falls back to them.- Binary builds use
GOWORK=offandGOFLAGS=-mod=readonly, so local workspace settings cannot change their dependencies.candidatechecks the source archive for compiled files by content type and extension, including static libraries, Go objects, Java archives and Python byte code. The voters’ checklist checks them too. make checksumsstops at the first package it cannot checksum, removes that.sha512, and checks each.sha512right after writing it. Before, a failed checksum left an empty.sha512, andmake releasewent on to sign every package.- The source package holds no font file. The two fonts of the conversation renderer are under the
SIL Open Font License, which the ASF puts in Category B, and the ASF does not allow a Category B
work in a source release.
.gitattributesmarks themexport-ignore, sogit archiveleaves them out, and candidate preparation refuses a source package that holds a font file anyway. The binaries in the binary packages embed the fonts, anddist-material/LICENSEnames them. A build from the source package draws the page with system fonts. make conversation-view-checkpasses in an unpacked source package. That package leaves out the renderer’s two fonts, so there the check leaves them out of its comparison, names them, and compares every other file. Before, it failed in every source package and told the voter to commit an update. In a clone of the tag it still compares the fonts, and the vote mail says which files it compares.- The
LICENSEandlicenses/of a binary package name only the modules built intoaszand the plugin.github.com/kr/textandgithub.com/rogpeppe/go-internal, which only the tests ofgopkg.in/yaml.v3need, are gone from them, because the ASF asks theLICENSEof a package to account for exactly what it holds.make dep-licensesandmake dep-licenses-checktake the modules for theLICENSEand theNOTICEalike fromgo list -depsof both binaries, on every platform inPLATFORMS. - Two builds of one tag give the same binary packages, byte for byte, when they use the same Go, the same tar and the same gzip. Go records the tag in each binary as the module’s version, so a build of the commit before it was tagged differs. Every file has the commit’s time, its owner and group are 0, and the entries are sorted. No package records who built it or when. GNU tar and bsdtar write their headers differently, and GNU gzip and Apple’s gzip compress differently, so a CI build, made with GNU tar and GNU gzip, and a macOS build, made with bsdtar and Apple’s gzip, differ.
- The compiled
collectorcheckandotlpdumphelpers, committed at the repository root by mistake, are removed, and.gitignorekeeps them out. Their sources stay undertools/. - Creating the GitHub release publishes the container image under the version the tag names, and
under
latestwhen it is the newest version, and nothing looser. Themajor.minorline tag,0.2beside0.2.0, is no longer published: a reader who pulled it could not tell which release answered. The0.2tag that 0.2.0 published stays on the registry and points at the same image as0.2.0.
Workspace changes
- Which files a tool call changed is shown beside the step. Two producers write one shape,
changes/1, defined bypkg/changes, whose id is the tool-use id and whosecaptured_bysays which producer observed it: the Claude Code adapter copies the runtime’s own patch from everyEditandWriteresult on the main stream into a seconddatapart beside the raw result, and the new asz Claude Code plugin,plugins/claude-code/, records shell commands, and edits inside subagents, into its own data directory. The plugin runs inside Claude Code’s hooks, one short process per event, needs nothing from asz, classifies read-only commands to skip their scans, excludes every known language’s build output by a frozen rule set, names overlapping windows on the same root, and keeps its output for 30 days. The newclaude-code-changesadapter, on by default, tails the plugin’s files and lands each line as a record of kindchangesunder the stream the tool ran in, with the session’s own lock and sequence, and the pipeline refreshes both sources in one pass. Session Data and Session Flow are unchanged:changesis a new kind, assembly emits no node for it, and a session folds to the same nodes with and without the records. asz.viewgainsworkspace_changes, every record joined to its step. A tool step names its records underchanges, andsummary.changescounts them. The version stays 1.0: no official release carries the format yet, so the keys are added rather than a version raised.- The plugin ships in every binary package under
claude-code-plugin/, andmake buildbuilds it besideasz. - The plugin’s hooks give Claude Code the binary as the command and
hookas its one argument, in the hook’sargs. Claude Code’s hooks reference says Claude Code then starts the command directly, with no shell, and that no shell splits the command into words on any platform. Claude Code 2.1.260 on macOS ran the hooks in this form, and the plugin’s record named the file a shell command created. The command line the hooks gave before went to a shell. The hooks reference says that on Windows without Git Bash, that shell is PowerShell. On macOS, PowerShell 7.5.3 refused that line, a quoted path followed by a word, and the binary did not start. Claude Code has not run either form on Windows. With no argument and its event on standard input, the binary now runs ashook, so a Claude Code that drops the argument does not block a tool with the usage text’s exit status 2. The hook command describes the form, and What was verified says what is still unknown on Windows.
Metrics
claude-code-localwithmetrics: truederives a reconstructed subset of the runtime’s own metric family from the landed files. Phase one derivesclaude_code.token.usage, under the name Claude Code’s OpenTelemetry exporter uses. The usage of a call is its last fragment’s in line order, and only a finished call counts. Points are per minute, by type, model, query source and session. The exporter’s account, speed, effort and attribution labels and its auxiliary calls are not reconstructed, and the export page states the difference. The points wait in the storage root’s_metrics/spool. They are sent over the metrics service, under asz’s identity, with the same budget and once-only rule as the files, at the end of each pass ofasz collectandasz server, and byasz push.metrics_lookback, 24 hours unless set, bounds the first derivation over a root with history, and so does the newest request the receiver landed, so a switch of source counts nothing twice. A pass is deterministic, names its requests after their landed files and saves its state once, so a pass cut short is run again to the same bytes. Every scenario checks the points on the wire against its plan, and the Collector job verifies the tokens that arrive from both sources.- A receiver’s partial success is a success the protocol says not to retry: the rejected records
or points are counted on the pass line as
rejected, and the files are marked sent. - A capture of what Claude Code 2.1.260 itself sent to the receiver, with every identifying value
replaced, is the parity fixture: a test holds the derived token metric to it, the same name,
unit, description, kind, value encoding and token types, the same session, model, query source
and type labels, and a difference that is exactly the labels and metrics the export page lists
as not derived. The derivation now writes doubles and a point for every type, zero included, as
the exporter does.
tools/otlpdumpprints a spooled request as the protocol’s JSON. claude-code-otlpis a second adapter for the runtime: an OpenTelemetry receiver its own exporter is pointed at, gRPC and HTTP with protobuf on onelistenport. Phase one lands the metrics requests it receives in the same spool, bytes as received, and they are sent from there as the derived points are. Logs and traces are accepted and dropped, counted in the status. It runs beside the local adapter underasz collectandasz server, and not with-once.metricsmay be on for one adapter, never both, and the configuration refuses to load otherwise, so the same tokens are never counted twice. A receiver takeslistenandmetricsonly. Collector settings on it are refused, since it polls nothing.export.otlp.logsandexport.otlp.metricsswitch the two things a push sends, the landed files and rounds as logs and the metrics spool as metrics, each on its own, both on unless set off.asz pushsays which it is sending, and a configuration with both off is refused.
Export
push.statenames the receiver its files were sent to, on anendpointline, and each file a receiver rejected records of, on arejectedline. Both lines are kept on every save. Apush.statewritten before this names no receiver. It is read as sent to an unknown receiver, so a scenario root that holds one removes nothing.- Over HTTP, a 2xx answer counts as sent only when its body is empty or protobuf, and a redirect is not followed. Before, an HTML page answered with 200, or a redirect to a login page, left files recorded as sent that no receiver stored.
Documentation
design-notes/is removed. What its three notes described that has landed is now documented in these pages, checked against the code rather than copied from the notes, which were older than the code. The notes remain in the git history. The documentation is the one reference.- Conversation Assembly now gives the size of the index that
design-notes/02-parse-and-assembly.mdmeasured: 47 MB of index for 1.0 GB of landed payload, about 21 times smaller. The note, as the tagv0.2.0holds it, said about 12 times for the same two numbers.